diff --git a/deps/OCCT/OCCT.cmake b/deps/OCCT/OCCT.cmake index b4cd9facd2..3ca4eaf9f5 100644 --- a/deps/OCCT/OCCT.cmake +++ b/deps/OCCT/OCCT.cmake @@ -28,7 +28,7 @@ orcaslicer_add_cmake_project(OCCT #-DBUILD_MODULE_DataExchange=OFF -DBUILD_MODULE_Draw=OFF -DBUILD_MODULE_FoundationClasses=OFF - -DBUILD_MODULE_ModelingAlgorithms=OFF + -DBUILD_MODULE_ModelingAlgorithms=ON -DBUILD_MODULE_ModelingData=OFF -DBUILD_MODULE_Visualization=OFF ) diff --git a/docs/design_tab_upstream_portability.md b/docs/design_tab_upstream_portability.md new file mode 100644 index 0000000000..1a475ee21d --- /dev/null +++ b/docs/design_tab_upstream_portability.md @@ -0,0 +1,58 @@ +# Design (CAD) tab — upstream portability assessment + +**Question:** can the SnapOrca *Design tab* (sketch-first parametric CAD: sketch → +constrain → extrude/revolve/fillet/hole/thread/shell, multi-body, undo) be contributed +to **mainline OrcaSlicer** (2.4 dev) rather than living only in this Snapmaker fork? + +**Short answer: yes, technically clean — the feature is self-contained and vendor-neutral. +The single real gatekeeper is whether upstream is willing to take on OpenCASCADE (OCCT) as a +build dependency, i.e. whether OrcaSlicer wants to become a CAD-integrated slicer.** + +## Why it is portable + +- **Self-contained.** The feature is ~28 kLOC of *new* files (kernel + `CadDocument` / `SketchEngine` / `GeometryEngine` / `Sketch*`; GUI + `DesignPanel` / `DesignCanvas` / `DesignSketchTool` / `GLGizmoSketch`) plus a + vendored, self-contained SolveSpace solver (`src/libslic3r/slvs/`, ~10 kLOC, LGPL). +- **Tiny, guarded injection into shared code (~2–3 % surface).** The only edits to + pre-existing OrcaSlicer files are: a `m_design_sketch_tool` member + a handful of + null-checked hooks in `GLCanvas3D` (render overlay, mouse routing, Delete/Esc/Ctrl+Z), + a tab member + construction in `MainFrame`, and a few forward declarations. **No + changes** to the slicing pipeline (Print/PrintObject/Layer/GCode), Plater, Tab, Model, + or the printer-profile/config system. +- **Zero Snapmaker coupling.** No "Snapmaker" references in any Design/CAD file; icons + are generic `resources/images/design_*.svg`; the code is pure wxWidgets/OpenGL/OCCT. + It would build and run in any OrcaSlicer fork unchanged. +- **Orthogonal git history.** The feature lives on `feature/cad-primitives` as a clean, + linear series of `Design:` / `C*` / `M*` commits with no interleaved fork-specific + work, so it cherry-picks onto a clean OrcaSlicer base without slicing-logic conflicts. + +## The real blocker: OCCT + +`src/libslic3r/CMakeLists.txt` links ~30 OCCT toolkits (`TKBRep TKFillet TKOffset +TKBool TKPrim TKTopAlgo TKMath TKernel …`) via `find_package(OpenCASCADE REQUIRED)`. +OCCT is large (hundreds of MB of binaries, +15–30 min to a clean deps build) and is a +dependency **mainline OrcaSlicer has never carried**. Accepting it is an architectural +decision about the project's scope, not a code problem. + +## Top work items to upstream (≈3–5 dev-days) + +1. **Make OCCT an *optional* dependency.** Add it to `deps/` like the other externals, + gate the whole feature behind a CMake option (`-DENABLE_DESIGN_CAD=OFF` by default), + and document OCCT install per-platform. Builds without OCCT simply omit the tab. +2. **Guard the tab construction** in `MainFrame` (and the `GLCanvas3D` hooks) on that + same flag so a minimal build links and runs with no Design code at all. +3. **Tests + docs + license hygiene.** Add regression coverage for the kernel + (sketch-solve, extrude, fillet, undo), ship the LGPL notice for the vendored + `slvs/`, and add user docs. (libslvs itself is self-contained, no external deps.) + +## Verdict + +Portability is **high (≈7/10): "needs moderate adaptation," not "deeply entangled."** +The engineering to upstream is modest and mechanical (optional-dependency plumbing + +tests/docs). The decision is strategic: **does OrcaSlicer want OCCT and a CAD tab?** If +yes, this feature is a near-drop-in starting point. + +--- +*Generated 2026-06-21 from a read-only analysis of the `feature/cad-primitives` branch. +Tracking issue: bd `snaporca-frp`.* diff --git a/resources/images/design_arc3pt.svg b/resources/images/design_arc3pt.svg new file mode 100644 index 0000000000..1b40569b4f --- /dev/null +++ b/resources/images/design_arc3pt.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_arc_center.svg b/resources/images/design_arc_center.svg new file mode 100644 index 0000000000..e4c85febfc --- /dev/null +++ b/resources/images/design_arc_center.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_array.svg b/resources/images/design_array.svg new file mode 100644 index 0000000000..d29da47bd7 --- /dev/null +++ b/resources/images/design_array.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_boolean.svg b/resources/images/design_boolean.svg new file mode 100644 index 0000000000..97813cf2eb --- /dev/null +++ b/resources/images/design_boolean.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/design_bspline.svg b/resources/images/design_bspline.svg new file mode 100644 index 0000000000..16a5dc9b31 --- /dev/null +++ b/resources/images/design_bspline.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_c_angle.svg b/resources/images/design_c_angle.svg new file mode 100644 index 0000000000..2359b23b6a --- /dev/null +++ b/resources/images/design_c_angle.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_c_coincident.svg b/resources/images/design_c_coincident.svg new file mode 100644 index 0000000000..83ab602eca --- /dev/null +++ b/resources/images/design_c_coincident.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_c_concentric.svg b/resources/images/design_c_concentric.svg new file mode 100644 index 0000000000..9861381b58 --- /dev/null +++ b/resources/images/design_c_concentric.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_c_diameter.svg b/resources/images/design_c_diameter.svg new file mode 100644 index 0000000000..8e3766c4dc --- /dev/null +++ b/resources/images/design_c_diameter.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_c_equal.svg b/resources/images/design_c_equal.svg new file mode 100644 index 0000000000..f0657b74b0 --- /dev/null +++ b/resources/images/design_c_equal.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_c_fix.svg b/resources/images/design_c_fix.svg new file mode 100644 index 0000000000..b64024286e --- /dev/null +++ b/resources/images/design_c_fix.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_c_horizontal.svg b/resources/images/design_c_horizontal.svg new file mode 100644 index 0000000000..9804a8ad82 --- /dev/null +++ b/resources/images/design_c_horizontal.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_c_midpoint.svg b/resources/images/design_c_midpoint.svg new file mode 100644 index 0000000000..6646a6cc1e --- /dev/null +++ b/resources/images/design_c_midpoint.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_c_parallel.svg b/resources/images/design_c_parallel.svg new file mode 100644 index 0000000000..cc78f9c452 --- /dev/null +++ b/resources/images/design_c_parallel.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_c_perpendicular.svg b/resources/images/design_c_perpendicular.svg new file mode 100644 index 0000000000..9a67cebb2b --- /dev/null +++ b/resources/images/design_c_perpendicular.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_c_radius.svg b/resources/images/design_c_radius.svg new file mode 100644 index 0000000000..5e1f1cdadf --- /dev/null +++ b/resources/images/design_c_radius.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_c_symmetric.svg b/resources/images/design_c_symmetric.svg new file mode 100644 index 0000000000..7a539be9a3 --- /dev/null +++ b/resources/images/design_c_symmetric.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_c_tangent.svg b/resources/images/design_c_tangent.svg new file mode 100644 index 0000000000..e82d23240d --- /dev/null +++ b/resources/images/design_c_tangent.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_c_vertical.svg b/resources/images/design_c_vertical.svg new file mode 100644 index 0000000000..98526e93d7 --- /dev/null +++ b/resources/images/design_c_vertical.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_chamfer.svg b/resources/images/design_chamfer.svg new file mode 100644 index 0000000000..861f7b819a --- /dev/null +++ b/resources/images/design_chamfer.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_check.svg b/resources/images/design_check.svg new file mode 100644 index 0000000000..82d5edd48e --- /dev/null +++ b/resources/images/design_check.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_circle.svg b/resources/images/design_circle.svg new file mode 100644 index 0000000000..edc74b13ea --- /dev/null +++ b/resources/images/design_circle.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_circle2pt.svg b/resources/images/design_circle2pt.svg new file mode 100644 index 0000000000..844b0797eb --- /dev/null +++ b/resources/images/design_circle2pt.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_circle3pt.svg b/resources/images/design_circle3pt.svg new file mode 100644 index 0000000000..6fdbab2474 --- /dev/null +++ b/resources/images/design_circle3pt.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_constrain.svg b/resources/images/design_constrain.svg new file mode 100644 index 0000000000..cc9be6ca9b --- /dev/null +++ b/resources/images/design_constrain.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_construction.svg b/resources/images/design_construction.svg new file mode 100644 index 0000000000..89d10f424d --- /dev/null +++ b/resources/images/design_construction.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_crect.svg b/resources/images/design_crect.svg new file mode 100644 index 0000000000..9e2ece0c24 --- /dev/null +++ b/resources/images/design_crect.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_cut.svg b/resources/images/design_cut.svg new file mode 100644 index 0000000000..3f68a7002e --- /dev/null +++ b/resources/images/design_cut.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/design_delete.svg b/resources/images/design_delete.svg new file mode 100644 index 0000000000..f5d5c6a1c7 --- /dev/null +++ b/resources/images/design_delete.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_dimension.svg b/resources/images/design_dimension.svg new file mode 100644 index 0000000000..f456abb6fc --- /dev/null +++ b/resources/images/design_dimension.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_draft.svg b/resources/images/design_draft.svg new file mode 100644 index 0000000000..2957584cb8 --- /dev/null +++ b/resources/images/design_draft.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_dressup.svg b/resources/images/design_dressup.svg new file mode 100644 index 0000000000..cc2a09250e --- /dev/null +++ b/resources/images/design_dressup.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_edit.svg b/resources/images/design_edit.svg new file mode 100644 index 0000000000..4de84fc390 --- /dev/null +++ b/resources/images/design_edit.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_ellipse.svg b/resources/images/design_ellipse.svg new file mode 100644 index 0000000000..1f946eb5fa --- /dev/null +++ b/resources/images/design_ellipse.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_ellipse_arc.svg b/resources/images/design_ellipse_arc.svg new file mode 100644 index 0000000000..e9cd4aa66a --- /dev/null +++ b/resources/images/design_ellipse_arc.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_extend.svg b/resources/images/design_extend.svg new file mode 100644 index 0000000000..9d4934fada --- /dev/null +++ b/resources/images/design_extend.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_extrude.svg b/resources/images/design_extrude.svg new file mode 100644 index 0000000000..8137336fb2 --- /dev/null +++ b/resources/images/design_extrude.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_eye.svg b/resources/images/design_eye.svg new file mode 100644 index 0000000000..97296c7465 --- /dev/null +++ b/resources/images/design_eye.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_filletedge.svg b/resources/images/design_filletedge.svg new file mode 100644 index 0000000000..435d7c2cb9 --- /dev/null +++ b/resources/images/design_filletedge.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_hole.svg b/resources/images/design_hole.svg new file mode 100644 index 0000000000..35128ef51e --- /dev/null +++ b/resources/images/design_hole.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_line.svg b/resources/images/design_line.svg new file mode 100644 index 0000000000..afb644ba3f --- /dev/null +++ b/resources/images/design_line.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_loft.svg b/resources/images/design_loft.svg new file mode 100644 index 0000000000..4529b52554 --- /dev/null +++ b/resources/images/design_loft.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_mirror.svg b/resources/images/design_mirror.svg new file mode 100644 index 0000000000..0a9642f8f3 --- /dev/null +++ b/resources/images/design_mirror.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_move.svg b/resources/images/design_move.svg new file mode 100644 index 0000000000..dba4f4f4c2 --- /dev/null +++ b/resources/images/design_move.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_movedown.svg b/resources/images/design_movedown.svg new file mode 100644 index 0000000000..89c442dc6e --- /dev/null +++ b/resources/images/design_movedown.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_moveup.svg b/resources/images/design_moveup.svg new file mode 100644 index 0000000000..b2fe5ec127 --- /dev/null +++ b/resources/images/design_moveup.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_offset.svg b/resources/images/design_offset.svg new file mode 100644 index 0000000000..74758ad0ba --- /dev/null +++ b/resources/images/design_offset.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_pattern.svg b/resources/images/design_pattern.svg new file mode 100644 index 0000000000..c07708509b --- /dev/null +++ b/resources/images/design_pattern.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_plane.svg b/resources/images/design_plane.svg new file mode 100644 index 0000000000..8a3886158f --- /dev/null +++ b/resources/images/design_plane.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_point.svg b/resources/images/design_point.svg new file mode 100644 index 0000000000..dd5d750545 --- /dev/null +++ b/resources/images/design_point.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_polararray.svg b/resources/images/design_polararray.svg new file mode 100644 index 0000000000..5e91d49f73 --- /dev/null +++ b/resources/images/design_polararray.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_polygon.svg b/resources/images/design_polygon.svg new file mode 100644 index 0000000000..de5cfd6f20 --- /dev/null +++ b/resources/images/design_polygon.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_polyline.svg b/resources/images/design_polyline.svg new file mode 100644 index 0000000000..96538abea7 --- /dev/null +++ b/resources/images/design_polyline.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_rect.svg b/resources/images/design_rect.svg new file mode 100644 index 0000000000..67b70cc6a1 --- /dev/null +++ b/resources/images/design_rect.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_rect_oblique.svg b/resources/images/design_rect_oblique.svg new file mode 100644 index 0000000000..5d252ca677 --- /dev/null +++ b/resources/images/design_rect_oblique.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_rect_rounded.svg b/resources/images/design_rect_rounded.svg new file mode 100644 index 0000000000..a8c7022c87 --- /dev/null +++ b/resources/images/design_rect_rounded.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_revolve.svg b/resources/images/design_revolve.svg new file mode 100644 index 0000000000..3a0e87f6f4 --- /dev/null +++ b/resources/images/design_revolve.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_rotate.svg b/resources/images/design_rotate.svg new file mode 100644 index 0000000000..5a9b48ebfe --- /dev/null +++ b/resources/images/design_rotate.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_scale.svg b/resources/images/design_scale.svg new file mode 100644 index 0000000000..5a53562ba3 --- /dev/null +++ b/resources/images/design_scale.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_select.svg b/resources/images/design_select.svg new file mode 100644 index 0000000000..6f2e4f4ae9 --- /dev/null +++ b/resources/images/design_select.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_shell.svg b/resources/images/design_shell.svg new file mode 100644 index 0000000000..aa11b572e6 --- /dev/null +++ b/resources/images/design_shell.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_sketch.svg b/resources/images/design_sketch.svg new file mode 100644 index 0000000000..102a9fe493 --- /dev/null +++ b/resources/images/design_sketch.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_slot.svg b/resources/images/design_slot.svg new file mode 100644 index 0000000000..f1c72f8c54 --- /dev/null +++ b/resources/images/design_slot.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_slot_arc.svg b/resources/images/design_slot_arc.svg new file mode 100644 index 0000000000..f17ab89144 --- /dev/null +++ b/resources/images/design_slot_arc.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_step.svg b/resources/images/design_step.svg new file mode 100644 index 0000000000..4bdf7811ff --- /dev/null +++ b/resources/images/design_step.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_svg.svg b/resources/images/design_svg.svg new file mode 100644 index 0000000000..34a0685153 --- /dev/null +++ b/resources/images/design_svg.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_sweep.svg b/resources/images/design_sweep.svg new file mode 100644 index 0000000000..f1cc301b1e --- /dev/null +++ b/resources/images/design_sweep.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_tangentarc.svg b/resources/images/design_tangentarc.svg new file mode 100644 index 0000000000..9e53da5c8e --- /dev/null +++ b/resources/images/design_tangentarc.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_text.svg b/resources/images/design_text.svg new file mode 100644 index 0000000000..78da223f62 --- /dev/null +++ b/resources/images/design_text.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_thread.svg b/resources/images/design_thread.svg new file mode 100644 index 0000000000..9bff4d778b --- /dev/null +++ b/resources/images/design_thread.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_trim.svg b/resources/images/design_trim.svg new file mode 100644 index 0000000000..405e9fe3c4 --- /dev/null +++ b/resources/images/design_trim.svg @@ -0,0 +1 @@ + diff --git a/resources/images/tab_design_active.svg b/resources/images/tab_design_active.svg new file mode 100644 index 0000000000..36ffc87ee5 --- /dev/null +++ b/resources/images/tab_design_active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/toolbar_modifier_cube_dark.svg b/resources/images/toolbar_modifier_cube_dark.svg new file mode 100644 index 0000000000..7e20cd0dd5 --- /dev/null +++ b/resources/images/toolbar_modifier_cube_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/toolbar_sketch.svg b/resources/images/toolbar_sketch.svg new file mode 100644 index 0000000000..351b3e7924 --- /dev/null +++ b/resources/images/toolbar_sketch.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/images/toolbar_sketch_dark.svg b/resources/images/toolbar_sketch_dark.svg new file mode 100644 index 0000000000..e5cf8d6859 --- /dev/null +++ b/resources/images/toolbar_sketch_dark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/scripts/Dockerfile.deps b/scripts/Dockerfile.deps new file mode 100644 index 0000000000..a73993fb64 --- /dev/null +++ b/scripts/Dockerfile.deps @@ -0,0 +1,70 @@ +# Deps-only base image for fast iteration on SnapOrca. +# Identical system+pinned-dependency setup to scripts/Dockerfile, but STOPS after +# `build_linux.sh -dr` (no slicer/AppImage build). Produces an image with the pinned +# deps baked at /OrcaSlicer/deps/build/destdir, so the slicer can be rebuilt +# incrementally via scripts/docker-iter-build.sh without re-running the long deps build. +# +# Build once (rebuild only when deps/ changes, e.g. OCCT module flags): +# docker build -t snaporca-deps -f scripts/Dockerfile.deps . +FROM docker.io/ubuntu:24.04 +LABEL maintainer="SnapOrca CAD iteration base" + +# Disable interactive package configuration +RUN apt-get update && \ + echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections + +# Add a deb-src +RUN echo deb-src http://archive.ubuntu.com/ubuntu \ + $(cat /etc/*release | grep VERSION_CODENAME | cut -d= -f2) main universe>> /etc/apt/sources.list + +RUN apt-get update && apt-get install -y \ + autoconf \ + build-essential \ + cmake \ + curl \ + eglexternalplatform-dev \ + extra-cmake-modules \ + file \ + git \ + gstreamer1.0-plugins-bad \ + gstreamer1.0-libav \ + libcairo2-dev \ + libcurl4-openssl-dev \ + libdbus-1-dev \ + libglew-dev \ + libglu1-mesa-dev \ + libgstreamer1.0-dev \ + libgstreamerd-3-dev \ + libgstreamer-plugins-base1.0-dev \ + libgstreamer-plugins-good1.0-dev \ + libgtk-3-dev \ + libsecret-1-dev \ + libsoup2.4-dev \ + libssl3 \ + libssl-dev \ + libtool \ + libudev-dev \ + libwayland-dev \ + libwebkit2gtk-4.1-dev \ + libxkbcommon-dev \ + locales \ + locales-all \ + m4 \ + pkgconf \ + sudo \ + wayland-protocols \ + wget + +ENV LC_ALL=en_US.utf8 +RUN locale-gen $LC_ALL +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt + +COPY ./ OrcaSlicer +WORKDIR OrcaSlicer + +# System dependencies +RUN ./build_linux.sh -u + +# Pinned dependencies in ./deps (OCCT 7.6 with ModelingAlgorithms enabled, OpenCV, +# OpenVDB, Boost, wxWidgets, ...). This is the long step; it is baked into the image. +RUN ./build_linux.sh -dr diff --git a/scripts/docker-iter-build.sh b/scripts/docker-iter-build.sh new file mode 100755 index 0000000000..04a71da7c8 --- /dev/null +++ b/scripts/docker-iter-build.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Incremental slicer build against the snaporca-deps base image. +# +# The deps-baked image (built from scripts/Dockerfile.deps) carries the pinned +# dependencies at /OrcaSlicer/deps/build/destdir. This script mounts the LIVE source +# tree and resources over the baked copy so code/CMake edits apply immediately, and +# persists /OrcaSlicer/build in a named volume so ninja recompiles only what changed. +# +# Result: edit -> rebuild in seconds-to-minutes instead of a full Docker rebuild. +# +# Usage (run on the build host, e.g. behemoth, from anywhere): +# scripts/docker-iter-build.sh +# IMAGE=snaporca-deps scripts/docker-iter-build.sh +# +# On success the binary is inside the persistent volume at +# /OrcaSlicer/build/package/bin/snapmaker-orca (copy it out with a follow-up +# `docker run --rm -v snaporca_buildcache:/b alpine cp ...` or via this script's tail). +set -euo pipefail + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +IMAGE="${IMAGE:-snaporca-deps}" +BUILD_VOL="${BUILD_VOL:-snaporca_buildcache}" + +echo "REPO=$REPO IMAGE=$IMAGE BUILD_VOL=$BUILD_VOL" + +docker run --rm \ + -v "$REPO/src":/OrcaSlicer/src \ + -v "$REPO/resources":/OrcaSlicer/resources \ + -v "$BUILD_VOL":/OrcaSlicer/build \ + "$IMAGE" \ + bash -lc 'cd /OrcaSlicer && ./build_linux.sh -sr' + +echo "=== build finished; checking for binary ===" +docker run --rm -v "$BUILD_VOL":/b "$IMAGE" \ + bash -lc 'ls -lh /b/package/bin/snapmaker-orca 2>/dev/null && file /b/package/bin/snapmaker-orca || echo "NO BINARY"' diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 9b67d7ab50..08975ff928 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -20,6 +20,9 @@ endif() option(BUILD_SHARED_LIBS "Build shared libs" OFF) +# Vendored SolveSpace constraint solver (2D sketch solver backbone). +add_subdirectory(slvs) + set(lisbslic3r_sources AABBMesh.cpp AABBMesh.hpp @@ -275,6 +278,8 @@ set(lisbslic3r_sources Geometry/VoronoiUtils.cpp Geometry/VoronoiUtils.hpp Geometry/VoronoiVisualUtils.hpp + GeometryEngine.cpp + GeometryEngine.hpp Int128.hpp KDTreeIndirect.hpp Layer.cpp @@ -385,6 +390,20 @@ set(lisbslic3r_sources SLA/JobController.hpp SLA/Pad.cpp SLA/Pad.hpp + SketchEngine.cpp + SketchEngine.hpp + SketchConstraints.cpp + SketchConstraints.hpp + SketchSolver.cpp + SketchSolver.hpp + SketchInference.cpp + SketchInference.hpp + SketchImport.cpp + SketchImport.hpp + CadDocument.cpp + CadDocument.hpp + ThreadStandards.cpp + ThreadStandards.hpp SLAPrint.cpp SLAPrint.hpp SLAPrintSteps.cpp @@ -546,6 +565,9 @@ find_package(JPEG REQUIRED) find_package(draco REQUIRED) set(OCCT_LIBS + TKFillet + TKOffset + TKBool TKXDESTEP TKSTEP TKSTEP209 @@ -591,6 +613,7 @@ target_link_libraries(libslic3r clipper Clipper2 draco::draco + libslvs glu-libtess JPEG::JPEG libslic3r_cgal diff --git a/src/libslic3r/CadDocument.cpp b/src/libslic3r/CadDocument.cpp new file mode 100644 index 0000000000..19ed9ce665 --- /dev/null +++ b/src/libslic3r/CadDocument.cpp @@ -0,0 +1,1540 @@ +#include "CadDocument.hpp" +#include "SketchConstraints.hpp" +#include "SketchSolver.hpp" +#include "SketchImport.hpp" // transform_regions for imported art + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // multi-body: compound of bodies for display/compat +#include +#include // outward-normal orientation for face-extrude +#include +#include +#include +#include +#include +#include // pattern: rigid copy transforms +#include // pattern: rotation axis (circular) +#include +#include +#include +#include + +namespace Slic3r { + +// ---- helical-thread construction helpers (file-local) ---------------------- + +// Helix spine on a cylinder (radius/pitch/height) about `axis`, as a wire. +static TopoDS_Wire make_helix_wire(const gp_Ax3& axis, double radius, + double pitch, double height) +{ + Handle(Geom_CylindricalSurface) cyl = new Geom_CylindricalSurface(axis, radius); + double turns = (pitch > 1e-6) ? (height / pitch) : 1.0; + // In the surface (u,v) parametrization u is the angle, v the axial height. + gp_Pnt2d p0(0.0, 0.0); + gp_Pnt2d p1(2.0 * M_PI * turns, height); + Handle(Geom2d_TrimmedCurve) seg = GCE2d_MakeSegment(p0, p1); + TopoDS_Edge e = BRepBuilderAPI_MakeEdge(seg, cyl).Edge(); + BRepLib::BuildCurves3d(e); + return BRepBuilderAPI_MakeWire(e).Wire(); +} + +// Triangular axial thread profile (a planar face) placed at the helix start +// (origin + radius*xdir). Spans +-pitch/2 axially; apex offset radially by depth. +// Both thread kinds sweep the SAME outward-biting V (base on the cylinder wall, +// apex `depth` into the surrounding material). Only the boolean differs: +// - external: the V is FUSED to the rod -> a raised helical ridge. +// - internal: the V is CUT from the wall -> a sunken helical groove. The cut MUST +// go outward into the wall to be visible; an inward V (the old behaviour) only +// sweeps already-empty bore space and removes nothing. +static TopoDS_Face make_thread_profile(const gp_Pnt& origin, const gp_Dir& xdir, + const gp_Dir& zdir, double radius, + double pitch, double depth, bool internal) +{ + (void)internal; + gp_Vec vx(xdir), vz(zdir); + double inner = radius - 0.05; // base, just inside the wall (overlaps rod / open bore) + double crest = radius + depth; // apex, `depth` into the surrounding material + gp_Pnt top (origin.XYZ() + (vx * inner).XYZ() + (vz * ( 0.5 * pitch)).XYZ()); + gp_Pnt bot (origin.XYZ() + (vx * inner).XYZ() + (vz * (-0.5 * pitch)).XYZ()); + gp_Pnt apex(origin.XYZ() + (vx * crest).XYZ()); + BRepBuilderAPI_MakePolygon poly(top, bot, apex, Standard_True); + return BRepBuilderAPI_MakeFace(poly.Wire(), Standard_True).Face(); +} + +// --------------------------------------------------------------------------- + +int CadDocument::add_sketch(SketchShape shape, const SketchPlane& plane, + double width, double height, double radius, + const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::Sketch; + f.name = name; + f.shape = shape; + f.plane = plane; + f.width = width; + f.height = height; + f.radius = radius; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_sketch_profile(const SketchProfile& profile, const SketchPlane& plane, + const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::Sketch; + f.name = name; + f.plane = plane; + f.profile = profile; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_sketch_entities(const std::vector& entities, + const SketchPlane& plane, const std::string& name, + const std::vector& constraints) +{ + CadFeature f; + f.type = CadFeatureType::Sketch; + f.name = name; + f.plane = plane; + f.entities = entities; + f.entity_constraints = constraints; // driving dimensions, solved by solve_sketch_feature + features.push_back(f); + return int(features.size()) - 1; +} + +// Solve Onshape-style constraints on a SketchEntity list (Fase 4.3). All entity +// types participate: Line (P0,P1), Arc (P0,P1,Center), Circle (Center), Point (P0). +// Solved coordinates are written back, with arc angles reflowed from the solved +// center+endpoints. Free function (declared in SketchEngine.hpp) so the in-session +// GUI sketch tool can live-solve the same way committed features do. +bool solve_sketch_entities(std::vector& entities, + const std::vector& constraints) +{ + // Delegated to the vendored SolveSpace solver (SketchSolver / libslvs): full + // constraint set, real DoF + over-constrained detection. + return sketch_solve(entities, constraints).ok; +} + +#if 0 // legacy hand-rolled Gauss-Newton solver — superseded by libslvs, kept for reference +static bool legacy_solve_sketch_entities(std::vector& entities, + const std::vector& constraints) +{ + if (constraints.empty()) return true; + + SketchConstraints sc; + // table[entity][role] -> solver point id, or -1 if that role is unregistered. + std::vector> table(entities.size(), {-1, -1, -1}); + auto reg = [&](int ei, SketchPointRole role, const Vec2d& p) { + table[ei][int(role)] = sc.add_point(p.x(), p.y()); + }; + for (size_t i = 0; i < entities.size(); ++i) { + const SketchEntity& e = entities[i]; + switch (e.type) { + case SketchEntity::Type::Line: + reg(int(i), SketchPointRole::P0, e.p0); + reg(int(i), SketchPointRole::P1, e.p1); + break; + case SketchEntity::Type::Arc: + reg(int(i), SketchPointRole::P0, e.p0); + reg(int(i), SketchPointRole::P1, e.p1); + reg(int(i), SketchPointRole::Center, e.center); + break; + case SketchEntity::Type::Circle: + reg(int(i), SketchPointRole::Center, e.center); + break; + case SketchEntity::Type::Point: + reg(int(i), SketchPointRole::P0, e.p0); + break; + } + } + + auto pid = [&](int ei, SketchPointRole role) -> int { + if (ei < 0 || ei >= int(table.size())) return -1; + return table[ei][int(role)]; + }; + + for (const SketchEntityConstraintDef& c : constraints) { + switch (c.type) { + // Point-form: refs A and B name individual entity points. + case SketchConstraintType::Fix: { + int a = pid(c.ea, c.ra); + if (a >= 0) sc.fix_point(a); + break; + } + case SketchConstraintType::Coincident: { + int a = pid(c.ea, c.ra), b = pid(c.eb, c.rb); + if (a >= 0 && b >= 0) sc.coincident(a, b); + break; + } + case SketchConstraintType::Horizontal: { + int a = pid(c.ea, c.ra), b = pid(c.eb, c.rb); + if (a >= 0 && b >= 0) sc.horizontal(a, b); + break; + } + case SketchConstraintType::Vertical: { + int a = pid(c.ea, c.ra), b = pid(c.eb, c.rb); + if (a >= 0 && b >= 0) sc.vertical(a, b); + break; + } + case SketchConstraintType::Distance: { + int a = pid(c.ea, c.ra), b = pid(c.eb, c.rb); + if (a >= 0 && b >= 0) sc.distance(a, b, c.value); + break; + } + case SketchConstraintType::LockX: { + int a = pid(c.ea, c.ra); + if (a >= 0) sc.lock_x(a, c.value); + break; + } + case SketchConstraintType::LockY: { + int a = pid(c.ea, c.ra); + if (a >= 0) sc.lock_y(a, c.value); + break; + } + // Segment-form: ea and eb name whole line segments (their P0->P1). + case SketchConstraintType::Parallel: + case SketchConstraintType::Perpendicular: + case SketchConstraintType::EqualLength: { + int a0 = pid(c.ea, SketchPointRole::P0), a1 = pid(c.ea, SketchPointRole::P1); + int b0 = pid(c.eb, SketchPointRole::P0), b1 = pid(c.eb, SketchPointRole::P1); + if (a0 < 0 || a1 < 0 || b0 < 0 || b1 < 0) break; + if (c.type == SketchConstraintType::Parallel) sc.parallel(a0, a1, b0, b1); + else if (c.type == SketchConstraintType::Perpendicular) sc.perpendicular(a0, a1, b0, b1); + else sc.equal_length(a0, a1, b0, b1); + break; + } + case SketchConstraintType::Concentric: { + int a = pid(c.ea, SketchPointRole::Center); + int b = pid(c.eb, SketchPointRole::Center); + if (a >= 0 && b >= 0) sc.coincident(a, b); + break; + } + case SketchConstraintType::Midpoint: { + int m = pid(c.ea, c.ra); + int a = pid(c.eb, SketchPointRole::P0); + int b = pid(c.eb, SketchPointRole::P1); + if (m >= 0 && a >= 0 && b >= 0) sc.midpoint(m, a, b); + break; + } + case SketchConstraintType::Symmetric: { + int a = pid(c.ea, c.ra); + int b = pid(c.eb, c.rb); + int x0 = pid(c.ec, SketchPointRole::P0); + int x1 = pid(c.ec, SketchPointRole::P1); + if (a >= 0 && b >= 0 && x0 >= 0 && x1 >= 0) sc.symmetric(a, b, x0, x1); + break; + } + case SketchConstraintType::Angle: { + int a0 = pid(c.ea, SketchPointRole::P0), a1 = pid(c.ea, SketchPointRole::P1); + int b0 = pid(c.eb, SketchPointRole::P0), b1 = pid(c.eb, SketchPointRole::P1); + if (a0 >= 0 && a1 >= 0 && b0 >= 0 && b1 >= 0) sc.angle(a0, a1, b0, b1, c.value); + break; + } + case SketchConstraintType::Radius: + case SketchConstraintType::Diameter: + // dimensions: applied in the post-solve pass below, not via the solver. + break; + case SketchConstraintType::PointOnLine: { + // Point `ea`/`ra` is held at signed perpendicular distance `value` from + // line `eb` (value 0 -> on the line). Drives e.g. a circle centre onto a + // construction axis and keeps it there through later edits. + int p = pid(c.ea, c.ra); + int l0 = pid(c.eb, SketchPointRole::P0), l1 = pid(c.eb, SketchPointRole::P1); + if (p >= 0 && l0 >= 0 && l1 >= 0) sc.point_line_distance(p, l0, l1, c.value); + break; + } + case SketchConstraintType::Tangent: { + auto in_range = [&](int e){ return e >= 0 && e < (int)entities.size(); }; + if (!in_range(c.ea) || !in_range(c.eb)) break; + const SketchEntity& ea_e = entities[c.ea]; + const SketchEntity& eb_e = entities[c.eb]; + auto is_round = [](const SketchEntity& e){ + return e.type == SketchEntity::Type::Circle || e.type == SketchEntity::Type::Arc; }; + if (is_round(ea_e) && eb_e.type == SketchEntity::Type::Line) { + int cen = pid(c.ea, SketchPointRole::Center); + int l0 = pid(c.eb, SketchPointRole::P0), l1 = pid(c.eb, SketchPointRole::P1); + if (cen >= 0 && l0 >= 0 && l1 >= 0) sc.point_line_distance(cen, l0, l1, ea_e.radius); + } else if (is_round(eb_e) && ea_e.type == SketchEntity::Type::Line) { + int cen = pid(c.eb, SketchPointRole::Center); + int l0 = pid(c.ea, SketchPointRole::P0), l1 = pid(c.ea, SketchPointRole::P1); + if (cen >= 0 && l0 >= 0 && l1 >= 0) sc.point_line_distance(cen, l0, l1, eb_e.radius); + } else if (is_round(ea_e) && is_round(eb_e)) { + int c0 = pid(c.ea, SketchPointRole::Center), c1 = pid(c.eb, SketchPointRole::Center); + if (c0 >= 0 && c1 >= 0) sc.distance(c0, c1, ea_e.radius + eb_e.radius); + } + break; + } + } + } + + const bool ok = sc.solve(); + // Write solved coordinates back into the participating entities. + for (size_t i = 0; i < entities.size(); ++i) { + SketchEntity& e = entities[i]; + int ip0 = table[i][int(SketchPointRole::P0)]; + int ip1 = table[i][int(SketchPointRole::P1)]; + int ic = table[i][int(SketchPointRole::Center)]; + if (ip0 >= 0) e.p0 = sc.get_point(ip0); + if (ip1 >= 0) e.p1 = sc.get_point(ip1); + if (ic >= 0) e.center = sc.get_point(ic); + + if (e.type == SketchEntity::Type::Arc && ic >= 0) { + // Reflow arc angles from solved center + endpoints, preserving the + // original sweep direction (CCW vs CW). + const double old_sweep = e.end_angle - e.start_angle; // signed, original + double ns = std::atan2(e.p0.y() - e.center.y(), e.p0.x() - e.center.x()); + double ne = std::atan2(e.p1.y() - e.center.y(), e.p1.x() - e.center.x()); + double sweep = ne - ns; + // Normalize `sweep` into (-2pi, 2pi) then match the sign of old_sweep so + // the arc keeps turning the same way it did before solving. + const double TWO_PI = 2.0 * M_PI; + while (sweep <= -TWO_PI) sweep += TWO_PI; + while (sweep >= TWO_PI) sweep -= TWO_PI; + if (old_sweep >= 0.0 && sweep < 0.0) sweep += TWO_PI; + if (old_sweep < 0.0 && sweep > 0.0) sweep -= TWO_PI; + e.start_angle = ns; + e.end_angle = ns + sweep; + e.radius = 0.5 * ((e.p0 - e.center).norm() + (e.p1 - e.center).norm()); + } + if (e.type == SketchEntity::Type::Circle && ic >= 0) { + // p0 mirrors the center for circles; keep them consistent. + e.p0 = e.center; + } + } + // Apply radius/diameter dimensions directly (radius is not a solver variable). + for (const auto& c : constraints) { + if (c.type != SketchConstraintType::Radius && + c.type != SketchConstraintType::Diameter) continue; + if (c.ea < 0 || c.ea >= (int)entities.size()) continue; + SketchEntity& e = entities[c.ea]; + if (e.type != SketchEntity::Type::Circle && e.type != SketchEntity::Type::Arc) continue; + const double r = (c.type == SketchConstraintType::Diameter) ? 0.5 * c.value : c.value; + if (r <= 0.0) continue; + e.radius = r; + if (e.type == SketchEntity::Type::Arc) { + // Rescale endpoints to the new radius around the (solved) center, keeping + // each endpoint's direction so the reflowed start/end angles stay valid. + auto rescale = [&](Vec2d& p) { + Vec2d d = p - e.center; + const double n = d.norm(); + if (n > 1e-12) p = e.center + (r / n) * d; + }; + rescale(e.p0); + rescale(e.p1); + } + } + return ok; +} +#endif // legacy solver + +bool CadDocument::solve_sketch_feature(int index) +{ + if (index < 0 || index >= int(features.size())) return false; + CadFeature& f = features[index]; + if (f.type != CadFeatureType::Sketch) return false; + + // Onshape-style entity sketches solve against entity endpoints (Fase 4.2). + if (!f.entities.empty()) + return solve_sketch_entities(f.entities, f.entity_constraints); + + if (f.constraints.empty()) return true; + + SketchConstraints sc; + for (const Vec2d& p : f.profile.points) + sc.add_point(p.x(), p.y()); + + for (const SketchConstraintDef& c : f.constraints) { + switch (c.type) { + case SketchConstraintType::Fix: sc.fix_point(c.a); break; + case SketchConstraintType::Coincident: sc.coincident(c.a, c.b); break; + case SketchConstraintType::Horizontal: sc.horizontal(c.a, c.b); break; + case SketchConstraintType::Vertical: sc.vertical(c.a, c.b); break; + case SketchConstraintType::Distance: sc.distance(c.a, c.b, c.value); break; + case SketchConstraintType::LockX: sc.lock_x(c.a, c.value); break; + case SketchConstraintType::LockY: sc.lock_y(c.a, c.value); break; + case SketchConstraintType::EqualLength: sc.equal_length(c.a, c.b, c.c, c.d); break; + case SketchConstraintType::Parallel: sc.parallel(c.a, c.b, c.c, c.d); break; + case SketchConstraintType::Perpendicular:sc.perpendicular(c.a, c.b, c.c, c.d); break; + } + } + + const bool ok = sc.solve(); + for (size_t i = 0; i < f.profile.points.size(); ++i) + f.profile.points[i] = sc.get_point(int(i)); + return ok; +} + +int CadDocument::add_extrude(int sketch_ref, double distance, bool symmetric, + BooleanMode mode, const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::Extrude; + f.name = name; + f.sketch_ref = sketch_ref; + f.distance = distance; + f.symmetric = symmetric; + f.mode = mode; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_extrude_entities(const std::vector& entities, + const SketchPlane& plane, double distance, + bool symmetric, BooleanMode mode, const std::string& name) +{ + // Self-contained extrude of a single loop: the entity subset lives on the feature + // itself (sketch_ref = -1), so build_sketch_wire(f) uses f.entities directly. The + // source sketch stays a separate feature, so its other loops remain selectable. + CadFeature f; + f.type = CadFeatureType::Extrude; + f.name = name; + f.sketch_ref = -1; + f.entities = entities; + f.plane = plane; + f.distance = distance; + f.symmetric = symmetric; + f.mode = mode; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_extrude_face(int src_face, double distance, bool symmetric, + BooleanMode mode, const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::Extrude; + f.name = name; + f.sketch_ref = -1; + f.extrude_src_face = src_face; + f.distance = distance; + f.symmetric = symmetric; + f.mode = mode; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_fillet(double radius, FaceGroup faces, const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::Fillet; + f.name = name; + f.dressup_size = radius; + f.face_group = faces; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_fillet(double radius, int edge_id, const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::Fillet; + f.name = name; + f.dressup_size = radius; + f.dressup_edge = edge_id; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_chamfer(double distance, FaceGroup faces, const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::Chamfer; + f.name = name; + f.dressup_size = distance; + f.face_group = faces; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_chamfer(double distance, int edge_id, const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::Chamfer; + f.name = name; + f.dressup_size = distance; + f.dressup_edge = edge_id; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_hole(double diameter, double depth, bool through, + double x, double y, const SketchPlane& plane, + const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::Hole; + f.name = name; + f.plane = plane; + f.hole_diameter = diameter; + f.hole_depth = depth; + f.hole_through = through; + f.hole_x = x; + f.hole_y = y; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_thread(double radius, double pitch, double height, double depth, + bool internal, double x, double y, const SketchPlane& plane, + const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::Thread; + f.name = name; + f.plane = plane; + f.thread_radius = radius; + f.thread_pitch = pitch; + f.thread_height = height; + f.thread_depth = depth; + f.thread_internal = internal; + f.thread_x = x; + f.thread_y = y; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_revolve(int sketch_ref, double angle, int axis, bool flip, + BooleanMode mode, const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::Revolve; + f.name = name; + f.sketch_ref = sketch_ref; + f.revolve_angle = angle; + f.revolve_axis = axis; + f.flip = flip; + f.mode = mode; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_revolve_entities(const std::vector& entities, + const SketchPlane& plane, double angle, int axis, + bool flip, BooleanMode mode, const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::Revolve; + f.name = name; + f.sketch_ref = -1; + f.entities = entities; + f.plane = plane; + f.revolve_angle = angle; + f.revolve_axis = axis; + f.flip = flip; + f.mode = mode; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_sweep(int profile_sketch_ref, int path_sketch_ref, BooleanMode mode, + const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::Sweep; + f.name = name; + f.sketch_ref = profile_sketch_ref; + f.sweep_path_ref = path_sketch_ref; + f.mode = mode; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_loft(const std::vector& profile_refs, bool ruled, BooleanMode mode, + const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::Loft; + f.name = name; + f.loft_profile_refs = profile_refs; + f.loft_ruled = ruled; + f.mode = mode; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_pattern(bool circular, int count, double spacing, int dir, + double angle_deg, int target_body, const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::Pattern; + f.name = name; + f.pattern_circular = circular; + f.pattern_count = count; + f.pattern_spacing = spacing; + f.pattern_dir = dir; + f.pattern_angle = angle_deg; + f.target_body = target_body; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_shell(double thickness, int face, int target_body, const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::Shell; + f.name = name; + f.shell_thickness = thickness; + f.shell_face = face; + f.target_body = target_body; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_draft(double angle, int face, int target_body, const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::Draft; + f.name = name; + f.draft_angle = angle; + f.draft_face = face; + f.target_body = target_body; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_boolean(BooleanMode op, int target_body, int tool_body, bool keep_tool, + double tolerance, int target_face, int tool_face, + const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::Boolean; + f.name = name; + f.mode = op; + f.target_body = target_body; + f.bool_tool_body = tool_body; + f.bool_keep_tool = keep_tool; + f.bool_tolerance = tolerance; + f.bool_target_face = target_face; + f.bool_tool_face = tool_face; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_cut(const SketchPlane& plane, double offset, bool flip, + bool keep_upper, bool keep_lower, int target_body, + const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::Cut; + f.name = name; + f.plane = plane; + f.cut_offset = offset; + f.cut_flip = flip; + f.cut_keep_upper = keep_upper; + f.cut_keep_lower = keep_lower; + f.target_body = target_body; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_plane(int base, double offset, double angle_tilt, int axis, + const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::Plane; + f.name = name; + f.plane_base = base; + f.plane_offset = offset; + f.plane_angle_tilt = angle_tilt; + f.plane_axis = axis; + features.push_back(f); + return int(features.size()) - 1; +} + +// Derive a SketchPlane: shift `base` along its normal by `offset`, then tilt +// `angle_deg` about the base's X (axis 0) or Y (axis 1) axis (Rodrigues rotation). +static SketchPlane offset_angle_plane(const SketchPlane& base, double offset, + double angle_deg, int axis) +{ + SketchPlane p; + p.origin = base.origin + base.normal * offset; + Vec3d n = base.normal, x = base.x_axis, y = base.y_axis; + if (std::abs(angle_deg) > 1e-9) { + const double a = angle_deg * M_PI / 180.0; + const Vec3d k = (axis == 1) ? base.y_axis : base.x_axis; // unit rotation axis + auto rot = [&](const Vec3d& v) -> Vec3d { // -> Vec3d forces eval (no dangling Eigen expr) + return v * std::cos(a) + k.cross(v) * std::sin(a) + + k * (k.dot(v)) * (1.0 - std::cos(a)); + }; + n = rot(n); + if (axis == 1) x = rot(x); // tilt about Y rotates X + normal, Y fixed + else y = rot(y); // tilt about X rotates Y + normal, X fixed + } + p.normal = n.normalized(); + p.x_axis = x.normalized(); + p.y_axis = y.normalized(); + return p; +} + +std::vector> CadDocument::resolve_datum_planes() const +{ + std::vector> out; + for (const CadFeature& f : features) { + if (f.type != CadFeatureType::Plane || !f.enabled) continue; + SketchPlane base; + if (f.plane_base == 1) base = SketchPlane::XZ(); + else if (f.plane_base == 2) base = SketchPlane::YZ(); + else if (f.plane_base >= 3) { + const int di = f.plane_base - 3; // index into earlier datum planes + if (di < int(out.size())) base = out[di].second; // else XY default + } + out.emplace_back(f.name, + offset_angle_plane(base, f.plane_offset, f.plane_angle_tilt, f.plane_axis)); + } + return out; +} + +void CadDocument::clear() +{ + features.clear(); + body = TopoDS_Shape(); + display_mesh = TriangleMesh{}; + display_tri_face.clear(); + error.clear(); + // A cleared document is a fresh start with no history. + m_undo.clear(); + m_redo.clear(); +} + +void CadDocument::checkpoint() +{ + m_undo.push_back(features); // snapshot the pre-mutation recipe + m_redo.clear(); // any new action invalidates the redo branch + if (m_undo.size() > k_undo_cap) + m_undo.erase(m_undo.begin()); +} + +bool CadDocument::undo() +{ + if (m_undo.empty()) + return false; + m_redo.push_back(std::move(features)); // current state becomes redoable + features = std::move(m_undo.back()); + m_undo.pop_back(); + recompute(); // benign-empty (only a sketch / empty doc) is a valid undo target + return true; +} + +bool CadDocument::redo() +{ + if (m_redo.empty()) + return false; + m_undo.push_back(std::move(features)); + features = std::move(m_redo.back()); + m_redo.pop_back(); + recompute(); + return true; +} + +// Re-run recompute(); if it fails for a GENUINE geometry error, restore `snapshot` +// and recompute that instead, so a rejected edit leaves the document exactly as it +// was. recompute() also returns false for the BENIGN case where the edit simply +// leaves no solid-producing feature (empty document, or only a sketch) — that is a +// valid result of a deletion, not a failure, so we accept it with an empty body. +static bool commit_or_rollback(CadDocument& doc, std::vector& snapshot) +{ + if (doc.recompute()) + return true; + + bool has_solid_feature = false; + for (const auto& f : doc.features) + if (f.enabled && f.type != CadFeatureType::Sketch) { has_solid_feature = true; break; } + if (!has_solid_feature) { + doc.bodies.clear(); + doc.body = TopoDS_Shape(); + doc.display_mesh = TriangleMesh{}; + doc.display_body_meshes.clear(); + doc.display_tri_face.clear(); + doc.display_tri_body.clear(); + doc.error.clear(); + return true; + } + + std::string fail_err = doc.error; // why the attempted edit failed + doc.features.swap(snapshot); + doc.recompute(); // restore the previous good body (clears error) + doc.error = fail_err.empty() ? std::string("feature is used by a later feature") + : fail_err; + return false; +} + +bool CadDocument::remove_feature(int index) +{ + if (index < 0 || index >= int(features.size())) + return false; + + std::vector snapshot = features; + + // Deleting a Sketch cascades to every Extrude that consumes it (a dangling + // Extrude would have no wire). A lone Sketch, by contrast, is harmless. + std::vector remove{index}; + if (features[index].type == CadFeatureType::Sketch) { + for (int j = 0; j < int(features.size()); ++j) + if (features[j].type == CadFeatureType::Extrude && features[j].sketch_ref == index) + remove.push_back(j); + } + std::sort(remove.begin(), remove.end()); + remove.erase(std::unique(remove.begin(), remove.end()), remove.end()); + + // Erase high-to-low so earlier indices stay valid. + for (auto it = remove.rbegin(); it != remove.rend(); ++it) + features.erase(features.begin() + *it); + + // Remap surviving sketch_ref through the deletions: subtract the count of + // removed indices that sat before it; orphaned refs (target removed) -> -1. + for (auto& f : features) { + if (f.type != CadFeatureType::Extrude || f.sketch_ref < 0) + continue; + if (std::binary_search(remove.begin(), remove.end(), f.sketch_ref)) { + f.sketch_ref = -1; + } else { + int shift = 0; + for (int r : remove) + if (r < f.sketch_ref) ++shift; + f.sketch_ref -= shift; + } + } + + return commit_or_rollback(*this, snapshot); +} + +bool CadDocument::move_feature(int index, int delta) +{ + if (index < 0 || index >= int(features.size())) + return false; + int target = index + delta; + if (target < 0 || target >= int(features.size())) + return true; // clamped at the ends — no-op, not a failure + + std::vector snapshot = features; + std::swap(features[index], features[target]); + + // The two slots traded places: fix any sketch_ref that pointed at either. + for (auto& f : features) { + if (f.type != CadFeatureType::Extrude) continue; + if (f.sketch_ref == index) f.sketch_ref = target; + else if (f.sketch_ref == target) f.sketch_ref = index; + } + + return commit_or_rollback(*this, snapshot); +} + +bool CadDocument::replace_feature(int index, const CadFeature& edited) +{ + if (index < 0 || index >= int(features.size())) + return false; + + std::vector snapshot = features; + + // Preserve identity (name) and the structural link (sketch_ref) from the + // original; only the user-editable parameters come from `edited`. + CadFeature f = edited; + f.name = features[index].name; + f.type = features[index].type; + if (f.type == CadFeatureType::Extrude) + f.sketch_ref = features[index].sketch_ref; + features[index] = f; + + return commit_or_rollback(*this, snapshot); +} + +bool CadDocument::replace_sketch_extrude(int sketch_idx, int extrude_idx, + const CadFeature& edited) +{ + if (sketch_idx < 0 || sketch_idx >= int(features.size())) return false; + if (extrude_idx < 0 || extrude_idx >= int(features.size())) return false; + + std::vector snapshot = features; + + // A box in the tree is two linked features: the Sketch consumes the profile + // params (shape/plane/width/height/radius), the Extrude consumes the solid + // params (distance/symmetric/mode). `edited` carries all of them; split it + // back into the two slots, preserving each slot's name/type and the link. + CadFeature& sk = features[sketch_idx]; + sk.shape = edited.shape; + sk.plane = edited.plane; + sk.width = edited.width; + sk.height = edited.height; + sk.radius = edited.radius; + + CadFeature& ex = features[extrude_idx]; + ex.distance = edited.distance; + ex.symmetric = edited.symmetric; + ex.mode = edited.mode; + + return commit_or_rollback(*this, snapshot); +} + +TopoDS_Wire CadDocument::build_sketch_wire(const CadFeature& sketch) const +{ + if (!sketch.entities.empty()) { + TopoDS_Wire w = SketchEngine::entities_to_wire(sketch.entities, sketch.plane); + if (!w.IsNull()) return w; + // fall through to legacy paths if entities produced nothing + } + if (!sketch.profile.points.empty()) { + SketchProfile prof = sketch.profile; + prof.closed = true; // extrude needs a closed wire + TopoDS_Wire w = prof.to_occt_wire(sketch.plane); + if (w.IsNull()) throw std::runtime_error("sketch profile wire failed"); + return w; + } + if (sketch.shape == SketchShape::Circle) { + gp_Pnt o(sketch.plane.origin.x(), sketch.plane.origin.y(), sketch.plane.origin.z()); + gp_Dir n(sketch.plane.normal.x(), sketch.plane.normal.y(), sketch.plane.normal.z()); + gp_Circ circ(gp_Ax2(o, n), sketch.radius); + TopoDS_Edge e = BRepBuilderAPI_MakeEdge(circ).Edge(); + BRepBuilderAPI_MakeWire wm(e); + if (!wm.IsDone()) throw std::runtime_error("circle wire failed"); + return wm.Wire(); + } + // Rectangle centered on the plane origin + SketchProfile prof; + double hw = sketch.width * 0.5, hh = sketch.height * 0.5; + prof.points.push_back(Vec2d(-hw, -hh)); + prof.points.push_back(Vec2d( hw, -hh)); + prof.points.push_back(Vec2d( hw, hh)); + prof.points.push_back(Vec2d(-hw, hh)); + prof.closed = true; + return prof.to_occt_wire(sketch.plane); +} + +void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body, + const TopoDS_Shape& context, const CadFeature& f) const +{ + switch (f.type) { + case CadFeatureType::Sketch: + return; // sketches carry no solid; consumed by an extrude + case CadFeatureType::Boolean: + return; // body-body boolean is handled in route_feature/apply_boolean, never here + case CadFeatureType::Import: + // Imported B-rep (STEP): rigid data carried on the feature, not built from + // parameters — adopt it as the new body (New-path: result starts empty). + result = f.imported_solid; + have_body = !result.IsNull(); + return; + case CadFeatureType::Extrude: { + const bool sym = (f.extrude_end == ExtrudeEnd::Symmetric); + const double signed_d = f.flip ? -f.distance : f.distance; + TopoDS_Shape tool; + if (f.extrude_src_face >= 0) { + // The source face is read from `context` (the owner body), which for a New + // face-extrude is the source solid while `result` is the empty new body. + if (context.IsNull()) throw std::runtime_error("face-extrude needs a body"); + TopoDS_Face srcf = GeometryEngine::face_by_index(context, f.extrude_src_face); + if (srcf.IsNull()) throw std::runtime_error("face-extrude: invalid face id"); + SketchPlane fpl = SketchPlane::from_face(srcf); + // from_face takes the surface's geometric normal and IGNORES the topological + // face orientation, so for a REVERSED face (e.g. the top cap of an extruded + // prism) it points INTO the solid -> a default push would fuse to nothing. + // Orient it outward so push/pull grows away from the material (Onshape default); + // the Flip checkbox (signed_d) still lets the user drive it inward for a cut. + if (srcf.Orientation() == TopAbs_REVERSED) fpl.normal = -fpl.normal; + tool = SketchEngine::make_extrude_face(srcf, fpl, signed_d, sym); + } else { + // Use the referenced sketch when sketch_ref is a valid Sketch index, + // otherwise fall back to f's own inline sketch params (this makes a + // single self-contained candidate previewable). + const CadFeature& sk = (f.sketch_ref >= 0 && f.sketch_ref < int(features.size()) + && features[f.sketch_ref].type == CadFeatureType::Sketch) + ? features[f.sketch_ref] : f; + // Imported rigid art (Text/SVG) extrudes via the faces-with-holes path + // (with its placement transform applied); otherwise build a single wire + // from entities/profile/shape. + tool = !sk.imported_regions.empty() + ? SketchEngine::make_extrude_regions( + transform_regions(sk.imported_regions, sk.import_offset, + sk.import_scale_x, sk.import_scale_y), + sk.plane, + f.extrude_end == ExtrudeEnd::ThroughAll ? 1e5 : signed_d, + f.extrude_end == ExtrudeEnd::ThroughAll ? true : (sym || f.extrude_end == ExtrudeEnd::TwoSided)) + : [&]() { + TopoDS_Wire wire = build_sketch_wire(sk); + TopoDS_Shape t; + switch (f.extrude_end) { + case ExtrudeEnd::Blind: + t = (std::abs(f.taper_deg) > 1e-6) + ? SketchEngine::make_extrude_taper(wire, sk.plane, signed_d, f.taper_deg) + : SketchEngine::make_extrude(wire, sk.plane, signed_d, false); + break; + case ExtrudeEnd::Symmetric: t = SketchEngine::make_extrude(wire, sk.plane, f.distance, true); break; + case ExtrudeEnd::TwoSided: t = SketchEngine::make_extrude_two_sided(wire, sk.plane, f.distance, f.distance2); break; + case ExtrudeEnd::ThroughAll: t = SketchEngine::make_extrude(wire, sk.plane, 1.0e5, true); break; + case ExtrudeEnd::UpToFace: { + const TopoDS_Face tgt = GeometryEngine::face_by_index(context, f.up_to_face); + double L = signed_d; + if (!tgt.IsNull()) { + const Vec3d c = GeometryEngine::face_centroid_world(tgt); + L = (c - sk.plane.origin).dot(sk.plane.normal); + } + t = (std::abs(f.taper_deg) > 1e-6) + ? SketchEngine::make_extrude_taper(wire, sk.plane, L, f.taper_deg) + : SketchEngine::make_extrude(wire, sk.plane, L, false); + break; + } + case ExtrudeEnd::UpToVertex: { + const double L = (f.up_to_point - sk.plane.origin).dot(sk.plane.normal); + t = SketchEngine::make_extrude(wire, sk.plane, L, false); + break; + } + default: t = SketchEngine::make_extrude(wire, sk.plane, signed_d, false); break; + } + return t; + }(); + } + // New / first-of-a-body => result becomes the tool (route_feature sends New extrudes + // here with an empty result, so a face-extrude New builds a fresh body from the source + // face in `context` without touching it). Add/Cut/Intersect boolean into `result`. + if (!have_body || f.mode == BooleanMode::New) { + result = tool; + have_body = true; + } else if (f.mode == BooleanMode::Add) { + BRepAlgoAPI_Fuse fuse(result, tool); + if (!fuse.IsDone()) throw std::runtime_error("fuse failed"); + result = fuse.Shape(); + } else if (f.mode == BooleanMode::Cut) { + BRepAlgoAPI_Cut cut(result, tool); + if (!cut.IsDone()) throw std::runtime_error("cut failed"); + result = cut.Shape(); + } else if (f.mode == BooleanMode::Intersect) { + BRepAlgoAPI_Common common(result, tool); + if (!common.IsDone()) throw std::runtime_error("intersect failed"); + result = common.Shape(); + } + break; + } + case CadFeatureType::Revolve: { + // Resolve the profile sketch like Extrude: referenced Sketch when valid, + // else this feature's own inline entities/profile (self-contained candidate). + const CadFeature& sk = (f.sketch_ref >= 0 && f.sketch_ref < int(features.size()) + && features[f.sketch_ref].type == CadFeatureType::Sketch) + ? features[f.sketch_ref] : f; + TopoDS_Wire wire = build_sketch_wire(sk); + const double ang = f.flip ? -f.revolve_angle : f.revolve_angle; + TopoDS_Shape tool = SketchEngine::make_revolve(wire, sk.plane, ang, f.revolve_axis); + if (!have_body || f.mode == BooleanMode::New) { + result = tool; + have_body = true; + } else if (f.mode == BooleanMode::Add) { + BRepAlgoAPI_Fuse fuse(result, tool); + if (!fuse.IsDone()) throw std::runtime_error("fuse failed"); + result = fuse.Shape(); + } else if (f.mode == BooleanMode::Cut) { + BRepAlgoAPI_Cut cut(result, tool); + if (!cut.IsDone()) throw std::runtime_error("cut failed"); + result = cut.Shape(); + } else if (f.mode == BooleanMode::Intersect) { + BRepAlgoAPI_Common common(result, tool); + if (!common.IsDone()) throw std::runtime_error("intersect failed"); + result = common.Shape(); + } + break; + } + case CadFeatureType::Sweep: { + // Resolve the profile sketch like Extrude/Revolve, and the path (spine) from + // the referenced path Sketch. Both build through build_sketch_wire (the path + // sketch is entity-based, so its wire keeps its open/closed shape as drawn). + const CadFeature& sk = (f.sketch_ref >= 0 && f.sketch_ref < int(features.size()) + && features[f.sketch_ref].type == CadFeatureType::Sketch) + ? features[f.sketch_ref] : f; + if (f.sweep_path_ref < 0 || f.sweep_path_ref >= int(features.size()) + || features[f.sweep_path_ref].type != CadFeatureType::Sketch) + throw std::runtime_error("sweep needs a valid path sketch"); + TopoDS_Wire profile = build_sketch_wire(sk); + TopoDS_Wire path = build_sketch_wire(features[f.sweep_path_ref]); + TopoDS_Shape tool = SketchEngine::make_sweep(profile, path); + if (!have_body || f.mode == BooleanMode::New) { + result = tool; + have_body = true; + } else if (f.mode == BooleanMode::Add) { + BRepAlgoAPI_Fuse fuse(result, tool); + if (!fuse.IsDone()) throw std::runtime_error("fuse failed"); + result = fuse.Shape(); + } else if (f.mode == BooleanMode::Cut) { + BRepAlgoAPI_Cut cut(result, tool); + if (!cut.IsDone()) throw std::runtime_error("cut failed"); + result = cut.Shape(); + } else if (f.mode == BooleanMode::Intersect) { + BRepAlgoAPI_Common common(result, tool); + if (!common.IsDone()) throw std::runtime_error("intersect failed"); + result = common.Shape(); + } + break; + } + case CadFeatureType::Loft: { + // Loft through 2+ closed profile Sketches, in recipe order. Each profile builds + // a wire via build_sketch_wire (so it keeps its own plane); make_loft skins them. + std::vector profiles; + for (int ref : f.loft_profile_refs) { + if (ref < 0 || ref >= int(features.size()) + || features[ref].type != CadFeatureType::Sketch) + continue; + profiles.push_back(build_sketch_wire(features[ref])); + } + if (profiles.size() < 2) + throw std::runtime_error("loft needs 2+ valid profile sketches"); + TopoDS_Shape tool = SketchEngine::make_loft(profiles, f.loft_ruled); + if (!have_body || f.mode == BooleanMode::New) { + result = tool; + have_body = true; + } else if (f.mode == BooleanMode::Add) { + BRepAlgoAPI_Fuse fuse(result, tool); + if (!fuse.IsDone()) throw std::runtime_error("fuse failed"); + result = fuse.Shape(); + } else if (f.mode == BooleanMode::Cut) { + BRepAlgoAPI_Cut cut(result, tool); + if (!cut.IsDone()) throw std::runtime_error("cut failed"); + result = cut.Shape(); + } else if (f.mode == BooleanMode::Intersect) { + BRepAlgoAPI_Common common(result, tool); + if (!common.IsDone()) throw std::runtime_error("intersect failed"); + result = common.Shape(); + } + break; + } + case CadFeatureType::Pattern: { + // Replicate the target body. Each copy is a rigid gp_Trsf of the seed, all + // fused into one body. Linear: i*spacing along plane axis pattern_dir + // (0=X,1=Y). Circular: i*(angle/count) about the plane normal through the + // plane origin (so a seed offset from the origin orbits the axis). + if (!have_body) throw std::runtime_error("pattern needs a body"); + const int n = std::max(1, f.pattern_count); + const TopoDS_Shape seed = result; + for (int i = 1; i < n; ++i) { + gp_Trsf trsf; + if (f.pattern_circular) { + Vec3d o = f.plane.to_world(Vec2d(0, 0)); + gp_Ax1 ax(gp_Pnt(o.x(), o.y(), o.z()), + gp_Dir(f.plane.normal.x(), f.plane.normal.y(), f.plane.normal.z())); + const double step = (f.pattern_angle * M_PI / 180.0) / double(n); + trsf.SetRotation(ax, step * i); + } else { + const Vec3d& d = (f.pattern_dir == 1) ? f.plane.y_axis : f.plane.x_axis; + trsf.SetTranslation(gp_Vec(d.x() * f.pattern_spacing * i, + d.y() * f.pattern_spacing * i, + d.z() * f.pattern_spacing * i)); + } + TopoDS_Shape copy = BRepBuilderAPI_Transform(seed, trsf, true).Shape(); + BRepAlgoAPI_Fuse fuse(result, copy); + if (!fuse.IsDone()) throw std::runtime_error("pattern fuse failed"); + result = fuse.Shape(); + } + break; + } + case CadFeatureType::Fillet: + if (!have_body) throw std::runtime_error("fillet needs a body"); + if (f.dressup_edge >= 0) + result = GeometryEngine::apply_fillet(result, f.dressup_size, f.dressup_edge); + else + result = GeometryEngine::apply_fillet(result, f.dressup_size, f.face_group); + break; + case CadFeatureType::Chamfer: + if (!have_body) throw std::runtime_error("chamfer needs a body"); + if (f.dressup_edge >= 0) + result = GeometryEngine::apply_chamfer(result, f.dressup_size, f.dressup_edge); + else + result = GeometryEngine::apply_chamfer(result, f.dressup_size, f.face_group); + break; + case CadFeatureType::Hole: { + if (!have_body) throw std::runtime_error("hole needs a body"); + // Circle wire centered at the positioned point on the plane + Vec3d c = f.plane.to_world(Vec2d(f.hole_x, f.hole_y)); + gp_Pnt o(c.x(), c.y(), c.z()); + gp_Dir n(f.plane.normal.x(), f.plane.normal.y(), f.plane.normal.z()); + gp_Circ circ(gp_Ax2(o, n), f.hole_diameter * 0.5); + TopoDS_Edge e = BRepBuilderAPI_MakeEdge(circ).Edge(); + BRepBuilderAPI_MakeWire wm(e); + if (!wm.IsDone()) throw std::runtime_error("hole wire failed"); + // Through = symmetric huge cut (passes fully through any body); + // Blind = +normal extrude of hole_depth into the body. + TopoDS_Shape tool = f.hole_through + ? SketchEngine::make_extrude(wm.Wire(), f.plane, 1.0e5, true, 0.0) + : SketchEngine::make_extrude(wm.Wire(), f.plane, f.hole_depth, false, 0.0); + BRepAlgoAPI_Cut cut(result, tool); + if (!cut.IsDone()) throw std::runtime_error("hole cut failed"); + result = cut.Shape(); + break; + } + case CadFeatureType::Thread: { + // Axis at the positioned point on the plane; +normal = thread rise. + Vec3d c3 = f.plane.to_world(Vec2d(f.thread_x, f.thread_y)); + gp_Pnt c(c3.x(), c3.y(), c3.z()); + gp_Dir zdir(f.plane.normal.x(), f.plane.normal.y(), f.plane.normal.z()); + gp_Dir xdir(f.plane.x_axis.x(), f.plane.x_axis.y(), f.plane.x_axis.z()); + gp_Ax3 ax3(c, zdir, xdir); + gp_Ax2 ax2(c, zdir, xdir); + + // Build the swept helical ridge (guarded — never fatal). + TopoDS_Shape ridge; + bool have_ridge = false; + try { + TopoDS_Wire spine = make_helix_wire(ax3, f.thread_radius, + f.thread_pitch, f.thread_height); + TopoDS_Face prof = make_thread_profile(c, xdir, zdir, f.thread_radius, + f.thread_pitch, f.thread_depth, + f.thread_internal); + BRepOffsetAPI_MakePipe pipe(spine, prof); + pipe.Build(); + if (pipe.IsDone()) { + ridge = pipe.Shape(); + have_ridge = !ridge.IsNull(); + } + } catch (const std::exception&) { + have_ridge = false; // fall back to the bare cylinder/bore below + } + + if (f.thread_internal) { + if (!have_body) throw std::runtime_error("internal thread needs a body"); + // Tapped bore: ensure a clean cylindrical pocket, then carve the + // OUTWARD helical groove into its wall. When the thread is invoked on + // an existing hole the bore cut is coincident (a no-op that may report + // !IsDone) — tolerate it so the visible groove cut below still runs. + TopoDS_Shape bore = BRepPrimAPI_MakeCylinder(ax2, f.thread_radius, + f.thread_height).Shape(); + try { + BRepAlgoAPI_Cut cut_bore(result, bore); + if (cut_bore.IsDone() && !cut_bore.Shape().IsNull()) + result = cut_bore.Shape(); + } catch (const std::exception&) { /* keep existing bore */ } + if (have_ridge) { + BRepAlgoAPI_Cut cut_ridge(result, ridge); + if (cut_ridge.IsDone() && !cut_ridge.Shape().IsNull()) + result = cut_ridge.Shape(); + } + } else { + // External threaded rod = a New body: base cylinder + fused ridge. + TopoDS_Shape rod = BRepPrimAPI_MakeCylinder(ax2, f.thread_radius, + f.thread_height).Shape(); + if (have_ridge) { + BRepAlgoAPI_Fuse fuse(rod, ridge); + if (fuse.IsDone()) rod = fuse.Shape(); + } + result = rod; + have_body = true; + } + break; + } + case CadFeatureType::Shell: { + if (!have_body) throw std::runtime_error("shell needs a body"); + // Hollow the body to a wall thickness; the picked face (if any) is removed so the + // shell is open there. MakeThickSolidByJoin with a NEGATIVE offset shells inward. + TopTools_ListOfShape remove; + if (f.shell_face >= 0) { + TopoDS_Face fc = GeometryEngine::face_by_index(result, f.shell_face); + if (!fc.IsNull()) remove.Append(fc); + } + BRepOffsetAPI_MakeThickSolid mts; + mts.MakeThickSolidByJoin(result, remove, -std::abs(f.shell_thickness), 1.0e-3); + mts.Build(); + if (!mts.IsDone()) throw std::runtime_error("shell failed"); + result = mts.Shape(); + if (result.IsNull()) throw std::runtime_error("shell produced no geometry"); + break; + } + case CadFeatureType::Draft: { + if (!have_body) throw std::runtime_error("draft needs a body"); + if (f.draft_face < 0) throw std::runtime_error("draft needs a picked face"); + TopoDS_Face fc = GeometryEngine::face_by_index(result, f.draft_face); + if (fc.IsNull()) throw std::runtime_error("draft: face not found"); + // Neutral plane = horizontal plane through the body's bbox bottom, pull direction +Z. + // The face pivots about the line where it meets the neutral plane and tilts by the angle. + // ponytail: neutral plane / pull direction fixed to world up; pick-based neutral plane + // deferred (same as the datum-plane pick types, snaporca-dgv). + Bnd_Box bb; BRepBndLib::Add(result, bb); + Standard_Real xmin, ymin, zmin, xmax, ymax, zmax; + bb.Get(xmin, ymin, zmin, xmax, ymax, zmax); + gp_Dir pull(0, 0, 1); + gp_Pln neutral(gp_Pnt(0, 0, zmin), pull); + BRepOffsetAPI_DraftAngle draft(result); + draft.Add(fc, pull, f.draft_angle * M_PI / 180.0, neutral); + if (!draft.AddDone()) + throw std::runtime_error("draft: face cannot be drafted (is it parallel to the base?)"); + draft.Build(); + if (!draft.IsDone()) throw std::runtime_error("draft failed"); + result = draft.Shape(); + if (result.IsNull()) throw std::runtime_error("draft produced no geometry"); + break; + } + } +} + +// Compound of all body shapes (1 body => that body verbatim, so single-body display and +// global face/edge ids are byte-identical to the pre-multi-body behaviour). +static TopoDS_Shape compound_of(const std::vector& bodies) +{ + if (bodies.size() == 1) return bodies[0].shape; + TopoDS_Compound comp; + BRep_Builder bld; + bld.MakeCompound(comp); + for (const CadBody& b : bodies) + if (!b.shape.IsNull()) bld.Add(comp, b.shape); + return comp; +} + +// Tessellate every body separately and concatenate into one mesh, recording per-triangle +// (body index, face id WITHIN that body). Single-body => byte-identical to tessellate(body). +static TriangleMesh tessellate_bodies(const std::vector& bodies, + std::vector& tri_face, std::vector& tri_body, + std::vector& body_meshes, + double lin, double ang) +{ + tri_face.clear(); + tri_body.clear(); + body_meshes.clear(); + indexed_triangle_set merged; + for (int bi = 0; bi < int(bodies.size()); ++bi) { + std::vector tf; + TriangleMesh bm = SketchEngine::tessellate(bodies[bi].shape, tf, lin, ang); + const indexed_triangle_set& its = bm.its; + const int voff = int(merged.vertices.size()); + for (const auto& v : its.vertices) merged.vertices.push_back(v); + for (const auto& t : its.indices) + merged.indices.emplace_back(t[0] + voff, t[1] + voff, t[2] + voff); + for (int fid : tf) { tri_face.push_back(fid); tri_body.push_back(bi); } + body_meshes.push_back(std::move(bm)); // per-body mesh kept for distinct GLVolume colors + } + return TriangleMesh(merged); +} + +void CadDocument::apply_boolean(std::vector& bodies, const CadFeature& f) const +{ + const int nb = int(bodies.size()); + const int tgt = (f.target_body >= 0 && f.target_body < nb) ? f.target_body : nb - 1; + const int tool = (f.bool_tool_body >= 0 && f.bool_tool_body < nb) ? f.bool_tool_body : -1; + if (tgt < 0 || tool < 0 || tgt == tool) return; // need two distinct bodies; otherwise no-op + const TopoDS_Shape A = bodies[tgt].shape; // target survives + const TopoDS_Shape B = bodies[tool].shape; // tool, consumed unless kept + if (A.IsNull() || B.IsNull()) return; + + TopTools_ListOfShape args, tools; + args.Append(A); + tools.Append(B); + auto run = [&](BRepAlgoAPI_BooleanOperation& bop) -> TopoDS_Shape { + bop.SetArguments(args); + bop.SetTools(tools); + if (f.bool_tolerance > 0.0) bop.SetFuzzyValue(f.bool_tolerance); // OCCT fuzzy: merge near-coincident faces + bop.Build(); + if (!bop.IsDone()) throw std::runtime_error("boolean operation failed"); + return bop.Shape(); + }; + TopoDS_Shape result; + switch (f.mode) { + case BooleanMode::Add: { BRepAlgoAPI_Fuse op; result = run(op); break; } // union + case BooleanMode::Cut: { BRepAlgoAPI_Cut op; result = run(op); break; } // target - tool + case BooleanMode::Intersect: { BRepAlgoAPI_Common op; result = run(op); break; } // overlap + default: return; // BooleanMode::New is meaningless between two existing bodies + } + if (result.IsNull()) throw std::runtime_error("boolean produced an empty shape"); + + bodies[tgt].shape = result; + if (!f.bool_keep_tool) bodies.erase(bodies.begin() + tool); // consume the tool body +} + +void CadDocument::apply_cut(std::vector& bodies, const CadFeature& f) const +{ + const int nb = int(bodies.size()); + if (nb == 0) throw std::runtime_error("cut: no target body"); + const int tgt = (f.target_body >= 0 && f.target_body < nb) ? f.target_body : nb - 1; + if (tgt < 0 || bodies[tgt].shape.IsNull()) throw std::runtime_error("cut: no target body"); + + if (!f.cut_keep_upper && !f.cut_keep_lower) + throw std::runtime_error("cut keeps nothing"); + + SketchPlane cp = f.plane; + cp.origin += cp.normal * f.cut_offset; + if (f.cut_flip) cp.normal = -cp.normal; + + // Build a large square wire in the cut plane, centered at plane origin. + const double L = 1.0e5; + Vec3d x = cp.x_axis * L; + Vec3d y = cp.y_axis * L; + Vec3d o = cp.origin; + auto p = [&](double sx, double sy) { + Vec3d v = o + x * sx + y * sy; + return gp_Pnt(v.x(), v.y(), v.z()); + }; + BRepBuilderAPI_MakePolygon poly; + poly.Add(p( 1, 1)); + poly.Add(p( 1, -1)); + poly.Add(p(-1, -1)); + poly.Add(p(-1, 1)); + poly.Close(); + if (!poly.IsDone()) throw std::runtime_error("cut: failed to build cut wire"); + TopoDS_Wire wire = poly.Wire(); + + TopoDS_Shape upper_piece, lower_piece; + const TopoDS_Shape& target = bodies[tgt].shape; + + if (f.cut_keep_upper) { + TopoDS_Shape upper_tool = SketchEngine::make_extrude(wire, cp, L, false, 0.0); + BRepAlgoAPI_Common common(target, upper_tool); + if (!common.IsDone()) throw std::runtime_error("cut operation failed"); + upper_piece = common.Shape(); + } + + if (f.cut_keep_lower) { + SketchPlane lp = cp; + lp.normal = -lp.normal; + TopoDS_Shape lower_tool = SketchEngine::make_extrude(wire, lp, L, false, 0.0); + BRepAlgoAPI_Common common(target, lower_tool); + if (!common.IsDone()) throw std::runtime_error("cut operation failed"); + lower_piece = common.Shape(); + } + + if (f.cut_keep_upper && f.cut_keep_lower) { + bodies[tgt].shape = upper_piece; + bodies.insert(bodies.begin() + tgt + 1, + CadBody{ lower_piece, bodies[tgt].name + " (2)" }); + } else if (f.cut_keep_upper) { + bodies[tgt].shape = upper_piece; + } else { + bodies[tgt].shape = lower_piece; + } +} + +void CadDocument::route_feature(std::vector& bodies, const CadFeature& f) const +{ + if (f.type == CadFeatureType::Plane) return; // datum plane: not part of the body pipeline + if (f.type == CadFeatureType::Boolean) { apply_boolean(bodies, f); return; } // body-body op + if (f.type == CadFeatureType::Cut) { apply_cut(bodies, f); return; } // plane-split body + // Resolve the target body: explicit target_body when valid, else the last body. + const int t = (f.target_body >= 0 && f.target_body < int(bodies.size())) + ? f.target_body : int(bodies.size()) - 1; + const TopoDS_Shape context = (t >= 0) ? bodies[t].shape : TopoDS_Shape(); + // A New extrude (or the very first solid feature) starts a fresh body; everything else + // mutates the target body in place. + const bool starts_new = bodies.empty() + || f.type == CadFeatureType::Import // an imported solid is always its own base body + || ((f.type == CadFeatureType::Extrude || f.type == CadFeatureType::Revolve + || f.type == CadFeatureType::Sweep || f.type == CadFeatureType::Loft) + && f.mode == BooleanMode::New); + + if (starts_new) { + TopoDS_Shape result; // empty -> apply_feature fills it (New path) + bool have_body = false; + apply_feature(result, have_body, context, f); + if (have_body && !result.IsNull()) + bodies.push_back({ result, f.name.empty() ? std::string("Body") : f.name }); + } else { + if (t < 0) throw std::runtime_error("feature needs a body"); + TopoDS_Shape result = bodies[t].shape; // shallow handle; apply_feature mutates it + bool have_body = true; + apply_feature(result, have_body, context, f); + bodies[t].shape = result; + } +} + +bool CadDocument::recompute() +{ + error.clear(); + std::vector built; + try { + for (const CadFeature& f : features) { + if (!f.enabled) continue; + if (f.type == CadFeatureType::Sketch) continue; // consumed by an extrude + if (f.type == CadFeatureType::Plane) continue; // datum: no solid, derived on demand + route_feature(built, f); + } + } catch (const Standard_Failure& e) { + // OCCT raises Standard_Failure (NOT a std::exception) — must be caught + // here or it escapes the event handler and terminates the app. + error = e.GetMessageString() ? e.GetMessageString() : "OCCT operation failed"; + return false; + } catch (const std::exception& e) { + error = e.what(); + return false; + } catch (...) { + error = "unknown geometry error"; + return false; + } + if (built.empty()) { error = "no solid-producing features"; return false; } + + // recompute() replaces the bodies vector wholesale, which would drop any per-body + // colour override (Color tool). Body indices are stable across a rebuild (bodies are + // appended in feature order), so carry the override forward by index — same indexing + // contract the GUI relies on for per-body visibility/Move. + for (size_t i = 0; i < built.size() && i < bodies.size(); ++i) { + if (bodies[i].has_color) { + built[i].has_color = true; + built[i].color = bodies[i].color; + } + } + bodies = std::move(built); + body = compound_of(bodies); + display_mesh = tessellate_bodies(bodies, display_tri_face, display_tri_body, + display_body_meshes, + linear_deflection, angular_deflection); + if (display_mesh.its.indices.empty()) { + error = "tessellation produced an empty mesh"; + return false; + } + return true; +} + +bool CadDocument::preview(const CadFeature& candidate, TriangleMesh& out_mesh, + std::vector& out_body_meshes, std::string& err) const +{ + err.clear(); + out_body_meshes.clear(); + std::vector tmp = bodies; // start from the current committed bodies + try { + route_feature(tmp, candidate); // candidate may append a new body or mutate one + } catch (const Standard_Failure& e) { + err = e.GetMessageString() ? e.GetMessageString() : "OCCT operation failed"; + return false; + } catch (const std::exception& e) { + err = e.what(); + return false; + } catch (...) { + err = "unknown geometry error"; + return false; + } + if (tmp.empty()) { + err = "preview produced no geometry"; + return false; + } + // Tessellate per body (same path as recompute) so the GUI can re-apply its display-only + // per-body Move transforms to the ghost; out_mesh is the merged whole. + std::vector tf, tb; + out_mesh = tessellate_bodies(tmp, tf, tb, out_body_meshes, linear_deflection, angular_deflection); + if (out_mesh.its.indices.empty()) { + err = "preview produced an empty mesh"; + return false; + } + return true; +} + +bool CadDocument::preview(const CadFeature& candidate, TriangleMesh& out_mesh, std::string& err) const +{ + std::vector ignore; + return preview(candidate, out_mesh, ignore, err); +} + +} // namespace Slic3r diff --git a/src/libslic3r/CadDocument.hpp b/src/libslic3r/CadDocument.hpp new file mode 100644 index 0000000000..0de663a74e --- /dev/null +++ b/src/libslic3r/CadDocument.hpp @@ -0,0 +1,355 @@ +#ifndef slic3r_CadDocument_hpp_ +#define slic3r_CadDocument_hpp_ + +#include "TriangleMesh.hpp" +#include "SketchEngine.hpp" +#include "GeometryEngine.hpp" // FaceGroup +#include "Color.hpp" // ColorRGBA (per-body display colour override) + +#include +#include +#include +#include +#include + +namespace Slic3r { + +enum class CadFeatureType { Sketch, Extrude, Fillet, Chamfer, Hole, Thread, Shell, Revolve, Sweep, Pattern, Plane, Loft, Draft, Import, Boolean, Cut }; +enum class SketchShape { Rectangle, Circle }; +enum class BooleanMode { New, Add, Cut, Intersect }; + +enum class ExtrudeEnd { Blind, Symmetric, TwoSided, ThroughAll, UpToFace, UpToVertex }; + +struct CadFeature { + CadFeatureType type{CadFeatureType::Sketch}; + std::string name; + bool enabled{true}; + + // Sketch params (centered on the plane origin) + SketchShape shape{SketchShape::Rectangle}; + SketchPlane plane{SketchPlane::XY()}; + double width{20}; + double height{20}; + double radius{10}; + + // Real 2D sketch geometry (Onshape-style). When non-empty this takes + // precedence over the shape/width/height/radius enum path in build_sketch_wire. + SketchProfile profile; + + // Onshape-style multi-entity sketch geometry. When non-empty this takes + // precedence over both `profile` and the shape-enum path in build_sketch_wire. + std::vector entities; + + // 2D geometric constraints on `profile` (point indices). Solved in place. + std::vector constraints; + + // Onshape-style constraints on `entities` (Fase 4.2). Solved in place against + // entity endpoints. Used when `entities` is non-empty (the legacy `constraints` + // vector applies only to the `profile` path). + std::vector entity_constraints; + + // Imported rigid 2D art (Text glyphs / SVG vector paths) as filled regions. + // Each region: contour[0] = outer loop, contour[1..] = holes; points in + // plane (u,v) millimetres. Rendered as a sketch overlay and extruded via a + // faces-with-holes path (SketchEngine::make_extrude_regions) — deliberately + // NOT solver entities, so imported art contributes zero DoF and never + // pollutes the constraint solver / DoF readout. When non-empty it takes + // precedence over the entities/profile/shape paths in the Extrude case. + std::vector>> imported_regions; + + // Imported rigid 3D B-rep solid (STEP). When the feature type is Import this carries + // the OCCT shape verbatim — it is adopted as a base body in route_feature (no parametric + // recipe). Downstream face/edge features (fillet/chamfer/cut/shell/...) act on it like any + // other body. TopoDS_Shape is a cheap handle, so copying it through recompute/checkpoint + // snapshots is cheap. In-session only for now (no BRep serialization yet). + TopoDS_Shape imported_solid; + + // Non-destructive placement transform for imported_regions (Text/SVG), + // applied at display + extrude time as + // p -> (p.x*import_scale_x + import_offset.x, p.y*import_scale_y + import_offset.y). + // Lets the art be moved / enlarged / stretched (independent X/Y) repeatedly + // without re-vectorising. Identity = no change. + Vec2d import_offset{0, 0}; + double import_scale_x{1.0}; + double import_scale_y{1.0}; + // Text/SVG dropped ONTO a solid face (centred on it): the extrude then defaults to an + // inward Cut (engraving) targeting `import_face_body`. False = free art on a plane. + bool import_on_face{false}; + int import_face_body{-1}; + + // Extrude params + int sketch_ref{-1}; // index into features[] of the consumed sketch + double distance{10}; + bool symmetric{false}; + BooleanMode mode{BooleanMode::New}; + ExtrudeEnd extrude_end{ExtrudeEnd::Blind}; + double distance2{0}; // second-side depth for TwoSided + double taper_deg{0}; // draft angle (C4-part2) + bool flip{false}; // reverse the extrude direction (negate plane normal) + int up_to_face{-1}; // target solid-face id for UpToFace (C4-part2) + int extrude_src_face{-1}; // global face id on the current body to extrude as a profile; -1 = use sketch wire + Vec3d up_to_point{0,0,0}; // target for UpToVertex (C4-part2) + // Multi-body target: which body (index into CadDocument::bodies) this feature acts on. + // -1 = auto (last body). A New extrude appends a fresh body; Add/Cut/Intersect, dress-up, + // hole and face-extrude(non-New) mutate bodies[target]; face-extrude reads its source + // face from bodies[target] too. The source-face owner for face-extrude lives here. + int target_body{-1}; + + // Dress-up params (Fillet/Chamfer) — applied to the current body in order + double dressup_size{1.0}; // fillet radius or chamfer distance + FaceGroup face_group{FaceGroup::All}; + int dressup_edge{-1}; // global edge id for edge-targeted fillet/chamfer; -1 = use face_group + + // Hole params (positioned circular cut into the current body) + double hole_diameter{5}; + double hole_depth{10}; + bool hole_through{true}; // true = symmetric through-cut, ignores hole_depth + double hole_x{0}; // position on the plane (plane u/x axis) + double hole_y{0}; // position on the plane (plane v/y axis) + + // Thread params (helical thread about the plane normal at a positioned point) + double thread_radius{5}; // nominal cylinder radius + double thread_pitch{2}; // axial advance per turn + double thread_height{10}; // total axial length + double thread_depth{1}; // radial crest depth of the thread profile + bool thread_internal{false}; // false = external threaded rod (New body); + // true = tapped bore cut into the current body + double thread_x{0}; // axis position on the plane (u/x axis) + double thread_y{0}; // axis position on the plane (v/y axis) + + // Shell params (hollow the current body to a wall thickness, removing one open face) + double shell_thickness{2}; // wall thickness (inward offset) + int shell_face{-1}; // global face id to remove (open the shell); -1 = none + + // Draft params (taper a single solid face about a neutral plane = body bbox bottom, pull +Z) + int draft_face{-1}; // global face id to draft; -1 = none + double draft_angle{5}; // draft angle in degrees (signed: + leans the face inward) + + // Revolve params (sweep a profile about an in-plane axis through the plane origin). + // Reuses sketch_ref / entities (profile), flip (direction), mode (boolean) and + // target_body. revolve_axis: 0 = plane X axis, 1 = plane Y axis. + double revolve_angle{360}; // sweep angle in degrees (1..360) + int revolve_axis{0}; // 0 = plane X, 1 = plane Y + + // Sweep: profile carried by sketch_ref / entities (like Extrude); the spine is a + // second Sketch referenced by sweep_path_ref (an open or closed wire). Reuses + // mode (boolean) and target_body. + int sweep_path_ref{-1}; // index into features[] of the path Sketch + + // Loft: build a solid through 2+ closed profile Sketches (loft_profile_refs, in + // order, each on its own plane). loft_ruled=false → smooth sections, true → ruled. + // Reuses mode (boolean) and target_body. + std::vector loft_profile_refs; // ordered indices into features[] of profile Sketches + bool loft_ruled{false}; + + // Pattern: replicate the target body, copies fused into it. pattern_circular=false + // → linear (pattern_count instances spaced pattern_spacing along plane axis + // pattern_dir: 0=X, 1=Y); true → circular (pattern_count instances over + // pattern_angle° total about the plane normal through the plane origin, so a seed + // offset from the origin orbits the axis). Reuses target_body + plane. + bool pattern_circular{false}; + int pattern_count{3}; // total instances incl. the seed (>=1) + double pattern_spacing{20}; // linear step (mm) + int pattern_dir{0}; // linear direction: 0 = plane X, 1 = plane Y + double pattern_angle{360}; // circular total angle (degrees) + + // Datum/reference plane: a derived SketchPlane the document offers as a selectable + // sketch plane (no solid). plane_base selects the reference (0=XY,1=XZ,2=YZ, or 3+N + // = the Nth earlier datum plane); plane_offset shifts along the base normal; + // plane_angle tilts plane_angle° about the base axis plane_axis (0=base X, 1=base Y). + int plane_base{0}; + double plane_offset{20}; + double plane_angle_tilt{0}; // degrees (named *_tilt to avoid clash w/ revolve) + int plane_axis{0}; // tilt axis: 0 = base X, 1 = base Y + + // Boolean: combine two EXISTING bodies. `mode` reuses BooleanMode (Add = union, + // Cut = subtract tool from target, Intersect = keep overlap; New unused). `target_body` + // is the body that survives (result written back to it); `bool_tool_body` is the other + // operand, consumed (erased) unless `bool_keep_tool`. `bool_tolerance` = OCCT fuzzy value + // (0 = exact). Per-face merge: when both bool_target_face/bool_tool_face are set, the tool + // is first snapped so those two faces are coincident (gap closed within bool_tolerance), + // then the boolean welds them and coplanar faces are unified into one clean face. + int bool_tool_body{-1}; + bool bool_keep_tool{false}; + double bool_tolerance{0.0}; + int bool_target_face{-1}; // global face id on the target body to mate (-1 = none) + int bool_tool_face{-1}; // global face id on the tool body to mate (-1 = none) + + // Cut: split one target body with a plane, keeping the upper half, lower half, or both. + // Reuses `plane` for the cut plane and `target_body` for which body is cut. + double cut_offset{0.0}; // offset along the cut-plane normal (mm) + bool cut_flip{false}; // flip the normal => swaps which side is "upper" + bool cut_keep_upper{true}; // keep the +normal half + bool cut_keep_lower{false}; // keep the -normal half (both => split into two bodies) +}; + +// One independent solid in a multi-body document. +struct CadBody { + TopoDS_Shape shape; + std::string name; + // Per-body display colour override (Color tool). When has_color is false the GUI + // falls back to the auto body-index palette. Carried across recompute() by body index. + bool has_color{false}; + ColorRGBA color; +}; + +// OCCT-only feature tree backing the Design tab. No GUI dependencies (lives in libslic3r). +class CadDocument { +public: + std::vector features; + // Multi-body result of the last replay. A "New" extrude appends a body; other ops + // mutate a target body. Empty after a failed/empty recompute. + std::vector bodies; + TopoDS_Shape body; // compound of all bodies (1 body => that body) — display/compat + TriangleMesh display_mesh; // tessellation of all bodies, concatenated (picking) + std::vector display_body_meshes; // one mesh per body, in `bodies` order (per-body color) + std::vector display_tri_face; // per-triangle face id WITHIN its source body + std::vector display_tri_body; // per-triangle source body index (into bodies) + std::string error; // last recompute error ("" = ok) + + double linear_deflection{0.01}; + double angular_deflection{0.5}; + + int add_sketch(SketchShape shape, const SketchPlane& plane, + double width, double height, double radius, + const std::string& name); + int add_sketch_profile(const SketchProfile& profile, const SketchPlane& plane, + const std::string& name); + // Onshape-style multi-entity sketch: stores the entity list verbatim. When + // non-empty it takes precedence over profile/enum in build_sketch_wire. + int add_sketch_entities(const std::vector& entities, + const SketchPlane& plane, const std::string& name, + const std::vector& constraints = {}); + // Solve features[index]'s sketch constraints, writing solved coordinates back + // into its profile.points. No-op (returns true) if the feature has no + // constraints. Returns false if index is invalid / not a Sketch / solve fails. + bool solve_sketch_feature(int index); + int add_extrude(int sketch_ref, double distance, bool symmetric, + BooleanMode mode, const std::string& name); + // Extrude a single loop given directly as entities (sketch_ref = -1, plane carried). + int add_extrude_entities(const std::vector& entities, + const SketchPlane& plane, double distance, bool symmetric, + BooleanMode mode, const std::string& name); + // Extrude an existing solid FACE (global face id on the body) as the profile. + int add_extrude_face(int src_face, double distance, bool symmetric, + BooleanMode mode, const std::string& name); + int add_fillet(double radius, FaceGroup faces, const std::string& name); + int add_fillet(double radius, int edge_id, const std::string& name); + int add_chamfer(double distance, FaceGroup faces, const std::string& name); + int add_chamfer(double distance, int edge_id, const std::string& name); + int add_hole(double diameter, double depth, bool through, + double x, double y, const SketchPlane& plane, + const std::string& name); + int add_thread(double radius, double pitch, double height, double depth, + bool internal, double x, double y, const SketchPlane& plane, + const std::string& name); + int add_revolve(int sketch_ref, double angle, int axis, bool flip, + BooleanMode mode, const std::string& name); + // Self-contained revolve of a single loop given directly as entities (sketch_ref=-1). + int add_revolve_entities(const std::vector& entities, + const SketchPlane& plane, double angle, int axis, bool flip, + BooleanMode mode, const std::string& name); + // Sweep the profile Sketch (profile_sketch_ref) along the path Sketch (path_sketch_ref). + int add_pattern(bool circular, int count, double spacing, int dir, + double angle_deg, int target_body, const std::string& name); + int add_sweep(int profile_sketch_ref, int path_sketch_ref, BooleanMode mode, + const std::string& name); + // Loft through the ordered profile Sketches (each a closed wire on its own plane). + int add_loft(const std::vector& profile_refs, bool ruled, BooleanMode mode, + const std::string& name); + int add_shell(double thickness, int face, int target_body, const std::string& name); + int add_draft(double angle, int face, int target_body, const std::string& name); + // Boolean between two existing bodies. op reuses BooleanMode (Add=union, Cut=subtract, + // Intersect=common; New invalid). target survives, tool is consumed unless keep_tool. + // tolerance = OCCT fuzzy value; target_face/tool_face (-1 = none) drive the per-face snap+merge. + int add_boolean(BooleanMode op, int target_body, int tool_body, bool keep_tool, + double tolerance, int target_face, int tool_face, const std::string& name); + // Plane Cut (Onshape split-by-plane): trim target_body by the plane (origin offset along + // its normal by `offset`, normal flipped iff `flip`). keep_upper/keep_lower select the + // +normal / -normal half; both => the body is split into two coexisting bodies. + int add_cut(const SketchPlane& plane, double offset, bool flip, + bool keep_upper, bool keep_lower, int target_body, const std::string& name); + // Datum plane: derived from base (0=XY/1=XZ/2=YZ/3+N=Nth earlier datum), offset + // along its normal, optional tilt about a base axis. Produces no solid. + int add_plane(int base, double offset, double angle_tilt, int axis, + const std::string& name); + // Every datum plane currently in the recipe, in feature order, as (name, plane). + // Used by the GUI to populate plane pickers (after the 3 base planes). + std::vector> resolve_datum_planes() const; + void clear(); + bool recompute(); // replay features -> body + display_mesh; false on error + + // Undo/redo of the feature recipe (Onshape-style Ctrl+Z). The caller marks a + // user-action boundary by calling checkpoint() BEFORE the mutation(s) for that + // action (add/delete/move/replace, or a direct features edit). undo()/redo() then + // restore the snapshot and recompute(). Because everything else (bodies/meshes/ + // body) is derived by recompute(), snapshotting `features` alone is a complete, + // exact history; one checkpoint == one Ctrl+Z step. + void checkpoint(); // snapshot `features` for undo + invalidate redo + bool can_undo() const { return !m_undo.empty(); } + bool can_redo() const { return !m_redo.empty(); } + size_t undo_depth() const { return m_undo.size(); } + size_t redo_depth() const { return m_redo.size(); } + bool undo(); // restore the previous feature list + recompute(); false if no history + bool redo(); // re-apply the most recently undone change; false if none + + // Feature-tree editing (Onshape-style). All are transactional: they snapshot + // features, mutate, recompute(), and roll back to the snapshot (re-recomputing) + // if the result is invalid — so a failed edit never leaves a broken body. + // + // remove_feature: erase features[index]; deleting a Sketch cascades to the + // Extrude(s) that consume it; surviving sketch_ref indices are remapped. + // move_feature: shift features[index] by delta (-1 up / +1 down), clamped; + // sketch_ref indices of the two swapped slots are remapped. + // replace_feature: overwrite features[index] with `edited` (its name and, for + // an Extrude, its sketch_ref are preserved from the original). + bool remove_feature(int index); + bool move_feature(int index, int delta); + bool replace_feature(int index, const CadFeature& edited); + // replace_sketch_extrude: a box is two linked features (Sketch + Extrude); + // overwrite both slots from one `edited` candidate (sketch params -> + // features[sketch_idx], extrude params -> features[extrude_idx]), keeping + // each slot's name/type and the sketch_ref link. Transactional like above. + bool replace_sketch_extrude(int sketch_idx, int extrude_idx, const CadFeature& edited); + + // Apply ONE candidate feature on top of the current committed body and + // tessellate the result into out_mesh, WITHOUT modifying features/body/ + // display_mesh. Returns false (with err set) if the candidate is invalid. + // Used by the Design tab to show a translucent ghost before Confirm. + bool preview(const CadFeature& candidate, TriangleMesh& out_mesh, std::string& err) const; + // Same, but also returns the per-body meshes (in `bodies` order; the candidate may append + // one), so the GUI can apply its display-only per-body Move transforms to the ghost and keep + // it overlaid on the moved body instead of floating back at the untransformed origin. + bool preview(const CadFeature& candidate, TriangleMesh& out_mesh, + std::vector& out_body_meshes, std::string& err) const; + +private: + TopoDS_Wire build_sketch_wire(const CadFeature& sketch) const; + // Apply a single feature to (result, have_body), throwing std::runtime_error on + // failure. `context` is the body whose faces/edges the feature reads (face-extrude + // source, up-to-face target, dress-up, hole) — it differs from `result` only when the + // feature builds a NEW body from an existing one (face-extrude New). Shared by route. + void apply_feature(TopoDS_Shape& result, bool& have_body, + const TopoDS_Shape& context, const CadFeature& f) const; + // Route one feature into the bodies list: resolve its target body, decide whether it + // starts a new body (empty list, or an Extrude with mode New) vs mutates an existing + // one, then apply_feature. Shared by recompute() (replay all) and preview() (candidate). + void route_feature(std::vector& bodies, const CadFeature& f) const; + // Boolean between two existing bodies: resolve target + tool, optionally snap the tool so + // the picked faces mate, run the OCCT op (with fuzzy tolerance), write the result back to the + // target and erase the consumed tool. Mutates the bodies vector directly (unlike apply_feature, + // which works on a single result shape). Throws std::runtime_error on a failed op. + void apply_boolean(std::vector& bodies, const CadFeature& f) const; + void apply_cut(std::vector& bodies, const CadFeature& f) const; + + // Undo/redo stacks of feature-list snapshots. checkpoint() pushes onto m_undo and + // clears m_redo; undo()/redo() shuffle the current state between them. Capped so a + // long session can't grow unbounded. + std::vector> m_undo; + std::vector> m_redo; + static constexpr size_t k_undo_cap = 200; +}; + +} // namespace Slic3r + +#endif // slic3r_CadDocument_hpp_ diff --git a/src/libslic3r/GeometryEngine.cpp b/src/libslic3r/GeometryEngine.cpp new file mode 100644 index 0000000000..6ecd02d20c --- /dev/null +++ b/src/libslic3r/GeometryEngine.cpp @@ -0,0 +1,442 @@ +#include "GeometryEngine.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Slic3r { + +// ---- STEP import (B-rep, not mesh) ---- +std::vector GeometryEngine::read_step_solids(const std::string& path, std::string& err) +{ + err.clear(); + std::vector out; + try { + STEPControl_Reader reader; + if (reader.ReadFile(path.c_str()) != IFSelect_RetDone) { + err = "cannot read STEP file"; + return out; + } + reader.TransferRoots(); + const TopoDS_Shape shape = reader.OneShape(); + if (shape.IsNull()) { err = "STEP file has no geometry"; return out; } + // One body per top-level solid; fall back to the whole shape (shells/faces) if none. + for (TopExp_Explorer ex(shape, TopAbs_SOLID); ex.More(); ex.Next()) + out.push_back(ex.Current()); + if (out.empty()) + out.push_back(shape); + } catch (const Standard_Failure& e) { + err = e.GetMessageString() ? e.GetMessageString() : "OCCT failed to read STEP"; + out.clear(); + } + return out; +} + +// ---- Primitive creation ---- + +TopoDS_Solid GeometryEngine::make_primitive(const PrimitiveParams& params) +{ + switch (params.type) { + case PrimitiveType::Box: + return BRepPrimAPI_MakeBox(gp_Pnt(-params.box_w/2, -params.box_d/2, 0), + params.box_w, params.box_d, params.box_h).Solid(); + case PrimitiveType::Cylinder: + return BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(0,0,0), gp_Dir(0,0,1)), + params.cyl_radius, params.cyl_height).Solid(); + case PrimitiveType::Sphere: + return BRepPrimAPI_MakeSphere(gp_Pnt(0,0,params.sph_radius), params.sph_radius).Solid(); + case PrimitiveType::Cone: + return BRepPrimAPI_MakeCone(gp_Ax2(gp_Pnt(0,0,0), gp_Dir(0,0,1)), + params.cone_r1, params.cone_r2, params.cone_height).Solid(); + case PrimitiveType::Torus: + return BRepPrimAPI_MakeTorus(gp_Ax2(gp_Pnt(0,0,params.torus_r2), gp_Dir(0,0,1)), + params.torus_r1, params.torus_r2).Solid(); + default: + return BRepPrimAPI_MakeBox(gp_Pnt(-10,-10,0), 20,20,20).Solid(); + } +} + +// ---- Face classification ---- + +FaceGroup GeometryEngine::classify_face(const TopoDS_Face& face, const TopoDS_Shape& /*solid*/) +{ + try { + BRepAdaptor_Surface surf(face); + if (surf.GetType() == GeomAbs_Plane) { + // Sample normal at center UV + double u = (surf.FirstUParameter() + surf.LastUParameter()) / 2.0; + double v = (surf.FirstVParameter() + surf.LastVParameter()) / 2.0; + gp_Pnt pt; gp_Vec du, dv; + surf.D1(u, v, pt, du, dv); + gp_Dir n = du.Crossed(dv); + if (face.Orientation() == TopAbs_REVERSED) n.Reverse(); + + if (n.Z() > 0.7) return FaceGroup::Top; + if (n.Z() < -0.7) return FaceGroup::Bottom; + return FaceGroup::Lateral; + } + } catch (...) {} + return FaceGroup::Lateral; +} + +// ---- Edge collection ---- + +std::vector GeometryEngine::collect_edges(const TopoDS_Shape& solid, FaceGroup target) +{ + std::vector result; + if (target == FaceGroup::All) { + for (TopExp_Explorer exp(solid, TopAbs_EDGE); exp.More(); exp.Next()) + result.push_back(TopoDS::Edge(exp.Current())); + return result; + } + + // Build edge-to-face map once + TopTools_IndexedDataMapOfShapeListOfShape edgeFaceMap; + TopExp::MapShapesAndAncestors(solid, TopAbs_EDGE, TopAbs_FACE, edgeFaceMap); + + for (TopExp_Explorer edgeExp(solid, TopAbs_EDGE); edgeExp.More(); edgeExp.Next()) { + const TopoDS_Edge& edge = TopoDS::Edge(edgeExp.Current()); + if (!edgeFaceMap.Contains(edge)) continue; + const TopTools_ListOfShape& faces = edgeFaceMap.FindFromKey(edge); + + bool include = false; + for (auto it = faces.begin(); it != faces.end(); ++it) { + FaceGroup fg = classify_face(TopoDS::Face(*it), solid); + if (target == FaceGroup::Top && fg == FaceGroup::Top) { include = true; break; } + if (target == FaceGroup::Bottom && fg == FaceGroup::Bottom) { include = true; break; } + if (target == FaceGroup::Lateral && fg == FaceGroup::Lateral) { include = true; break; } + } + + if (!include && target == FaceGroup::Top) { + for (auto it = faces.begin(); it != faces.end(); ++it) { + if (classify_face(TopoDS::Face(*it), solid) == FaceGroup::Top) { include = true; break; } + } + } + if (!include && target == FaceGroup::Bottom) { + for (auto it = faces.begin(); it != faces.end(); ++it) { + if (classify_face(TopoDS::Face(*it), solid) == FaceGroup::Bottom) { include = true; break; } + } + } + if (target == FaceGroup::Lateral && !include) { + int lateralCount = 0; + for (auto it = faces.begin(); it != faces.end(); ++it) { + if (classify_face(TopoDS::Face(*it), solid) == FaceGroup::Lateral) ++lateralCount; + } + if (lateralCount >= 2) include = true; + } + + if (include) result.push_back(edge); + } + + return result; +} + +// ---- Fillet/Chamfer ---- + +TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radius, FaceGroup faces) +{ + if (radius <= 0.001) return solid; + + std::vector edges = collect_edges(solid, faces); + if (edges.empty()) return solid; + + BRepFilletAPI_MakeFillet fillet(solid); + for (const auto& edge : edges) + fillet.Add(radius, edge); + fillet.Build(); + + // A too-large radius (e.g. >= half the smallest spanned dimension) makes the + // operation degenerate; OCCT leaves IsDone() false. Report it instead of + // silently returning the unfilleted solid (which reads as a false success). + if (!fillet.IsDone()) throw std::runtime_error("fillet radius too large for this geometry"); + return fillet.Shape(); +} + +TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double distance, FaceGroup faces) +{ + if (distance <= 0.001) return solid; + + std::vector edges = collect_edges(solid, faces); + if (edges.empty()) return solid; + + BRepFilletAPI_MakeChamfer chamfer(solid); + for (const auto& edge : edges) + chamfer.Add(distance, edge); // symmetric chamfer + chamfer.Build(); + + if (!chamfer.IsDone()) throw std::runtime_error("chamfer distance too large for this geometry"); + return chamfer.Shape(); +} + +TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radius, int edge_id) +{ + if (radius <= 0.001) return solid; + + TopoDS_Edge edge = edge_by_index(solid, edge_id); + if (edge.IsNull()) throw std::runtime_error("apply_fillet: invalid edge id"); + + BRepFilletAPI_MakeFillet mk(solid); + mk.Add(radius, edge); + mk.Build(); + + if (!mk.IsDone()) throw std::runtime_error("apply_fillet: OCCT fillet failed"); + return mk.Shape(); +} + +TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double distance, int edge_id) +{ + if (distance <= 0.001) return solid; + + TopoDS_Edge edge = edge_by_index(solid, edge_id); + if (edge.IsNull()) throw std::runtime_error("apply_chamfer: invalid edge id"); + + BRepFilletAPI_MakeChamfer mk(solid); + mk.Add(distance, edge); + mk.Build(); + + if (!mk.IsDone()) throw std::runtime_error("apply_chamfer: OCCT chamfer failed"); + return mk.Shape(); +} + +// ---- Tessellation ---- + +TriangleMesh GeometryEngine::tessellate(const TopoDS_Shape& shape, + double linear_deflection, + double angular_deflection) +{ + BRepMesh_IncrementalMesh mesh(shape, linear_deflection, false, angular_deflection, true); + + int nbNodes = 0, nbTri = 0; + for (TopExp_Explorer exp(shape, TopAbs_FACE); exp.More(); exp.Next()) { + TopLoc_Location loc; + Handle(Poly_Triangulation) tri = BRep_Tool::Triangulation(TopoDS::Face(exp.Current()), loc); + if (!tri.IsNull()) { nbNodes += tri->NbNodes(); nbTri += tri->NbTriangles(); } + } + if (nbTri == 0 || nbNodes == 0) return TriangleMesh{}; + + stl_file stl; + stl.stats.type = inmemory; + stl.stats.number_of_facets = (uint32_t)nbTri; + stl.stats.original_num_facets = stl.stats.number_of_facets; + stl_allocate(&stl); + + std::vector pts; pts.reserve(nbNodes); + int ndOff = 0, trOff = 0; + for (TopExp_Explorer exp(shape, TopAbs_FACE); exp.More(); exp.Next()) { + const TopoDS_Shape& F = exp.Current(); + TopLoc_Location loc; + Handle(Poly_Triangulation) tri = BRep_Tool::Triangulation(TopoDS::Face(F), loc); + if (tri.IsNull()) continue; + gp_Trsf T = loc.Transformation(); + for (int i = 1; i <= tri->NbNodes(); ++i) { + gp_Pnt p = tri->Node(i); p.Transform(T); + pts.emplace_back(Vec3f(p.X(), p.Y(), p.Z())); + } + auto orient = exp.Current().Orientation(); + int ids[3]; + for (int i = 1; i <= tri->NbTriangles(); ++i) { + Poly_Triangle t = tri->Triangle(i); t.Get(ids[0], ids[1], ids[2]); + if (orient == TopAbs_REVERSED) std::swap(ids[1], ids[2]); + stl_facet f; + f.vertex[0] = pts[ids[0]+ndOff-1].cast(); + f.vertex[1] = pts[ids[1]+ndOff-1].cast(); + f.vertex[2] = pts[ids[2]+ndOff-1].cast(); + f.extra[0]=0; f.extra[1]=0; + stl_normal n; stl_calculate_normal(n,&f); stl_normalize_vector(n); + f.normal=n; stl.facet_start[trOff+i-1]=f; + } + ndOff += tri->NbNodes(); trOff += tri->NbTriangles(); + } + TriangleMesh result; result.from_stl(stl); return result; +} + +std::string GeometryEngine::primitive_name(PrimitiveType type) +{ + switch (type) { + case PrimitiveType::Box: return "Box"; + case PrimitiveType::Cylinder: return "Cylinder"; + case PrimitiveType::Sphere: return "Sphere"; + case PrimitiveType::Cone: return "Cone"; + case PrimitiveType::Torus: return "Torus"; + default: return "Unknown"; + } +} + +// ---- Topology accessors ---- + +int GeometryEngine::face_count(const TopoDS_Shape& shape) +{ + int n = 0; + for (TopExp_Explorer e(shape, TopAbs_FACE); e.More(); e.Next()) + ++n; + return n; +} + +TopoDS_Face GeometryEngine::face_by_index(const TopoDS_Shape& shape, int index) +{ + if (index < 0) return TopoDS_Face(); + int ordinal = 0; + for (TopExp_Explorer e(shape, TopAbs_FACE); e.More(); e.Next()) { + if (ordinal == index) + return TopoDS::Face(e.Current()); + ++ordinal; + } + return TopoDS_Face(); +} + +std::vector GeometryEngine::edges_of_face(const TopoDS_Face& face) +{ + std::vector result; + TopTools_IndexedMapOfShape map; + TopExp::MapShapes(face, TopAbs_EDGE, map); + for (int i = 1; i <= map.Extent(); ++i) + result.push_back(TopoDS::Edge(map(i))); + return result; +} + +std::vector GeometryEngine::sample_edge_world(const TopoDS_Edge& edge, double chord_tol) +{ + if (BRep_Tool::Degenerated(edge)) + return {}; + + BRepAdaptor_Curve curve(edge); + GCPnts_TangentialDeflection disc(curve, 0.1, chord_tol); + + std::vector pts; + if (disc.NbPoints() >= 2) { + for (int i = 1; i <= disc.NbPoints(); ++i) { + gp_Pnt p = disc.Value(i); + pts.emplace_back(p.X(), p.Y(), p.Z()); + } + } else { + gp_Pnt p0 = curve.Value(curve.FirstParameter()); + gp_Pnt p1 = curve.Value(curve.LastParameter()); + pts.emplace_back(p0.X(), p0.Y(), p0.Z()); + pts.emplace_back(p1.X(), p1.Y(), p1.Z()); + } + return pts; +} + +Vec3d GeometryEngine::face_centroid_world(const TopoDS_Face& face) +{ + GProp_GProps props; + BRepGProp::SurfaceProperties(face, props); + gp_Pnt c = props.CentreOfMass(); + return Vec3d(c.X(), c.Y(), c.Z()); +} + +Vec3d GeometryEngine::face_normal_world(const TopoDS_Face& face) +{ + BRepAdaptor_Surface surf(face); + const double u = 0.5 * (surf.FirstUParameter() + surf.LastUParameter()); + const double v = 0.5 * (surf.FirstVParameter() + surf.LastVParameter()); + BRepLProp_SLProps props(surf, u, v, 1, 1e-6); + gp_Dir n(0.0, 0.0, 1.0); + if (props.IsNormalDefined()) n = props.Normal(); + if (face.Orientation() == TopAbs_REVERSED) n.Reverse(); // outward (account for face winding) + return Vec3d(n.X(), n.Y(), n.Z()); +} + +GeometryEngine::CylinderFace GeometryEngine::cylinder_of_face(const TopoDS_Face& face) +{ + CylinderFace cf; + if (face.IsNull()) return cf; + BRepAdaptor_Surface surf(face); + if (surf.GetType() != GeomAbs_Cylinder) return cf; + + const gp_Cylinder cyl = surf.Cylinder(); + const gp_Ax1 ax = cyl.Axis(); + const Vec3d axis(ax.Direction().X(), ax.Direction().Y(), ax.Direction().Z()); + const Vec3d apt (ax.Location().X(), ax.Location().Y(), ax.Location().Z()); + cf.radius = cyl.Radius(); + + // Axial extent: V is the axial parameter on a cylinder; bound the face's two ends and + // order them so `axis` points base -> top. + const double umid = 0.5 * (surf.FirstUParameter() + surf.LastUParameter()); + const gp_Pnt e0 = surf.Value(umid, surf.FirstVParameter()); + const gp_Pnt e1 = surf.Value(umid, surf.LastVParameter()); + double t0 = (Vec3d(e0.X(), e0.Y(), e0.Z()) - apt).dot(axis); + double t1 = (Vec3d(e1.X(), e1.Y(), e1.Z()) - apt).dot(axis); + if (t1 < t0) std::swap(t0, t1); + cf.base = apt + axis * t0; + cf.axis = axis; + cf.height = t1 - t0; + + // Internal (bore) vs external: compare the face's outward normal at its centre to the + // outward radial direction. A bore's normal points toward the axis (dot < 0). + const gp_Pnt sp = surf.Value(umid, 0.5 * (surf.FirstVParameter() + surf.LastVParameter())); + const Vec3d S(sp.X(), sp.Y(), sp.Z()); + const Vec3d axpt = cf.base + axis * (S - cf.base).dot(axis); + const Vec3d radial = (S - axpt).normalized(); + cf.internal = face_normal_world(face).dot(radial) < 0.0; + cf.ok = true; + return cf; +} + +bool GeometryEngine::face_plane_bounds(const TopoDS_Face& face, const Vec3d& origin, + const Vec3d& x_axis, const Vec3d& y_axis, + double& umin, double& umax, double& vmin, double& vmax) +{ + umin = vmin = 1e30; umax = vmax = -1e30; + bool any = false; + for (TopExp_Explorer ex(face, TopAbs_VERTEX); ex.More(); ex.Next()) { + const gp_Pnt p = BRep_Tool::Pnt(TopoDS::Vertex(ex.Current())); + const Vec3d P(p.X(), p.Y(), p.Z()); + const double u = (P - origin).dot(x_axis); + const double v = (P - origin).dot(y_axis); + umin = std::min(umin, u); umax = std::max(umax, u); + vmin = std::min(vmin, v); vmax = std::max(vmax, v); + any = true; + } + return any; +} + +int GeometryEngine::edge_count(const TopoDS_Shape& shape) +{ + TopTools_IndexedMapOfShape map; + TopExp::MapShapes(shape, TopAbs_EDGE, map); + return map.Extent(); +} + +TopoDS_Edge GeometryEngine::edge_by_index(const TopoDS_Shape& shape, int index) +{ + TopTools_IndexedMapOfShape map; + TopExp::MapShapes(shape, TopAbs_EDGE, map); + if (index < 0 || index >= map.Extent()) + return TopoDS_Edge(); + return TopoDS::Edge(map(index + 1)); +} + +int GeometryEngine::edge_index_of(const TopoDS_Shape& shape, const TopoDS_Edge& edge) +{ + TopTools_IndexedMapOfShape map; + TopExp::MapShapes(shape, TopAbs_EDGE, map); + int idx = map.FindIndex(edge); + return (idx > 0) ? (idx - 1) : -1; +} + +} // namespace Slic3r diff --git a/src/libslic3r/GeometryEngine.hpp b/src/libslic3r/GeometryEngine.hpp new file mode 100644 index 0000000000..f65afa4d5c --- /dev/null +++ b/src/libslic3r/GeometryEngine.hpp @@ -0,0 +1,121 @@ +#ifndef slic3r_GeometryEngine_hpp_ +#define slic3r_GeometryEngine_hpp_ + +#include "TriangleMesh.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Slic3r { + +enum class PrimitiveType { Box, Cylinder, Sphere, Cone, Torus, COUNT }; +enum class DressUpType { Fillet, Chamfer }; +enum class FaceGroup { Top, Bottom, Lateral, All }; + +struct PrimitiveParams { + PrimitiveType type{PrimitiveType::Box}; + double box_w{20}, box_h{20}, box_d{20}; + double cyl_radius{10}, cyl_height{20}; + double sph_radius{10}; + double cone_r1{10}, cone_r2{5}, cone_height{20}; + double torus_r1{10}, torus_r2{3}; + + // Dress-up + bool dressup_enabled{false}; + DressUpType dressup_type{DressUpType::Fillet}; + FaceGroup dressup_faces{FaceGroup::All}; + double dressup_radius{1.0}; // fillet radius + double dressup_chamfer_dist{1.0}; // chamfer distance (symmetric) + + // Mesh quality + double linear_deflection{0.01}; + double angular_deflection{0.5}; + + template + void serialize(Archive& ar) { + ar(type, box_w, box_h, box_d, cyl_radius, cyl_height, sph_radius, + cone_r1, cone_r2, cone_height, torus_r1, torus_r2, + dressup_enabled, dressup_type, dressup_faces, dressup_radius, dressup_chamfer_dist, + linear_deflection, angular_deflection); + } +}; + +class GeometryEngine +{ +public: + static TopoDS_Solid make_primitive(const PrimitiveParams& params); + + // Read a STEP file into its top-level solids (one TopoDS_Shape per solid; falls back to + // the whole shape if it contains no closed solids). Reuses OCCT's STEPControl_Reader, + // already linked via Format/STEP.cpp — no new dependency. err is set on failure (empty result). + static std::vector read_step_solids(const std::string& path, std::string& err); + + static TopoDS_Shape apply_fillet(const TopoDS_Shape& solid, double radius, + FaceGroup faces = FaceGroup::All); + static TopoDS_Shape apply_fillet(const TopoDS_Shape& solid, double radius, + int edge_id); + static TopoDS_Shape apply_chamfer(const TopoDS_Shape& solid, double distance, + FaceGroup faces = FaceGroup::All); + static TopoDS_Shape apply_chamfer(const TopoDS_Shape& solid, double distance, + int edge_id); + + static TriangleMesh tessellate(const TopoDS_Shape& shape, + double linear_deflection = 0.01, + double angular_deflection = 0.5); + static std::string primitive_name(PrimitiveType type); + + // Topology accessors for in-viewport face/edge picking (Design tab). Face index is the + // TopExp_Explorer(shape, TopAbs_FACE) ordinal — identical to SketchEngine::tessellate's + // per-triangle face id, so a picked triangle's id maps back to a face here. + static TopoDS_Face face_by_index(const TopoDS_Shape& shape, int index); // null if out of range + static int face_count(const TopoDS_Shape& shape); + static std::vector edges_of_face(const TopoDS_Face& face); + // Centre of mass (world) of a face — used to compute the extrude length for "up to face". + static Vec3d face_centroid_world(const TopoDS_Face& face); + // Outward unit normal of a face at its UV midpoint (orientation-aware) — for the shell gizmo. + static Vec3d face_normal_world(const TopoDS_Face& face); + // Sample an edge into a world-space polyline (>=2 pts) for pick-distance + highlight. + static std::vector sample_edge_world(const TopoDS_Edge& edge, double chord_tol = 0.05); + // 0-based edge index into TopExp::MapShapes(shape, TopAbs_EDGE, map). + static int edge_count(const TopoDS_Shape& shape); + static TopoDS_Edge edge_by_index(const TopoDS_Shape& shape, int index); + static int edge_index_of(const TopoDS_Shape& shape, const TopoDS_Edge& edge); + + // Analysis of a cylindrical face for the Thread tool (a hole bore or a cylinder's lateral + // surface): axis (base at the lower axial end + unit direction), radius, axial extent, and + // whether it is a bore (face normal points toward the axis = internal thread). ok=false if + // the face is not a cylinder. + struct CylinderFace { + bool ok{false}; + Vec3d base{0, 0, 0}; + Vec3d axis{0, 0, 1}; + double radius{0}; + double height{0}; + bool internal{false}; + }; + static CylinderFace cylinder_of_face(const TopoDS_Face& face); + + // Plane-coordinate (u,v) bounding box of a face's vertices, measured from `origin` along + // `x_axis`/`y_axis`. Lets the Hole tool dimension the hole from the face SIDES (umin/vmin = + // two adjacent edges) instead of from the centre. Returns false if the face has no vertices. + static bool face_plane_bounds(const TopoDS_Face& face, const Vec3d& origin, + const Vec3d& x_axis, const Vec3d& y_axis, + double& umin, double& umax, double& vmin, double& vmax); + +private: + static std::vector collect_edges(const TopoDS_Shape& solid, FaceGroup faces); + static FaceGroup classify_face(const TopoDS_Face& face, const TopoDS_Shape& solid); +}; + +} // namespace Slic3r + +#endif // slic3r_GeometryEngine_hpp_ diff --git a/src/libslic3r/SketchConstraints.cpp b/src/libslic3r/SketchConstraints.cpp new file mode 100644 index 0000000000..ea41ba152e --- /dev/null +++ b/src/libslic3r/SketchConstraints.cpp @@ -0,0 +1,307 @@ +#include "SketchConstraints.hpp" +#include +#include + +namespace Slic3r { + +int SketchConstraints::add_point(double x, double y) +{ + m_vars.push_back(x); + m_vars.push_back(y); + return static_cast(m_vars.size() / 2) - 1; +} + +void SketchConstraints::set_point(int id, double x, double y) +{ + size_t idx = 2 * id; + m_vars[idx] = x; + m_vars[idx + 1] = y; +} + +Vec2d SketchConstraints::get_point(int id) const +{ + size_t idx = 2 * id; + return Vec2d(m_vars[idx], m_vars[idx + 1]); +} + +int SketchConstraints::point_count() const +{ + return static_cast(m_vars.size() / 2); +} + +void SketchConstraints::fix_point(int id) +{ + size_t idx = 2 * id; + Con c; + c.type = FIX_POINT; + c.a = id; + c.b = c.c = c.d = 0; + c.k0 = m_vars[idx]; + c.k1 = m_vars[idx + 1]; + m_cons.push_back(c); +} + +void SketchConstraints::coincident(int a, int b) +{ + Con c; + c.type = COINCIDENT; + c.a = a; c.b = b; c.c = c.d = 0; + c.k0 = c.k1 = 0; + m_cons.push_back(c); +} + +void SketchConstraints::horizontal(int a, int b) +{ + Con c; + c.type = HORIZONTAL; + c.a = a; c.b = b; c.c = c.d = 0; + c.k0 = c.k1 = 0; + m_cons.push_back(c); +} + +void SketchConstraints::vertical(int a, int b) +{ + Con c; + c.type = VERTICAL; + c.a = a; c.b = b; c.c = c.d = 0; + c.k0 = c.k1 = 0; + m_cons.push_back(c); +} + +void SketchConstraints::distance(int a, int b, double d) +{ + Con c; + c.type = DISTANCE; + c.a = a; c.b = b; c.c = c.d = 0; + c.k0 = d; c.k1 = 0; + m_cons.push_back(c); +} + +void SketchConstraints::lock_x(int id, double x) +{ + Con c; + c.type = LOCK_X; + c.a = id; + c.b = c.c = c.d = 0; + c.k0 = x; c.k1 = 0; + m_cons.push_back(c); +} + +void SketchConstraints::lock_y(int id, double y) +{ + Con c; + c.type = LOCK_Y; + c.a = id; + c.b = c.c = c.d = 0; + c.k0 = y; c.k1 = 0; + m_cons.push_back(c); +} + +void SketchConstraints::equal_length(int a, int b, int c, int d) +{ + Con con; + con.type = EQUAL_LENGTH; + con.a = a; con.b = b; con.c = c; con.d = d; + con.k0 = con.k1 = 0; + m_cons.push_back(con); +} + +void SketchConstraints::parallel(int a, int b, int c, int d) +{ + Con con; + con.type = PARALLEL; + con.a = a; con.b = b; con.c = c; con.d = d; + con.k0 = con.k1 = 0; + m_cons.push_back(con); +} + +void SketchConstraints::perpendicular(int a, int b, int c, int d) +{ + Con con; + con.type = PERPENDICULAR; + con.a = a; con.b = b; con.c = c; con.d = d; + con.k0 = con.k1 = 0; + m_cons.push_back(con); +} + +void SketchConstraints::midpoint(int m, int a, int b) +{ + Con con; + con.type = MIDPOINT; + con.a = m; con.b = a; con.c = b; con.d = -1; + con.k0 = con.k1 = 0; + m_cons.push_back(con); +} + +void SketchConstraints::symmetric(int a, int b, int c, int d) +{ + Con con; + con.type = SYMMETRIC; + con.a = a; con.b = b; con.c = c; con.d = d; + con.k0 = con.k1 = 0; + m_cons.push_back(con); +} + +void SketchConstraints::angle(int a, int b, int c, int d, double radians) +{ + Con con; + con.type = ANGLE; + con.a = a; con.b = b; con.c = c; con.d = d; + con.k0 = radians; con.k1 = 0; + m_cons.push_back(con); +} + +void SketchConstraints::point_line_distance(int p, int a, int b, double dist) +{ + Con con; + con.type = PT_LINE_DIST; + con.a = p; con.b = a; con.c = b; con.d = -1; + con.k0 = dist; con.k1 = 0; + m_cons.push_back(con); +} + +Eigen::VectorXd SketchConstraints::residuals(const std::vector& v) const +{ + auto X = [&](int i) { return v[2 * i]; }; + auto Y = [&](int i) { return v[2 * i + 1]; }; + + std::vector res; + for (const auto& c : m_cons) { + switch (c.type) { + case FIX_POINT: + res.push_back(X(c.a) - c.k0); + res.push_back(Y(c.a) - c.k1); + break; + case COINCIDENT: + res.push_back(X(c.a) - X(c.b)); + res.push_back(Y(c.a) - Y(c.b)); + break; + case HORIZONTAL: + res.push_back(Y(c.a) - Y(c.b)); + break; + case VERTICAL: + res.push_back(X(c.a) - X(c.b)); + break; + case DISTANCE: + res.push_back(std::hypot(X(c.a) - X(c.b), Y(c.a) - Y(c.b)) - c.k0); + break; + case LOCK_X: + res.push_back(X(c.a) - c.k0); + break; + case LOCK_Y: + res.push_back(Y(c.a) - c.k0); + break; + case EQUAL_LENGTH: + res.push_back(std::hypot(X(c.a) - X(c.b), Y(c.a) - Y(c.b)) - + std::hypot(X(c.c) - X(c.d), Y(c.c) - Y(c.d))); + break; + case PARALLEL: + res.push_back((X(c.b) - X(c.a)) * (Y(c.d) - Y(c.c)) - + (Y(c.b) - Y(c.a)) * (X(c.d) - X(c.c))); + break; + case PERPENDICULAR: + res.push_back((X(c.b) - X(c.a)) * (X(c.d) - X(c.c)) + + (Y(c.b) - Y(c.a)) * (Y(c.d) - Y(c.c))); + break; + case MIDPOINT: + res.push_back(X(c.a) - 0.5 * (X(c.b) + X(c.c))); + res.push_back(Y(c.a) - 0.5 * (Y(c.b) + Y(c.c))); + break; + case SYMMETRIC: { + const double abx = X(c.b) - X(c.a), aby = Y(c.b) - Y(c.a); + const double cdx = X(c.d) - X(c.c), cdy = Y(c.d) - Y(c.c); + res.push_back(abx * cdx + aby * cdy); + const double mx = 0.5 * (X(c.a) + X(c.b)); + const double my = 0.5 * (Y(c.a) + Y(c.b)); + res.push_back((mx - X(c.c)) * cdy - (my - Y(c.c)) * cdx); + break; + } + case ANGLE: { + const double ux = X(c.b) - X(c.a), uy = Y(c.b) - Y(c.a); + const double wx = X(c.d) - X(c.c), wy = Y(c.d) - Y(c.c); + const double cross = ux * wy - uy * wx; + const double dot = ux * wx + uy * wy; + res.push_back(std::atan2(cross, dot) - c.k0); + break; + } + case PT_LINE_DIST: { + const double bx = X(c.b), by = Y(c.b); + const double cx = X(c.c), cy = Y(c.c); + const double L = std::hypot(cx - bx, cy - by); + const double num = (X(c.a) - bx) * (cy - by) - (Y(c.a) - by) * (cx - bx); + res.push_back((L > 1e-12 ? std::abs(num) / L : 0.0) - c.k0); + break; + } + } + } + + Eigen::VectorXd r(static_cast(res.size())); + for (size_t i = 0; i < res.size(); ++i) + r(static_cast(i)) = res[i]; + return r; +} + +Eigen::MatrixXd SketchConstraints::jacobian(const std::vector& v) const +{ + int m = static_cast(residuals(v).size()); + int n = static_cast(v.size()); + Eigen::MatrixXd J(m, n); + const double eps = 1e-7; + + std::vector vp = v; + std::vector vm = v; + + for (int j = 0; j < n; ++j) { + vp[j] = v[j] + eps; + vm[j] = v[j] - eps; + Eigen::VectorXd rp = residuals(vp); + Eigen::VectorXd rm = residuals(vm); + vp[j] = v[j]; + vm[j] = v[j]; + J.col(j) = (rp - rm) / (2.0 * eps); + } + + return J; +} + +bool SketchConstraints::solve(int max_iter, double tol) +{ + if (m_cons.empty()) return true; + double lambda = 1e-3; + Eigen::VectorXd r = residuals(m_vars); + for (int it = 0; it < max_iter; ++it) { + double rn = r.norm(); + if (rn < tol) return true; + Eigen::MatrixXd J = jacobian(m_vars); + Eigen::MatrixXd A = J.transpose() * J; + Eigen::VectorXd g = J.transpose() * r; + bool stepped = false; + for (int t = 0; t < 12; ++t) { + Eigen::MatrixXd Ad = A; + for (int i = 0; i < Ad.rows(); ++i) + Ad(i, i) += lambda * (1.0 + Ad(i, i)); + Eigen::VectorXd dx = Ad.ldlt().solve(-g); + std::vector cand = m_vars; + for (size_t i = 0; i < cand.size(); ++i) + cand[i] += dx[static_cast(i)]; + Eigen::VectorXd rc = residuals(cand); + if (rc.norm() < rn) { + m_vars = cand; + r = rc; + lambda = std::max(lambda * 0.4, 1e-12); + stepped = true; + break; + } + lambda *= 3.0; + } + if (!stepped) break; + } + return r.norm() < tol * 100; +} + +double SketchConstraints::residual_norm() const +{ + return residuals(m_vars).norm(); +} + +} // namespace Slic3r diff --git a/src/libslic3r/SketchConstraints.hpp b/src/libslic3r/SketchConstraints.hpp new file mode 100644 index 0000000000..88bcadc403 --- /dev/null +++ b/src/libslic3r/SketchConstraints.hpp @@ -0,0 +1,68 @@ +#ifndef slic3r_SketchConstraints_hpp_ +#define slic3r_SketchConstraints_hpp_ + +#include "libslic3r/Point.hpp" +#include +#include + +namespace Slic3r { + +class SketchConstraints { +public: + int add_point(double x, double y); + void set_point(int id, double x, double y); + Vec2d get_point(int id) const; + int point_count() const; + + void fix_point(int id); + void coincident(int a, int b); + void horizontal(int a, int b); + void vertical(int a, int b); + void distance(int a, int b, double d); + void lock_x(int id, double x); + void lock_y(int id, double y); + void equal_length(int a, int b, int c, int d); + void parallel(int a, int b, int c, int d); + void perpendicular(int a, int b, int c, int d); + void midpoint(int m, int a, int b); + void symmetric(int a, int b, int c, int d); + void angle(int a, int b, int c, int d, double radians); + void point_line_distance(int p, int a, int b, double dist); + + bool solve(int max_iter = 200, double tol = 1e-10); + double residual_norm() const; + +private: + std::vector m_vars; + + enum ConType : int { + FIX_POINT = 0, + COINCIDENT, + HORIZONTAL, + VERTICAL, + DISTANCE, + LOCK_X, + LOCK_Y, + EQUAL_LENGTH, + PARALLEL, + PERPENDICULAR, + MIDPOINT, + SYMMETRIC, + ANGLE, + PT_LINE_DIST + }; + + struct Con { + int type; + int a, b, c, d; + double k0, k1; + }; + std::vector m_cons; + + Eigen::VectorXd residuals(const std::vector& v) const; + Eigen::MatrixXd jacobian(const std::vector& v) const; +}; + +} // namespace Slic3r + +#endif // slic3r_SketchConstraints_hpp_ diff --git a/src/libslic3r/SketchEngine.cpp b/src/libslic3r/SketchEngine.cpp new file mode 100644 index 0000000000..a831360673 --- /dev/null +++ b/src/libslic3r/SketchEngine.cpp @@ -0,0 +1,1375 @@ +#include "SketchEngine.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Slic3r { + +// ---- SketchPlane ---- + +gp_Pln SketchPlane::to_occt() const +{ + gp_Pnt o(origin.x(), origin.y(), origin.z()); + gp_Dir n(normal.x(), normal.y(), normal.z()); + gp_Dir x(x_axis.x(), x_axis.y(), x_axis.z()); + return gp_Pln(gp_Ax3(o, n, x)); +} + +SketchPlane SketchPlane::from_face(const TopoDS_Face& face) +{ + SketchPlane sp; + // Use the first triangulation vertex + face normal + TopLoc_Location loc; + Handle(Poly_Triangulation) tri = BRep_Tool::Triangulation(face, loc); + if (!tri.IsNull() && tri->NbNodes() > 0) { + gp_Pnt p = tri->Node(1).Transformed(loc.Transformation()); + sp.origin = Vec3d(p.X(), p.Y(), p.Z()); + } + // Compute normal from BRep face + BRepAdaptor_Surface surf(face); + if (surf.GetType() != GeomAbs_Plane) { + // Fallback: try to use the first uv point + double u1 = surf.FirstUParameter(), v1 = surf.FirstVParameter(); + gp_Pnt pt; gp_Vec du, dv; + surf.D1(u1, v1, pt, du, dv); + gp_Dir n = gp_Dir(du.Crossed(dv)); + sp.normal = Vec3d(n.X(), n.Y(), n.Z()); + sp.x_axis = Vec3d(du.X(), du.Y(), du.Z()).normalized(); + sp.y_axis = sp.normal.cross(sp.x_axis).normalized(); + return sp; + } + // Plane face — use the plane directly + gp_Pln pln = surf.Plane(); + sp.origin = Vec3d(pln.Location().X(), pln.Location().Y(), pln.Location().Z()); + gp_Dir n = pln.Axis().Direction(); + sp.normal = Vec3d(n.X(), n.Y(), n.Z()); + gp_Dir xd = pln.XAxis().Direction(); + sp.x_axis = Vec3d(xd.X(), xd.Y(), xd.Z()); + sp.y_axis = sp.normal.cross(sp.x_axis).normalized(); + return sp; +} + +Vec2d SketchPlane::project(const Vec3d& ray_origin, const Vec3d& ray_dir) const +{ + double denom = ray_dir.dot(normal); + if (std::abs(denom) < 1e-12) + return {0, 0}; // ray parallel to plane + double t = (origin - ray_origin).dot(normal) / denom; + if (t < 0) + return {0, 0}; // behind camera + Vec3d hit = ray_origin + t * ray_dir; + Vec3d local = hit - origin; + return {local.dot(x_axis), local.dot(y_axis)}; +} + +Vec3d SketchPlane::to_world(const Vec2d& pt) const +{ + return origin + x_axis * pt.x() + y_axis * pt.y(); +} + +// ---- SketchProfile ---- + +bool SketchProfile::is_closed(double tolerance) const +{ + if (points.size() < 3) return false; + return (points.front() - points.back()).norm() < tolerance; +} + +bool SketchProfile::try_close(double tolerance) +{ + if (is_closed(tolerance)) { + closed = true; + return true; + } + if (points.size() < 2) return false; + if ((points.front() - points.back()).norm() < tolerance) { + closed = true; + return true; + } + return false; +} + +TopoDS_Wire SketchProfile::to_occt_wire(const SketchPlane& plane) const +{ + if (points.size() < 2) + throw std::runtime_error("Profile has fewer than 2 points"); + + BRepBuilderAPI_MakeWire builder; + for (size_t i = 0; i < points.size(); ++i) { + Vec3d a3 = plane.to_world(points[i]); + Vec3d b3 = plane.to_world(points[(i + 1) % points.size()]); + gp_Pnt pa(a3.x(), a3.y(), a3.z()); + gp_Pnt pb(b3.x(), b3.y(), b3.z()); + builder.Add(BRepBuilderAPI_MakeEdge(pa, pb).Edge()); + } + builder.Build(); + if (!builder.IsDone()) + throw std::runtime_error("Failed to build wire from profile"); + return builder.Wire(); +} + +// ---- SketchEngine ---- + +static TopoDS_Shape extrude_face_internal(const TopoDS_Face& face, const gp_Dir& dir, double length, bool symmetric) +{ + gp_Vec vec = gp_Vec(dir) * length; + if (symmetric) { + gp_Vec halfVec = gp_Vec(dir) * (length / 2.0); + BRepPrimAPI_MakePrism pos(face, halfVec); + BRepPrimAPI_MakePrism neg(face, -halfVec); + if (!pos.IsDone() || !neg.IsDone()) throw std::runtime_error("Symmetric extrude failed"); + BRepAlgoAPI_Fuse fuse(pos.Shape(), neg.Shape()); + if (!fuse.IsDone()) throw std::runtime_error("Fuse failed"); + return fuse.Shape(); + } + BRepPrimAPI_MakePrism prism(face, vec); + if (!prism.IsDone()) throw std::runtime_error("Extrude failed"); + return prism.Shape(); +} + +TopoDS_Shape SketchEngine::make_extrude(const TopoDS_Wire& wire, const SketchPlane& plane, + double length, bool symmetric, double /*taper_deg*/) +{ + BRepBuilderAPI_MakeFace fm(wire); + if (!fm.IsDone()) throw std::runtime_error("Failed to make face from wire"); + gp_Dir dir(plane.normal.x(), plane.normal.y(), plane.normal.z()); + return extrude_face_internal(fm.Face(), dir, length, symmetric); +} + +TopoDS_Shape SketchEngine::make_extrude_two_sided(const TopoDS_Wire& wire, const SketchPlane& plane, + double up, double down) +{ + BRepBuilderAPI_MakeFace fm(wire); + if (!fm.IsDone()) throw std::runtime_error("Failed to make face from wire"); + gp_Dir dir(plane.normal.x(), plane.normal.y(), plane.normal.z()); + const double u = std::abs(up), d = std::abs(down); + if (u < 1e-9 && d < 1e-9) return TopoDS_Shape(); + if (d < 1e-9) { BRepPrimAPI_MakePrism p(fm.Face(), gp_Vec(dir) * u); return p.Shape(); } + if (u < 1e-9) { BRepPrimAPI_MakePrism p(fm.Face(), gp_Vec(dir) * -d); return p.Shape(); } + BRepPrimAPI_MakePrism pos(fm.Face(), gp_Vec(dir) * u); + BRepPrimAPI_MakePrism neg(fm.Face(), gp_Vec(dir) * -d); + BRepAlgoAPI_Fuse fuse(pos.Shape(), neg.Shape()); + if (!fuse.IsDone()) throw std::runtime_error("two-sided extrude fuse failed"); + return fuse.Shape(); +} + +TopoDS_Shape SketchEngine::make_extrude_taper(const TopoDS_Wire& wire, const SketchPlane& plane, + double length, double taper_deg) +{ + gp_Dir dir(plane.normal.x(), plane.normal.y(), plane.normal.z()); + auto straight = [&]() -> TopoDS_Shape { + BRepBuilderAPI_MakeFace fm(wire); + BRepPrimAPI_MakePrism prism(fm.Face(), gp_Vec(dir) * length); + return prism.Shape(); + }; + if (std::abs(taper_deg) >= 89.0 || std::abs(length) < 1e-9) return straight(); + const double off = length * std::tan(taper_deg * M_PI / 180.0); + if (std::abs(off) < 1e-7) return straight(); + try { + // 1) offset the planar base wire in its own plane by `off` + BRepOffsetAPI_MakeOffset mko(wire, GeomAbs_Arc); + mko.Perform(off); + if (!mko.IsDone()) return straight(); + TopoDS_Shape offShape = mko.Shape(); + TopoDS_Wire topFlat; + if (offShape.ShapeType() == TopAbs_WIRE) topFlat = TopoDS::Wire(offShape); + else { for (TopExp_Explorer ex(offShape, TopAbs_WIRE); ex.More(); ex.Next()) { topFlat = TopoDS::Wire(ex.Current()); break; } } + if (topFlat.IsNull()) return straight(); + // 2) lift it along the normal by `length` + gp_Trsf tr; tr.SetTranslation(gp_Vec(dir) * length); + BRepBuilderAPI_Transform xf(topFlat, tr, Standard_True); + TopoDS_Wire topWire = TopoDS::Wire(xf.Shape()); + // 3) loft base -> top into a solid + BRepOffsetAPI_ThruSections loft(Standard_True /*solid*/, Standard_False /*ruled*/); + loft.AddWire(wire); + loft.AddWire(topWire); + loft.Build(); + if (!loft.IsDone()) return straight(); + TopoDS_Shape s = loft.Shape(); + if (s.IsNull()) return straight(); + return s; + } catch (const Standard_Failure&) { + return straight(); + } +} + +TopoDS_Shape SketchEngine::make_extrude_face(const TopoDS_Face& face, const SketchPlane& plane, + double length, bool symmetric, double /*taper_deg*/) +{ + gp_Dir dir(plane.normal.x(), plane.normal.y(), plane.normal.z()); + return extrude_face_internal(face, dir, length, symmetric); +} + +TopoDS_Shape SketchEngine::make_extrude_regions( + const std::vector>>& regions, + const SketchPlane& plane, double length, bool symmetric) +{ + // Drop consecutive coincident points and the closing duplicate. FreeType / + // SVG flattening routinely emits repeated points which would build a + // degenerate OCCT edge and make the wire builder throw — sanitising keeps a + // single bad glyph from killing the whole extrude. + auto clean = [](const std::vector& pts) { + const double eps2 = 1e-12; // ~1e-6 mm + std::vector out; + out.reserve(pts.size()); + for (const Vec2d& p : pts) + if (out.empty() || (p - out.back()).squaredNorm() > eps2) + out.push_back(p); + while (out.size() >= 2 && (out.front() - out.back()).squaredNorm() <= eps2) + out.pop_back(); + return out; + }; + + // Build a closed planar wire from a contour. Never throws — returns a null + // wire on any failure so the caller can skip just that contour. Winding is + // NOT normalised here: ShapeFix_Face::FixOrientation() below classifies outer + // vs hole by geometry and fixes orientations, which is robust to the + // inconsistent winding Emboss/NSVG glyph contours arrive with (manual + // winding guesses extrude holed glyphs (P, e, o, 8) inverted or solid). + auto contour_wire = [&](const std::vector& raw) -> TopoDS_Wire { + std::vector pts = clean(raw); + if (pts.size() < 3) return TopoDS_Wire{}; + SketchProfile prof; + prof.points = std::move(pts); + prof.closed = true; + try { return prof.to_occt_wire(plane); } + catch (...) { return TopoDS_Wire{}; } + }; + + gp_Dir dir(plane.normal.x(), plane.normal.y(), plane.normal.z()); + + // Accumulate each region's solid into a compound rather than boolean-fusing: + // glyphs are independent profiles, so a compound avoids every boolean-failure + // mode (and is faster). Holes are still handled per region by MakeFace. + BRep_Builder bb; + TopoDS_Compound comp; + bb.MakeCompound(comp); + int count = 0; + TopoDS_Shape last; + + for (const auto& region : regions) { + if (region.empty()) continue; + try { + TopoDS_Wire outer = contour_wire(region[0]); + if (outer.IsNull()) continue; + + // Add the outer loop + every hole loop as-is, then let ShapeFix_Face + // classify outer vs holes by area/containment and set correct wire + // orientations. This is winding-independent, so holed glyphs extrude + // with a solid body and empty counters regardless of source winding. + BRepBuilderAPI_MakeFace fm(outer); + if (!fm.IsDone()) continue; + for (size_t h = 1; h < region.size(); ++h) { + TopoDS_Wire hole = contour_wire(region[h]); + if (hole.IsNull()) continue; + fm.Add(hole); + } + if (!fm.IsDone()) continue; + + ShapeFix_Face sff(fm.Face()); + sff.FixOrientation(); + const TopoDS_Face face = sff.Face(); + + TopoDS_Shape solid = extrude_face_internal(face, dir, length, symmetric); + bb.Add(comp, solid); + last = solid; + ++count; + } catch (...) { + continue; // skip one bad glyph rather than fail the whole insert + } + } + + if (count == 0) throw std::runtime_error("imported regions produced no extrudable geometry"); + return count == 1 ? last : TopoDS_Shape(comp); // avoid a compound-of-one +} + +TopoDS_Shape SketchEngine::make_revolve(const TopoDS_Wire& wire, const SketchPlane& plane, + double angle_deg, int axis_sel) +{ + BRepBuilderAPI_MakeFace faceMaker(wire); + if (!faceMaker.IsDone()) + throw std::runtime_error("Failed to make face from wire"); + TopoDS_Face face = faceMaker.Face(); + + // Revolution axis lies in the sketch plane through its origin: X (0) or Y (1). + const Vec3d& adir = (axis_sel == 1) ? plane.y_axis : plane.x_axis; + gp_Pnt o(plane.origin.x(), plane.origin.y(), plane.origin.z()); + gp_Dir xd(adir.x(), adir.y(), adir.z()); + gp_Ax1 axis(o, xd); + + double angle_rad = angle_deg * M_PI / 180.0; + // A negative angle is expressed as a positive sweep about the reversed axis, + // since BRepPrimAPI_MakeRevol expects an angle in (0, 2*pi]. + if (angle_rad < 0) { axis.Reverse(); angle_rad = -angle_rad; } + BRepPrimAPI_MakeRevol rev(face, axis, angle_rad); + if (!rev.IsDone()) + throw std::runtime_error("Failed to revolve"); + return rev.Shape(); +} + +TopoDS_Shape SketchEngine::make_sweep(const TopoDS_Wire& profile, const TopoDS_Wire& path) +{ + BRepBuilderAPI_MakeFace faceMaker(profile); + if (!faceMaker.IsDone()) + throw std::runtime_error("Failed to make face from sweep profile"); + TopoDS_Face face = faceMaker.Face(); + + BRepOffsetAPI_MakePipe pipe(path, face); + pipe.Build(); + if (!pipe.IsDone()) + throw std::runtime_error("Failed to sweep profile along path"); + return pipe.Shape(); +} + +TopoDS_Shape SketchEngine::make_loft(const std::vector& profiles, bool ruled) +{ + if (profiles.size() < 2) + throw std::runtime_error("loft needs at least 2 profiles"); + BRepOffsetAPI_ThruSections loft(Standard_True /*solid*/, + ruled ? Standard_True : Standard_False); + for (const TopoDS_Wire& w : profiles) { + if (w.IsNull()) throw std::runtime_error("loft: null profile wire"); + loft.AddWire(w); + } + loft.Build(); + if (!loft.IsDone()) throw std::runtime_error("loft failed"); + TopoDS_Shape s = loft.Shape(); + if (s.IsNull()) throw std::runtime_error("loft produced no solid"); + return s; +} + +TopoDS_Shape SketchEngine::make_pocket(const TopoDS_Wire& wire, const SketchPlane& plane, + const TopoDS_Shape& target, double depth) +{ + BRepBuilderAPI_MakeFace fm(wire); + if (!fm.IsDone()) throw std::runtime_error("Pocket face failed"); + TopoDS_Shape tool = extrude_face_internal(fm.Face(), + gp_Dir(plane.normal.x(), plane.normal.y(), plane.normal.z()), depth + 1.0, false); + BRepAlgoAPI_Cut cut(target, tool); + if (!cut.IsDone()) throw std::runtime_error("Pocket cut failed"); + return cut.Shape(); +} + +TriangleMesh SketchEngine::tessellate(const TopoDS_Shape& shape, + double linear_deflection, + double angular_deflection) +{ + std::vector dummy; + return tessellate(shape, dummy, linear_deflection, angular_deflection); +} + +TriangleMesh SketchEngine::tessellate(const TopoDS_Shape& shape, + std::vector& tri_face, + double linear_deflection, + double angular_deflection) +{ + tri_face.clear(); + BRepMesh_IncrementalMesh mesh(shape, linear_deflection, false, angular_deflection, true); + + int nbNodes = 0, nbTriangles = 0; + for (TopExp_Explorer exp(shape, TopAbs_FACE); exp.More(); exp.Next()) { + TopLoc_Location loc; + Handle(Poly_Triangulation) tri = BRep_Tool::Triangulation(TopoDS::Face(exp.Current()), loc); + if (!tri.IsNull()) { + nbNodes += tri->NbNodes(); + nbTriangles += tri->NbTriangles(); + } + } + + if (nbTriangles == 0 || nbNodes == 0) + return TriangleMesh{}; + + indexed_triangle_set raw; + raw.vertices.reserve(nbNodes); + raw.indices.reserve(nbTriangles); + tri_face.reserve(nbTriangles); + + int faceIdx = -1; + int nodeOff = 0; + for (TopExp_Explorer exp(shape, TopAbs_FACE); exp.More(); exp.Next()) { + ++faceIdx; + + TopLoc_Location loc; + Handle(Poly_Triangulation) tri = BRep_Tool::Triangulation(TopoDS::Face(exp.Current()), loc); + if (tri.IsNull()) continue; + + gp_Trsf trsf = loc.Transformation(); + for (int i = 1; i <= tri->NbNodes(); ++i) { + gp_Pnt p = tri->Node(i); + p.Transform(trsf); + raw.vertices.emplace_back(Vec3f(p.X(), p.Y(), p.Z())); + } + + TopAbs_Orientation orient = exp.Current().Orientation(); + int ids[3]; + for (int i = 1; i <= tri->NbTriangles(); ++i) { + Poly_Triangle t = tri->Triangle(i); + t.Get(ids[0], ids[1], ids[2]); + if (orient == TopAbs_REVERSED) + std::swap(ids[1], ids[2]); + + raw.indices.emplace_back(nodeOff + ids[0] - 1, + nodeOff + ids[1] - 1, + nodeOff + ids[2] - 1); + tri_face.push_back(faceIdx); + } + nodeOff += tri->NbNodes(); + } + + std::map, int> vmap; + indexed_triangle_set its; + its.indices.reserve(raw.indices.size()); + its.vertices.reserve(raw.vertices.size() / 2); + + for (const auto& tri : raw.indices) { + stl_triangle_vertex_indices new_tri; + for (int j = 0; j < 3; ++j) { + const stl_vertex& v = raw.vertices[tri[j]]; + auto key = std::make_tuple(v.x(), v.y(), v.z()); + auto it = vmap.find(key); + if (it == vmap.end()) { + int new_id = static_cast(its.vertices.size()); + vmap[key] = new_id; + its.vertices.push_back(v); + new_tri[j] = new_id; + } else { + new_tri[j] = it->second; + } + } + its.indices.push_back(new_tri); + } + + return TriangleMesh(std::move(its)); +} + +TopoDS_Wire SketchEngine::entities_to_wire(const std::vector& entities, + const SketchPlane& plane) +{ + std::vector valid; + for (const auto& e : entities) { + if (e.construction) continue; + if (e.type == SketchEntity::Type::Point) continue; + valid.push_back(&e); + } + if (valid.empty()) return TopoDS_Wire{}; + + // Build an OCCT ellipse (gp_Elips) in the sketch plane from an Ellipse(Arc) + // entity. Major-axis direction = plane-rotated (cos phi, sin phi). Enforces + // a >= b (OCCT requirement); the GUI builder already guarantees this. + auto make_elips = [&](const SketchEntity& c) -> gp_Elips { + Vec3d c3 = plane.to_world(c.center); + gp_Pnt center(c3.x(), c3.y(), c3.z()); + gp_Dir n(plane.normal.x(), plane.normal.y(), plane.normal.z()); + Vec2d maj2(std::cos(c.rotation), std::sin(c.rotation)); + Vec3d x3 = plane.to_world(c.center + maj2) - c3; + gp_Dir xdir(x3.x(), x3.y(), x3.z()); + double a = c.radius, b = c.rminor; + if (a < b) std::swap(a, b); + return gp_Elips(gp_Ax2(center, n, xdir), a, b); + }; + + // Clamped uniform B-spline (degree min(3, n-1)) through the control poles. The + // knot construction is mirrored in DesignSketchTool's GUI sampler so the on-screen + // curve matches the extruded geometry exactly. + auto make_bspline = [&](const SketchEntity& c) -> Handle(Geom_BSplineCurve) { + const int n = int(c.ctrl.size()); + const int p = n >= 4 ? 3 : (n >= 2 ? n - 1 : 0); + if (p < 1) return Handle(Geom_BSplineCurve)(); + TColgp_Array1OfPnt poles(1, n); + for (int i = 0; i < n; ++i) { + Vec3d w = plane.to_world(c.ctrl[i]); + poles.SetValue(i + 1, gp_Pnt(w.x(), w.y(), w.z())); + } + const int interior = n - p - 1; // count of single interior knots + const int nknots = interior + 2; + TColStd_Array1OfReal knots(1, nknots); + TColStd_Array1OfInteger mults(1, nknots); + knots.SetValue(1, 0.0); mults.SetValue(1, p + 1); + for (int i = 1; i <= interior; ++i) { knots.SetValue(i + 1, double(i)); mults.SetValue(i + 1, 1); } + knots.SetValue(nknots, double(interior + 1)); mults.SetValue(nknots, p + 1); + return new Geom_BSplineCurve(poles, knots, mults, p); + }; + + bool has_closed_single = false; // Circle or full Ellipse (stand-alone closed) + bool has_chain = false; // Line / Arc / EllipseArc + for (const auto* e : valid) { + if (e->type == SketchEntity::Type::Circle || e->type == SketchEntity::Type::Ellipse) + has_closed_single = true; + else + has_chain = true; + } + + // Case 1: exactly one closed entity (Circle or Ellipse) and nothing else + if (has_closed_single && !has_chain && valid.size() == 1) { + const SketchEntity& c = *valid[0]; + TopoDS_Edge e; + if (c.type == SketchEntity::Type::Ellipse) { + if (c.radius <= 1e-9 || c.rminor <= 1e-9) return TopoDS_Wire{}; + e = BRepBuilderAPI_MakeEdge(make_elips(c)).Edge(); + } else { + Vec3d c3 = plane.to_world(c.center); + gp_Pnt center(c3.x(), c3.y(), c3.z()); + gp_Dir n(plane.normal.x(), plane.normal.y(), plane.normal.z()); + gp_Circ circ(gp_Ax2(center, n), c.radius); + e = BRepBuilderAPI_MakeEdge(circ).Edge(); + } + BRepBuilderAPI_MakeWire wm(e); + if (!wm.IsDone()) return TopoDS_Wire{}; + return wm.Wire(); + } + + // Case 2: closed chain of Line/Arc/EllipseArc entities (no closed-single) + if (!has_closed_single && has_chain) { + BRepBuilderAPI_MakeWire builder; + for (const SketchEntity* e : valid) { + if (e->type == SketchEntity::Type::Line) { + Vec3d p0 = plane.to_world(e->p0); + Vec3d p1 = plane.to_world(e->p1); + gp_Pnt pa(p0.x(), p0.y(), p0.z()); + gp_Pnt pb(p1.x(), p1.y(), p1.z()); + builder.Add(BRepBuilderAPI_MakeEdge(pa, pb).Edge()); + } else if (e->type == SketchEntity::Type::EllipseArc) { + if (e->radius <= 1e-9 || e->rminor <= 1e-9) return TopoDS_Wire{}; + GC_MakeArcOfEllipse arc_maker(make_elips(*e), e->start_angle, e->end_angle, Standard_True); + if (!arc_maker.IsDone()) return TopoDS_Wire{}; + builder.Add(BRepBuilderAPI_MakeEdge(arc_maker.Value()).Edge()); + } else if (e->type == SketchEntity::Type::Arc) { + Vec3d p0 = plane.to_world(e->p0); + Vec3d p1 = plane.to_world(e->p1); + double mid_angle = (e->start_angle + e->end_angle) * 0.5; + Vec2d mid_2d(e->center.x() + e->radius * std::cos(mid_angle), + e->center.y() + e->radius * std::sin(mid_angle)); + Vec3d mid_3d = plane.to_world(mid_2d); + gp_Pnt pa(p0.x(), p0.y(), p0.z()); + gp_Pnt pm(mid_3d.x(), mid_3d.y(), mid_3d.z()); + gp_Pnt pb(p1.x(), p1.y(), p1.z()); + GC_MakeArcOfCircle arc_maker(pa, pm, pb); + if (!arc_maker.IsDone()) return TopoDS_Wire{}; + Handle(Geom_TrimmedCurve) curve = arc_maker.Value(); + builder.Add(BRepBuilderAPI_MakeEdge(curve).Edge()); + } else if (e->type == SketchEntity::Type::BSpline) { + Handle(Geom_BSplineCurve) crv = make_bspline(*e); + if (crv.IsNull()) return TopoDS_Wire{}; + builder.Add(BRepBuilderAPI_MakeEdge(crv).Edge()); + } + } + builder.Build(); + if (!builder.IsDone()) return TopoDS_Wire{}; + return builder.Wire(); + } + + return TopoDS_Wire{}; +} + +std::vector SketchEngine::mirror_entities( + const std::vector& src, const Vec2d& a, const Vec2d& b) +{ + Vec2d dir = b - a; + if (dir.norm() < 1e-12) + return src; + + dir.normalize(); + + auto reflect = [&](const Vec2d& p) -> Vec2d { + Vec2d v = p - a; + return a + (2.0 * v.dot(dir)) * dir - v; + }; + + std::vector out; + out.reserve(src.size()); + + for (const auto& e : src) { + SketchEntity m = e; + switch (e.type) { + case SketchEntity::Type::Line: + m.p0 = reflect(e.p0); + m.p1 = reflect(e.p1); + break; + case SketchEntity::Type::Point: + m.p0 = reflect(e.p0); + break; + case SketchEntity::Type::Circle: + m.center = reflect(e.center); + m.p0 = m.center; + break; + case SketchEntity::Type::Arc: { + m.p0 = reflect(e.p0); + m.p1 = reflect(e.p1); + m.center = reflect(e.center); + + const Vec2d& c = m.center; + m.start_angle = std::atan2(m.p0.y() - c.y(), m.p0.x() - c.x()); + double raw_end = std::atan2(m.p1.y() - c.y(), m.p1.x() - c.x()); + + double s = e.end_angle - e.start_angle; + + double sweep = raw_end - m.start_angle; + while (sweep <= -2.0 * M_PI) sweep += 2.0 * M_PI; + while (sweep >= 2.0 * M_PI) sweep -= 2.0 * M_PI; + + if (s != 0.0 && sweep * s > 0.0) { + if (sweep > 0.0) + sweep -= 2.0 * M_PI; + else + sweep += 2.0 * M_PI; + } + + m.end_angle = m.start_angle + sweep; + m.radius = e.radius; + break; + } + case SketchEntity::Type::Ellipse: + case SketchEntity::Type::EllipseArc: { + m.center = reflect(e.center); + // Reflect the major-axis direction; a/b unchanged. + const Vec2d majdir(std::cos(e.rotation), std::sin(e.rotation)); + const Vec2d rdir = reflect(e.center + majdir) - m.center; + m.rotation = std::atan2(rdir.y(), rdir.x()); + if (e.type == SketchEntity::Type::Ellipse) { + m.p0 = m.center; + } else { + m.p0 = reflect(e.p0); + m.p1 = reflect(e.p1); + // Reflection reverses orientation: recompute parametric angles in + // the reflected frame, original end -> new start (CCW sense kept). + auto param = [&](const Vec2d& P) { + const Vec2d d = P - m.center; + const double cu = std::cos(m.rotation), su = std::sin(m.rotation); + const double u = d.x() * cu + d.y() * su; + const double v = -d.x() * su + d.y() * cu; + return std::atan2(v / std::max(e.rminor, 1e-9), u / std::max(e.radius, 1e-9)); + }; + m.start_angle = param(m.p1); + m.end_angle = param(m.p0); + } + break; + } + case SketchEntity::Type::BSpline: + for (auto& cp : m.ctrl) cp = reflect(cp); + m.p0 = reflect(e.p0); + m.p1 = reflect(e.p1); + break; + } + out.push_back(m); + } + + return out; +} + +std::vector SketchEngine::offset_entities( + const std::vector& src, double d) +{ + std::vector out; + + for (const auto& e : src) { + switch (e.type) { + case SketchEntity::Type::Line: { + Vec2d t = e.p1 - e.p0; + if (t.norm() < 1e-12) continue; + t.normalize(); + Vec2d n(-t.y(), t.x()); + SketchEntity o = e; + o.p0 = e.p0 + d * n; + o.p1 = e.p1 + d * n; + out.push_back(o); + break; + } + case SketchEntity::Type::Circle: { + double r = e.radius + d; + if (r <= 1e-9) continue; + SketchEntity o = e; + o.radius = r; + o.p0 = o.center; + out.push_back(o); + break; + } + case SketchEntity::Type::Arc: { + double r = e.radius + d; + if (r <= 1e-9) continue; + SketchEntity o = e; + o.radius = r; + o.p0 = e.center + r * Vec2d(std::cos(e.start_angle), std::sin(e.start_angle)); + o.p1 = e.center + r * Vec2d(std::cos(e.end_angle), std::sin(e.end_angle)); + out.push_back(o); + break; + } + case SketchEntity::Type::Point: + continue; + case SketchEntity::Type::Ellipse: + case SketchEntity::Type::EllipseArc: + // A true parallel offset of an ellipse is not an ellipse; skip in v1. + continue; + case SketchEntity::Type::BSpline: + // Offset of a spline is not a same-degree spline; skip in v1. + continue; + } + } + + return out; +} + +std::vector SketchEngine::array_entities( + const std::vector& src, int count, + const Vec2d& step, double angle_step, const Vec2d& pivot) +{ + std::vector out; + if (count < 2) return out; + + for (int i = 1; i < count; ++i) { + const double ang = i * angle_step; + const double ca = std::cos(ang), sa = std::sin(ang); + const Vec2d tr = double(i) * step; + // Rigid map: rotate about pivot by `ang`, then translate by `tr`. + auto xf = [&](const Vec2d& p) -> Vec2d { + const Vec2d d = p - pivot; + return Vec2d(pivot.x() + ca * d.x() - sa * d.y(), + pivot.y() + sa * d.x() + ca * d.y()) + tr; + }; + + for (const auto& e : src) { + SketchEntity m = e; // carry construction flag, radii, etc. + switch (e.type) { + case SketchEntity::Type::Line: + m.p0 = xf(e.p0); m.p1 = xf(e.p1); + break; + case SketchEntity::Type::Point: + m.p0 = xf(e.p0); + break; + case SketchEntity::Type::Circle: + m.center = xf(e.center); m.p0 = m.center; // radius unchanged + break; + case SketchEntity::Type::Arc: { + m.center = xf(e.center); + m.start_angle = e.start_angle + ang; + m.end_angle = e.end_angle + ang; // rigid: sweep preserved + m.p0 = m.center + e.radius * Vec2d(std::cos(m.start_angle), std::sin(m.start_angle)); + m.p1 = m.center + e.radius * Vec2d(std::cos(m.end_angle), std::sin(m.end_angle)); + break; + } + case SketchEntity::Type::Ellipse: + case SketchEntity::Type::EllipseArc: + m.center = xf(e.center); + m.rotation = e.rotation + ang; // major axis rotates with the body + if (e.type == SketchEntity::Type::Ellipse) { + m.p0 = m.center; + } else { + m.p0 = xf(e.p0); m.p1 = xf(e.p1); + // Parametric angles are in the (rotated) body frame -> unchanged. + } + break; + case SketchEntity::Type::BSpline: + for (auto& cp : m.ctrl) cp = xf(cp); + m.p0 = xf(e.p0); m.p1 = xf(e.p1); + break; + } + out.push_back(m); + } + } + return out; +} + +std::vector SketchEngine::transform_entities( + const std::vector& src, + const Vec2d& move, double angle, double scale, const Vec2d& pivot) +{ + std::vector out; + out.reserve(src.size()); + const double ca = std::cos(angle), sa = std::sin(angle); + const double rs = std::abs(scale); // radii are unsigned magnitudes + // Affine map: translate pivot to origin, scale, rotate, then translate by `move`. + auto xf = [&](const Vec2d& p) -> Vec2d { + const Vec2d d = scale * (p - pivot); + return Vec2d(pivot.x() + ca * d.x() - sa * d.y(), + pivot.y() + sa * d.x() + ca * d.y()) + move; + }; + + for (const auto& e : src) { + SketchEntity m = e; // carry construction flag, etc. + switch (e.type) { + case SketchEntity::Type::Line: + m.p0 = xf(e.p0); m.p1 = xf(e.p1); + break; + case SketchEntity::Type::Point: + m.p0 = xf(e.p0); + break; + case SketchEntity::Type::Circle: + m.center = xf(e.center); m.radius = e.radius * rs; m.p0 = m.center; + break; + case SketchEntity::Type::Arc: { + m.center = xf(e.center); + m.radius = e.radius * rs; + m.start_angle = e.start_angle + angle; + m.end_angle = e.end_angle + angle; // rigid sweep, shifted by rotation + m.p0 = m.center + m.radius * Vec2d(std::cos(m.start_angle), std::sin(m.start_angle)); + m.p1 = m.center + m.radius * Vec2d(std::cos(m.end_angle), std::sin(m.end_angle)); + break; + } + case SketchEntity::Type::Ellipse: + case SketchEntity::Type::EllipseArc: + m.center = xf(e.center); + m.radius = e.radius * rs; + m.rminor = e.rminor * rs; + m.rotation = e.rotation + angle; // major axis rotates with the body + if (e.type == SketchEntity::Type::Ellipse) { + m.p0 = m.center; + } else { + m.p0 = xf(e.p0); m.p1 = xf(e.p1); + // Parametric angles live in the (rotated) body frame -> unchanged. + } + break; + case SketchEntity::Type::BSpline: + for (auto& cp : m.ctrl) cp = xf(cp); + m.p0 = xf(e.p0); m.p1 = xf(e.p1); + break; + } + out.push_back(m); + } + return out; +} + +bool SketchEngine::fillet_lines(const SketchEntity& a, const SketchEntity& b, double r, + SketchEntity& a_out, SketchEntity& b_out, SketchEntity& arc_out) +{ + if (a.type != SketchEntity::Type::Line || b.type != SketchEntity::Type::Line || r <= 1e-9) + return false; + + Vec2d da = a.p1 - a.p0; + Vec2d db = b.p1 - b.p0; + double denom = da.x() * db.y() - da.y() * db.x(); + if (std::abs(denom) < 1e-12) + return false; + + Vec2d diff = b.p0 - a.p0; + double s = (diff.x() * db.y() - diff.y() * db.x()) / denom; + Vec2d C = a.p0 + s * da; + + Vec2d ua; + int a_near_idx; + { + double d0 = (a.p0 - C).norm(); + double d1 = (a.p1 - C).norm(); + if (d0 <= d1) { + a_near_idx = 0; + ua = a.p1 - C; + } else { + a_near_idx = 1; + ua = a.p0 - C; + } + } + if (ua.norm() < 1e-12) return false; + ua.normalize(); + + Vec2d ub; + int b_near_idx; + { + double d0 = (b.p0 - C).norm(); + double d1 = (b.p1 - C).norm(); + if (d0 <= d1) { + b_near_idx = 0; + ub = b.p1 - C; + } else { + b_near_idx = 1; + ub = b.p0 - C; + } + } + if (ub.norm() < 1e-12) return false; + ub.normalize(); + + double cosT = ua.dot(ub); + cosT = std::max(-1.0, std::min(1.0, cosT)); + double theta = std::acos(cosT); + if (theta < 1e-6 || theta > M_PI - 1e-6) + return false; + + double t = r / std::tan(theta / 2.0); + { + Vec2d a_far = (a_near_idx == 0) ? a.p1 : a.p0; + Vec2d b_far = (b_near_idx == 0) ? b.p1 : b.p0; + if (t > (a_far - C).norm() || t > (b_far - C).norm()) + return false; + } + + Vec2d Ta = C + t * ua; + Vec2d Tb = C + t * ub; + + Vec2d bis = ua + ub; + if (bis.norm() < 1e-12) return false; + bis.normalize(); + + double dCO = r / std::sin(theta / 2.0); + Vec2d O = C + dCO * bis; + + a_out = a; + b_out = b; + if (a_near_idx == 0) + a_out.p0 = Ta; + else + a_out.p1 = Ta; + + if (b_near_idx == 0) + b_out.p0 = Tb; + else + b_out.p1 = Tb; + + arc_out = SketchEntity{}; + arc_out.type = SketchEntity::Type::Arc; + arc_out.center = O; + arc_out.radius = r; + arc_out.p0 = Ta; + arc_out.p1 = Tb; + arc_out.start_angle = std::atan2(Ta.y() - O.y(), Ta.x() - O.x()); + + double sb = std::atan2(Tb.y() - O.y(), Tb.x() - O.x()); + double sweep = sb - arc_out.start_angle; + while (sweep <= -M_PI) sweep += 2.0 * M_PI; + while (sweep > M_PI) sweep -= 2.0 * M_PI; + arc_out.end_angle = arc_out.start_angle + sweep; + + return true; +} + +bool SketchEngine::chamfer_lines(const SketchEntity& a, const SketchEntity& b, double d, + SketchEntity& a_out, SketchEntity& b_out, SketchEntity& seg_out) +{ + if (a.type != SketchEntity::Type::Line || b.type != SketchEntity::Type::Line || d <= 1e-9) + return false; + + Vec2d da = a.p1 - a.p0; + Vec2d db = b.p1 - b.p0; + double denom = da.x() * db.y() - da.y() * db.x(); + if (std::abs(denom) < 1e-12) + return false; // parallel: no corner to chamfer + + // Shared corner = line/line intersection. + Vec2d diff = b.p0 - a.p0; + double s = (diff.x() * db.y() - diff.y() * db.x()) / denom; + Vec2d C = a.p0 + s * da; + + // For each line, unit vector pointing from the corner toward its far endpoint + // (the endpoint that is kept); the near endpoint is the one trimmed back. + auto pick = [&](const SketchEntity& ln, int& near_idx, Vec2d& u) -> bool { + double d0 = (ln.p0 - C).norm(); + double d1 = (ln.p1 - C).norm(); + if (d0 <= d1) { near_idx = 0; u = ln.p1 - C; } + else { near_idx = 1; u = ln.p0 - C; } + if (u.norm() < 1e-12) return false; + u.normalize(); + return true; + }; + int a_near_idx, b_near_idx; + Vec2d ua, ub; + if (!pick(a, a_near_idx, ua)) return false; + if (!pick(b, b_near_idx, ub)) return false; + + // Reject collinear (no real corner) and ensure the setback fits both lines. + double cosT = std::max(-1.0, std::min(1.0, ua.dot(ub))); + if (cosT > 1.0 - 1e-9 || cosT < -1.0 + 1e-9) + return false; + { + Vec2d a_far = (a_near_idx == 0) ? a.p1 : a.p0; + Vec2d b_far = (b_near_idx == 0) ? b.p1 : b.p0; + if (d > (a_far - C).norm() || d > (b_far - C).norm()) + return false; + } + + Vec2d Ta = C + d * ua; + Vec2d Tb = C + d * ub; + + a_out = a; + b_out = b; + if (a_near_idx == 0) a_out.p0 = Ta; else a_out.p1 = Ta; + if (b_near_idx == 0) b_out.p0 = Tb; else b_out.p1 = Tb; + + seg_out = SketchEntity{}; + seg_out.type = SketchEntity::Type::Line; + seg_out.p0 = Ta; + seg_out.p1 = Tb; + return true; +} + +static std::vector line_entity_hits(const Vec2d& a0, const Vec2d& adir, + const SketchEntity& other) +{ + std::vector hits; + double La2 = adir.dot(adir); + if (La2 < 1e-18) return hits; + + switch (other.type) { + case SketchEntity::Type::Line: { + Vec2d bdir = other.p1 - other.p0; + double bxa = bdir.x() * adir.y() - bdir.y() * adir.x(); + if (std::abs(bxa) < 1e-12) return hits; + + Vec2d w = a0 - other.p0; + double u = (w.x() * adir.y() - w.y() * adir.x()) / bxa; + if (u < -1e-9 || u > 1.0 + 1e-9) return hits; + + double axb = adir.x() * bdir.y() - adir.y() * bdir.x(); + Vec2d w2 = other.p0 - a0; + double t = (w2.x() * bdir.y() - w2.y() * bdir.x()) / axb; + hits.push_back(t); + break; + } + case SketchEntity::Type::Circle: + case SketchEntity::Type::Arc: { + double R = other.radius; + Vec2d f = a0 - other.center; + double A = La2; + double B = 2.0 * adir.dot(f); + double Cc = f.dot(f) - R * R; + double disc = B * B - 4.0 * A * Cc; + if (disc < -1e-12) return hits; + if (disc < 0.0) disc = 0.0; + double sq = std::sqrt(disc); + double t1 = (-B - sq) / (2.0 * A); + double t2 = (-B + sq) / (2.0 * A); + + auto angle_in_sweep = [&](const Vec2d& P) -> bool { + double phi = std::atan2(P.y() - other.center.y(), P.x() - other.center.x()); + double sweep = other.end_angle - other.start_angle; + double delta = phi - other.start_angle; + if (sweep >= 0.0) { + while (delta < -1e-9) delta += 2.0 * M_PI; + while (delta > 2.0 * M_PI) delta -= 2.0 * M_PI; + return delta <= sweep + 1e-9; + } else { + while (delta > 1e-9) delta -= 2.0 * M_PI; + while (delta < -2.0 * M_PI) delta += 2.0 * M_PI; + return delta >= sweep - 1e-9; + } + }; + + auto check = [&](double t) { + Vec2d P = a0 + t * adir; + if (other.type == SketchEntity::Type::Circle || angle_in_sweep(P)) + hits.push_back(t); + }; + + check(t1); + if (disc > 1e-12) + check(t2); + break; + } + default: + break; + } + + return hits; +} + +// Angles (atan2, radians) where `other` crosses the circle of radius R about C. +// For Arc/Circle cutters the crossing point must lie within the cutter's own +// sweep (a full circle always qualifies). Powers arc/circle-subject trim/extend, +// where the subject is parametrized by angle rather than by a line ray param. +static std::vector circle_cross_angles(const Vec2d& C, double R, + const SketchEntity& other) +{ + std::vector out; + if (R < 1e-12) return out; + + auto on_other = [&](const Vec2d& P) -> bool { + switch (other.type) { + case SketchEntity::Type::Line: { + Vec2d d = other.p1 - other.p0; + double L2 = d.dot(d); + if (L2 < 1e-18) return false; + double u = (P - other.p0).dot(d) / L2; + return u > -1e-9 && u < 1.0 + 1e-9; + } + case SketchEntity::Type::Circle: + return true; + case SketchEntity::Type::Arc: { + double phi = std::atan2(P.y() - other.center.y(), P.x() - other.center.x()); + double sweep = other.end_angle - other.start_angle; + double delta = phi - other.start_angle; + if (sweep >= 0.0) { + while (delta < -1e-9) delta += 2.0 * M_PI; + while (delta > 2.0 * M_PI) delta -= 2.0 * M_PI; + return delta <= sweep + 1e-9; + } else { + while (delta > 1e-9) delta -= 2.0 * M_PI; + while (delta < -2.0 * M_PI) delta += 2.0 * M_PI; + return delta >= sweep - 1e-9; + } + } + default: + return false; + } + }; + auto add = [&](const Vec2d& P) { + out.push_back(std::atan2(P.y() - C.y(), P.x() - C.x())); + }; + + switch (other.type) { + case SketchEntity::Type::Line: { + Vec2d a0 = other.p0, adir = other.p1 - other.p0; + double A = adir.dot(adir); + if (A < 1e-18) break; + Vec2d f = a0 - C; + double B = 2.0 * adir.dot(f); + double Cc = f.dot(f) - R * R; + double disc = B * B - 4.0 * A * Cc; + if (disc < 0.0) break; + double sq = std::sqrt(disc); + Vec2d P1 = a0 + ((-B - sq) / (2.0 * A)) * adir; + if (on_other(P1)) add(P1); + if (disc > 1e-12) { + Vec2d P2 = a0 + ((-B + sq) / (2.0 * A)) * adir; + if (on_other(P2)) add(P2); + } + break; + } + case SketchEntity::Type::Circle: + case SketchEntity::Type::Arc: { + Vec2d C2 = other.center; + double R2 = other.radius; + Vec2d d = C2 - C; + double dd = d.norm(); + if (dd < 1e-12) break; // concentric + if (dd > R + R2 + 1e-9) break; // too far apart + if (dd < std::abs(R - R2) - 1e-9) break; // one circle inside the other + double a = (R * R - R2 * R2 + dd * dd) / (2.0 * dd); + double h2 = R * R - a * a; + if (h2 < 0.0) h2 = 0.0; + double h = std::sqrt(h2); + Vec2d mid = C + (a / dd) * d; + Vec2d perp(-d.y() / dd, d.x() / dd); + Vec2d P1 = mid + h * perp; + if (on_other(P1)) add(P1); + if (h > 1e-12) { + Vec2d P2 = mid - h * perp; + if (on_other(P2)) add(P2); + } + break; + } + default: + break; + } + return out; +} + +// Wrap x into [0, 2pi). +static double wrap_2pi(double x) +{ + while (x < 0.0) x += 2.0 * M_PI; + while (x >= 2.0 * M_PI) x -= 2.0 * M_PI; + return x; +} + +bool SketchEngine::trim_entity(SketchEntity& e, const std::vector& others, + const Vec2d& pick) +{ + // Arc subject: parametrize by sweep fraction u in [0,1]; cut on the picked side. + if (e.type == SketchEntity::Type::Arc) { + double sweep = e.end_angle - e.start_angle; + if (std::abs(sweep) < 1e-12) return false; + double phi_pick = std::atan2(pick.y() - e.center.y(), pick.x() - e.center.x()); + double u_pick = (phi_pick - e.start_angle) / sweep; + // Bring the pick onto the arc's [0,1] domain. + while (u_pick < -1e-9) u_pick += (2.0 * M_PI) / std::abs(sweep); + u_pick = std::max(0.0, std::min(1.0, u_pick)); + + std::vector cuts; + for (const auto& other : others) { + for (double phi : circle_cross_angles(e.center, e.radius, other)) { + double u = (phi - e.start_angle) / sweep; + while (u < -1e-9) u += (2.0 * M_PI) / std::abs(sweep); + if (u > 1e-9 && u < 1.0 - 1e-9) cuts.push_back(u); + } + } + if (cuts.empty()) return false; + + if (u_pick <= 0.5) { + double uc = std::numeric_limits::max(); + for (double u : cuts) if (u > u_pick + 1e-9 && u < uc) uc = u; + if (uc == std::numeric_limits::max()) return false; + e.start_angle = e.start_angle + uc * sweep; // drop [0, uc) + } else { + double uc = -std::numeric_limits::max(); + for (double u : cuts) if (u < u_pick - 1e-9 && u > uc) uc = u; + if (uc == -std::numeric_limits::max()) return false; + e.end_angle = e.start_angle + uc * sweep; // drop (uc, 1] + } + return true; + } + + // Circle subject: trimming opens it into an Arc that excludes the picked gap. + if (e.type == SketchEntity::Type::Circle) { + std::vector ang; + for (const auto& other : others) + for (double phi : circle_cross_angles(e.center, e.radius, other)) + ang.push_back(wrap_2pi(phi)); + std::sort(ang.begin(), ang.end()); + ang.erase(std::unique(ang.begin(), ang.end(), + [](double a, double b){ return std::abs(a - b) < 1e-7; }), + ang.end()); + if (ang.size() < 2) return false; + + double pk = wrap_2pi(std::atan2(pick.y() - e.center.y(), pick.x() - e.center.x())); + const int n = int(ang.size()); + int idx = -1; + for (int i = 0; i < n; ++i) { + double lo = ang[i]; + double hi = (i + 1 < n) ? ang[i + 1] : ang[0] + 2.0 * M_PI; + double p = (pk < lo - 1e-12) ? pk + 2.0 * M_PI : pk; + if (p >= lo - 1e-12 && p < hi + 1e-12) { idx = i; break; } + } + if (idx < 0) return false; + double lo = ang[idx]; + double hi = (idx + 1 < n) ? ang[idx + 1] : ang[0] + 2.0 * M_PI; + // Keep the complement of the (lo,hi) gap: sweep ccw from hi back round to lo. + e.type = SketchEntity::Type::Arc; + e.start_angle = hi; + e.end_angle = lo + 2.0 * M_PI; + return true; + } + + if (e.type != SketchEntity::Type::Line) return false; + + Vec2d adir = e.p1 - e.p0; + double La2 = adir.dot(adir); + if (La2 < 1e-18) return false; + + double t_pick = (pick - e.p0).dot(adir) / La2; + t_pick = std::max(0.0, std::min(1.0, t_pick)); + + std::vector cuts; + for (const auto& other : others) { + auto h = line_entity_hits(e.p0, adir, other); + for (double t : h) { + if (t > 1e-9 && t < 1.0 - 1e-9) + cuts.push_back(t); + } + } + if (cuts.empty()) return false; + + if (t_pick <= 0.5) { + double tc = std::numeric_limits::max(); + for (double t : cuts) { + if (t > t_pick + 1e-9 && t < tc) + tc = t; + } + if (tc == std::numeric_limits::max()) return false; + e.p0 = e.p0 + tc * adir; + } else { + double tc = -std::numeric_limits::max(); + for (double t : cuts) { + if (t < t_pick - 1e-9 && t > tc) + tc = t; + } + if (tc == -std::numeric_limits::max()) return false; + e.p1 = e.p0 + tc * adir; + } + + return true; +} + +bool SketchEngine::extend_entity(SketchEntity& e, const std::vector& others, + const Vec2d& pick) +{ + // Arc subject: grow the sweep toward the picked end up to the nearest crossing, + // capped at a full turn so the arc never self-overlaps. (A Circle is already + // closed — nothing to extend.) + if (e.type == SketchEntity::Type::Arc) { + double sweep = e.end_angle - e.start_angle; + double mag = std::abs(sweep); + if (mag < 1e-12) return false; + double sgn = (sweep >= 0.0) ? 1.0 : -1.0; + double room = 2.0 * M_PI - mag; // max extra sweep before a full turn + if (room <= 1e-9) return false; + + double phi_pick = std::atan2(pick.y() - e.center.y(), pick.x() - e.center.x()); + double up = wrap_2pi(sgn * (phi_pick - e.start_angle)) / mag; // pick fraction on arc + const bool extend_end = (up > 0.5); + + double best = std::numeric_limits::max(); + for (const auto& other : others) { + for (double phi : circle_cross_angles(e.center, e.radius, other)) { + double adv = extend_end ? wrap_2pi(sgn * (phi - e.end_angle)) + : wrap_2pi(sgn * (e.start_angle - phi)); + if (adv > 1e-9 && adv <= room + 1e-9 && adv < best) best = adv; + } + } + if (best == std::numeric_limits::max()) return false; + if (extend_end) e.end_angle += sgn * best; + else e.start_angle -= sgn * best; + return true; + } + + if (e.type != SketchEntity::Type::Line) return false; + + Vec2d adir = e.p1 - e.p0; + double La2 = adir.dot(adir); + if (La2 < 1e-18) return false; + + double t_pick = (pick - e.p0).dot(adir) / La2; + + std::vector hits; + for (const auto& other : others) { + auto h = line_entity_hits(e.p0, adir, other); + hits.insert(hits.end(), h.begin(), h.end()); + } + if (hits.empty()) return false; + + if (t_pick > 0.5) { + double tc = std::numeric_limits::max(); + for (double t : hits) { + if (t > 1.0 + 1e-9 && t < tc) + tc = t; + } + if (tc == std::numeric_limits::max()) return false; + e.p1 = e.p0 + tc * adir; + } else { + double tc = -std::numeric_limits::max(); + for (double t : hits) { + if (t < -1e-9 && t > tc) + tc = t; + } + if (tc == -std::numeric_limits::max()) return false; + e.p0 = e.p0 + tc * adir; + } + + return true; +} + +} // namespace Slic3r diff --git a/src/libslic3r/SketchEngine.hpp b/src/libslic3r/SketchEngine.hpp new file mode 100644 index 0000000000..ee82a8b792 --- /dev/null +++ b/src/libslic3r/SketchEngine.hpp @@ -0,0 +1,257 @@ +#ifndef slic3r_SketchEngine_hpp_ +#define slic3r_SketchEngine_hpp_ + +#include "TriangleMesh.hpp" +#include "libslic3r/Point.hpp" +#include "GeometryEngine.hpp" + +#include +#include +#include +#include +#include +#include + +namespace Slic3r { + +struct SketchSegment { + enum Type { Line, Arc, Circle, Rectangle, Polygon }; + Type type{Line}; + Vec2d p0{0,0}, p1{0,0}; + Vec2d center{0,0}; + double radius{0}, start_angle{0}, end_angle{0}; + std::vector points; + template + void serialize(Archive& ar) { ar(type, p0, p1, center, radius, start_angle, end_angle, points); } +}; + +struct SketchEntity { + enum class Type { Line, Arc, Circle, Point, Ellipse, EllipseArc, BSpline }; + Type type{Type::Line}; + Vec2d p0{0,0}; // Line: start; Arc/EllipseArc: start; Circle/Point/Ellipse: center; BSpline: first pole + Vec2d p1{0,0}; // Line: end; Arc/EllipseArc: end; (unused for Circle/Point/Ellipse); BSpline: last pole + Vec2d center{0,0}; // Arc/Circle/Ellipse(Arc) center + double radius{0}; // Circle/Arc radius; Ellipse(Arc): semi-major axis (a) + double start_angle{0}; // Arc sweep start; Ellipse(Arc): parametric start angle (radians) + double end_angle{0}; // Arc sweep end; Ellipse(Arc): parametric end angle + bool construction{false}; + double rminor{0}; // Ellipse(Arc): semi-minor axis (b) + double rotation{0}; // Ellipse(Arc): major-axis angle phi (radians, about center) + std::vector ctrl; // BSpline: control points (poles); p0/p1 mirror first/last pole + template + void serialize(Archive& ar) { + // Append-only: rminor/rotation added for Ellipse(Arc) (P2 Tier-B.1); ctrl for BSpline (B.2). + ar(type, p0, p1, center, radius, start_angle, end_angle, construction, rminor, rotation, ctrl); + } +}; + +struct SketchPlane { + Vec3d origin{0,0,0}; + Vec3d normal{0,0,1}; + Vec3d x_axis{1,0,0}; + Vec3d y_axis{0,1,0}; + + gp_Pln to_occt() const; + static SketchPlane from_face(const TopoDS_Face& face); + static SketchPlane XY() { return {}; } + static SketchPlane XZ() { return {{0,0,0}, {0,1,0}, {1,0,0}, {0,0,1}}; } + static SketchPlane YZ() { return {{0,0,0}, {1,0,0}, {0,1,0}, {0,0,1}}; } + + Vec2d project(const Vec3d& ray_origin, const Vec3d& ray_dir) const; + Vec3d to_world(const Vec2d& pt) const; + + template + void serialize(Archive& ar) { ar(origin, normal, x_axis, y_axis); } +}; + +struct SketchProfile { + std::vector points; + bool closed{false}; + + bool is_closed(double tolerance = 0.5) const; + bool try_close(double tolerance = 0.5); + void clear() { points.clear(); closed = false; } + TopoDS_Wire to_occt_wire(const SketchPlane& plane) const; + + template + void serialize(Archive& ar) { ar(points, closed); } +}; + +enum class SketchConstraintType { + Fix, Coincident, Horizontal, Vertical, Distance, + LockX, LockY, EqualLength, Parallel, Perpendicular, + Concentric, + Tangent, Midpoint, Symmetric, Angle, + Radius, Diameter, + PointOnLine, // a point lies on a line (or at signed perpendicular distance `value`) + PointOnObject // a point lies on an entity edge (line -> PT_ON_LINE, circle -> PT_ON_CIRCLE) +}; + +// Constraint on a SketchProfile, referencing profile point indices (a,b,c,d). +// `value` carries the target for Distance/LockX/LockY (ignored otherwise). +struct SketchConstraintDef { + SketchConstraintType type{SketchConstraintType::Coincident}; + int a{-1}, b{-1}, c{-1}, d{-1}; + double value{0.0}; + template void serialize(Archive& ar) { ar(type, a, b, c, d, value); } +}; + +// Which point of an entity a constraint reference names. +// P0 = SketchEntity::p0 (Line start / Point position) +// P1 = SketchEntity::p1 (Line end) +// Center = SketchEntity::center (Arc/Circle center) +enum class SketchPointRole { P0, P1, Center }; + +// Constraint on coexisting SketchEntity objects (Fase 4.2). Each reference is an +// (entity index, point role) pair. Point-form constraints +// (Fix/Coincident/Horizontal/Vertical/Distance/LockX/LockY) use refs A and B as +// individual points. Segment-form constraints (Parallel/Perpendicular/EqualLength) +// use entity indices `ea`/`eb` as whole line segments (their P0->P1); roles are +// ignored for those. `value` carries the target for Distance/LockX/LockY. +struct SketchEntityConstraintDef { + SketchConstraintType type{SketchConstraintType::Coincident}; + int ea{-1}, eb{-1}; // entity indices + SketchPointRole ra{SketchPointRole::P0}; // role within ea + SketchPointRole rb{SketchPointRole::P0}; // role within eb + double value{0.0}; + int ec{-1}; // third entity ref (Symmetric axis) + SketchPointRole rc{SketchPointRole::P0}; // role within ec + template void serialize(Archive& ar) { ar(type, ea, eb, ra, rb, value, ec, rc); } +}; + +// Solve a bare entity list in place against entity-form constraints. Shared by +// CadDocument::solve_sketch_feature (committed features) and the in-session GUI +// sketch tool (live solving as dimensions/constraints are added). Returns true on +// convergence; an empty constraint list is a no-op that returns true. +bool solve_sketch_entities(std::vector& entities, + const std::vector& constraints); + +struct SketchParams { + // Extrude/Revolve + double extrude_len{10}; bool extrude_sym{false}; double extrude_taper{0}; + double revolve_deg{360}; + bool is_pocket{false}; // cut into selected object instead of new + + // Dress-up + bool dressup_enabled{false}; + DressUpType dressup_type{DressUpType::Fillet}; + FaceGroup dressup_faces{FaceGroup::All}; + double dressup_radius{1.0}; + double dressup_chamfer_dist{1.0}; + + // Mesh + double linear_deflection{0.01}; + + template + void serialize(Archive& ar) { + ar(extrude_len, extrude_sym, extrude_taper, revolve_deg, is_pocket, + dressup_enabled, dressup_type, dressup_faces, dressup_radius, dressup_chamfer_dist, + linear_deflection); + } +}; + +class SketchEngine +{ +public: + static TopoDS_Shape make_extrude(const TopoDS_Wire& wire, const SketchPlane& plane, + double length, bool symmetric = false, double taper_deg = 0.0); + // Asymmetric two-sided prism: extrude the wire's face by `up` along +normal and `down` + // along -normal, fused into one solid. up/down are non-negative magnitudes. + // Tapered (draft) extrude of a planar wire: the top profile is the base wire offset in its + // plane by length*tan(taper_deg), lofted from base to top. Falls back to a straight prism on + // any failure (self-intersecting offset / loft error). taper_deg>0 widens the top. + static TopoDS_Shape make_extrude_taper(const TopoDS_Wire& wire, const SketchPlane& plane, + double length, double taper_deg); + static TopoDS_Shape make_extrude_two_sided(const TopoDS_Wire& wire, const SketchPlane& plane, + double up, double down); + static TopoDS_Shape make_extrude_face(const TopoDS_Face& face, const SketchPlane& plane, + double length, bool symmetric = false, double taper_deg = 0.0); + + // Extrude a set of imported rigid regions (Text/SVG). Each region is + // contour[0]=outer loop + contour[1..]=hole loops, in plane (u,v) mm. Builds + // one planar face-with-holes per region, extrudes it, and fuses all region + // solids into a single shape. Empty/degenerate contours are skipped. + static TopoDS_Shape make_extrude_regions( + const std::vector>>& regions, + const SketchPlane& plane, double length, bool symmetric = false); + + // Revolve a planar profile wire about an axis lying in the sketch plane and + // passing through the plane origin: axis_sel 0 = plane X axis, 1 = plane Y axis. + // A negative angle_deg sweeps the opposite direction (Flip). The profile must + // lie to one side of the axis (Onshape rule); a straddling profile self-intersects. + static TopoDS_Shape make_revolve(const TopoDS_Wire& wire, const SketchPlane& plane, + double angle_deg = 360.0, int axis_sel = 0); + + // Sweep a planar profile wire along a path (spine) wire. The profile is turned + // into a face and swept with BRepOffsetAPI_MakePipe, which keeps the profile + // perpendicular to the spine along its length. The path may be open or closed; + // for a clean solid the path's first point should sit on/near the profile plane. + static TopoDS_Shape make_sweep(const TopoDS_Wire& profile, const TopoDS_Wire& path); + + // Loft a solid through 2+ closed profile wires (each on its own plane), in the + // given order. ruled=true => straight (ruled) sections; false => smooth (C2). + static TopoDS_Shape make_loft(const std::vector& profiles, bool ruled); + + static TopoDS_Shape make_pocket(const TopoDS_Wire& wire, const SketchPlane& plane, + const TopoDS_Shape& target, double depth); + + static TriangleMesh tessellate(const TopoDS_Shape& shape, + double linear_deflection = 0.01, + double angular_deflection = 0.5); + + static TriangleMesh tessellate(const TopoDS_Shape& shape, + std::vector& tri_face, + double linear_deflection = 0.01, + double angular_deflection = 0.5); + + static TopoDS_Wire entities_to_wire(const std::vector& entities, + const SketchPlane& plane); + + static std::vector mirror_entities( + const std::vector& src, const Vec2d& a, const Vec2d& b); + + static std::vector offset_entities( + const std::vector& src, double d); + + // Rigid-transform array. Returns the (count-1) copies for instance i=1..count-1 + // (the originals in `src` are NOT included). Each copy i is `src` rigidly + // transformed by: rotate by i*angle_step about `pivot`, then translate by i*step. + // Rectangular/linear array: angle_step = 0, step = spacing*direction (pivot unused). + // Polar array: step = (0,0), angle_step = sweep/count, pivot = centre. + // Orientation-preserving, so arc/ellipse parametric angles shift by i*angle_step. + static std::vector array_entities( + const std::vector& src, int count, + const Vec2d& step, double angle_step, const Vec2d& pivot); + + // General affine transform (move / rotate / scale), applied IN PLACE: returns + // the SAME entities (same count and order), each mapped by + // p -> pivot + scale * R(angle) * (p - pivot) + move + // (radii scale by |scale|; arc/ellipse parametric/rotation angles shift by + // `angle`). Unlike array_entities this mutates the subjects rather than adding + // copies. Move: angle=0, scale=1. Rotate-in-place: move=(0,0), scale=1, + // pivot=centroid. Scale: angle=0. + static std::vector transform_entities( + const std::vector& src, + const Vec2d& move, double angle, double scale, const Vec2d& pivot); + + static bool fillet_lines(const SketchEntity& a, const SketchEntity& b, double r, + SketchEntity& a_out, SketchEntity& b_out, SketchEntity& arc_out); + + // Symmetric chamfer between two lines meeting at a corner: trims each line back + // by setback distance `d` from the shared corner and returns the connecting + // straight segment (seg_out) in place of the corner. a_out/b_out are the trimmed + // lines; seg_out goes seg_out.p0 (on a) -> seg_out.p1 (on b). False if the lines + // are parallel or `d` overruns either line. + static bool chamfer_lines(const SketchEntity& a, const SketchEntity& b, double d, + SketchEntity& a_out, SketchEntity& b_out, SketchEntity& seg_out); + + static bool trim_entity(SketchEntity& e, const std::vector& others, + const Vec2d& pick); + + static bool extend_entity(SketchEntity& e, const std::vector& others, + const Vec2d& pick); +}; + +} // namespace Slic3r + +#endif // slic3r_SketchEngine_hpp_ diff --git a/src/libslic3r/SketchImport.cpp b/src/libslic3r/SketchImport.cpp new file mode 100644 index 0000000000..0d6e2a93ad --- /dev/null +++ b/src/libslic3r/SketchImport.cpp @@ -0,0 +1,145 @@ +#include "SketchImport.hpp" + +#include "Emboss.hpp" +#include "NSVGUtils.hpp" +#include "ExPolygon.hpp" +#include "TextConfiguration.hpp" // FontProp +#include "libslic3r.h" // SCALING_FACTOR +#include "Utils.hpp" // resources_dir + +#include +#include + +namespace Slic3r { + +// Convert one ExPolygon (outer contour + CW holes) into an ImportRegion, +// mapping each integer Point to plane (u,v) mm via `to_mm`. +template +static ImportRegion expoly_to_region(const ExPolygon& ex, ToMm to_mm) +{ + auto contour_pts = [&](const Polygon& poly) { + std::vector c; + c.reserve(poly.points.size()); + for (const Point& p : poly.points) + c.push_back(to_mm(p)); + return c; + }; + ImportRegion region; + region.push_back(contour_pts(ex.contour)); + for (const Polygon& h : ex.holes) + region.push_back(contour_pts(h)); + return region; +} + +// Shift all regions so their common bounding-box centre sits on the origin +// (Onshape/typical CAD insert places imported art centred on the sketch). +static void center_regions(ImportRegions& regs) +{ + double lo_x = std::numeric_limits::max(); + double lo_y = std::numeric_limits::max(); + double hi_x = -std::numeric_limits::max(); + double hi_y = -std::numeric_limits::max(); + bool any = false; + for (const auto& region : regs) + for (const auto& contour : region) + for (const Vec2d& p : contour) { + lo_x = std::min(lo_x, p.x()); hi_x = std::max(hi_x, p.x()); + lo_y = std::min(lo_y, p.y()); hi_y = std::max(hi_y, p.y()); + any = true; + } + if (!any) return; + const Vec2d c(0.5 * (lo_x + hi_x), 0.5 * (lo_y + hi_y)); + for (auto& region : regs) + for (auto& contour : region) + for (Vec2d& p : contour) + p -= c; +} + +static std::string default_font_path() +{ + return resources_dir() + "/fonts/HarmonyOS_Sans_SC_Regular.ttf"; +} + +ImportRegions text_to_regions(const std::string& utf8, double size_mm, + const std::string& font_path) +{ + if (utf8.empty() || size_mm <= 0.0) + return {}; + + const std::string path = font_path.empty() ? default_font_path() : font_path; + std::unique_ptr ff = Emboss::create_font_file(path.c_str()); + if (!ff) + return {}; + Emboss::FontFileWithCache fwc(std::move(ff)); + if (!fwc.has_value()) + return {}; + + FontProp prop(static_cast(size_mm)); // per_glyph=false + HealedExPolygons healed = Emboss::text2shapes(fwc, utf8.c_str(), prop); + if (healed.expolygons.empty()) + return {}; + + // Shape points are integers scaled by 1/SHAPE_SCALE in font units; + // get_text_shape_scale collapses (size_in_mm / unit_per_em) * SHAPE_SCALE + // into a single mm-per-shape-unit factor. FreeType y is up already. + const double s = Emboss::get_text_shape_scale(prop, *fwc.font_file); + auto to_mm = [s](const Point& p) { return Vec2d(p.x() * s, p.y() * s); }; + + ImportRegions regs; + regs.reserve(healed.expolygons.size()); + for (const ExPolygon& ex : healed.expolygons) + regs.push_back(expoly_to_region(ex, to_mm)); + + center_regions(regs); + return regs; +} + +ImportRegions svg_to_regions(const std::string& svg_path, double scale) +{ + if (svg_path.empty() || scale <= 0.0) + return {}; + + NSVGimage_ptr image = nsvgParseFromFile(svg_path, "mm", 96.0f); + if (!image) + return {}; + + // A filled shape that also carries a stroke would import the stroke as a + // thick outline band wrapped around the fill (the reported "too large line + // width"). For CAD import the fill silhouette is what's wanted, so drop the + // stroke on any shape that has a fill; stroke-only line art is kept. + for (NSVGshape* s = image->shapes; s != nullptr; s = s->next) + if (s->fill.type != NSVG_PAINT_NONE) + s->stroke.type = NSVG_PAINT_NONE; + + // tesselation tolerance is in image (mm) scale; 0.3 mm keeps curves smooth + // without exploding the contour count. is_y_negative (default) flips SVG's + // y-down to the sketch's y-up. + NSVGLineParams param(0.3); + ExPolygonsWithIds ids = create_shape_with_ids(*image, param); + + // NSVG points are integers scaled by 1/SCALING_FACTOR (param.scale default): + // mm = point * SCALING_FACTOR, then the user scale factor. + const double s = SCALING_FACTOR * scale; + auto to_mm = [s](const Point& p) { return Vec2d(p.x() * s, p.y() * s); }; + + ImportRegions regs; + for (const ExPolygonsWithId& w : ids) + for (const ExPolygon& ex : w.expoly) + regs.push_back(expoly_to_region(ex, to_mm)); + + center_regions(regs); + return regs; +} + +ImportRegions transform_regions(const ImportRegions& src, const Vec2d& offset, + double scale_x, double scale_y) +{ + ImportRegions out = src; + for (auto& region : out) + for (auto& contour : region) + for (Vec2d& p : contour) + p = Vec2d(p.x() * scale_x + offset.x(), p.y() * scale_y + offset.y()); + return out; +} + +} // namespace Slic3r diff --git a/src/libslic3r/SketchImport.hpp b/src/libslic3r/SketchImport.hpp new file mode 100644 index 0000000000..7ac11b5a54 --- /dev/null +++ b/src/libslic3r/SketchImport.hpp @@ -0,0 +1,37 @@ +#ifndef slic3r_SketchImport_hpp_ +#define slic3r_SketchImport_hpp_ + +#include "Point.hpp" // Vec2d + +#include +#include + +namespace Slic3r { + +// A rigid imported region: contour[0] = outer loop, contour[1..] = holes; +// points in plane (u,v) millimetres. The nested vector type matches +// CadFeature::imported_regions exactly, so results assign directly. +using ImportRegion = std::vector>; +using ImportRegions = std::vector; + +// Vectorize UTF-8 text into filled regions (mm), centred on the origin. +// `size_mm` is the cap/line height. `font_path` empty -> a bundled default +// font (resources/fonts). Returns an empty vector on any failure. +ImportRegions text_to_regions(const std::string& utf8, double size_mm, + const std::string& font_path = std::string()); + +// Parse an SVG file's filled paths into regions (mm), centred on the origin. +// `scale` multiplies the authored size (1.0 = as authored). Returns an empty +// vector on any failure. +ImportRegions svg_to_regions(const std::string& svg_path, double scale = 1.0); + +// Apply an axis-aligned placement transform to regions: +// p -> ( p.x * scale_x + offset.x, p.y * scale_y + offset.y ) +// Used to move / enlarge / stretch imported art non-destructively (the +// feature keeps the centred source regions + this transform). +ImportRegions transform_regions(const ImportRegions& src, const Vec2d& offset, + double scale_x, double scale_y); + +} // namespace Slic3r + +#endif // slic3r_SketchImport_hpp_ diff --git a/src/libslic3r/SketchInference.cpp b/src/libslic3r/SketchInference.cpp new file mode 100644 index 0000000000..b2df50a3cb --- /dev/null +++ b/src/libslic3r/SketchInference.cpp @@ -0,0 +1,120 @@ +#include "SketchInference.hpp" + +#include + +namespace Slic3r { + +// Candidate target collected during the scan; we keep the closest within each +// priority tier and resolve ties by tier then distance. +namespace { +struct Cand { + InferenceSnap::Kind kind{InferenceSnap::Kind::None}; + int entity{-1}; + SketchPointRole role{SketchPointRole::P0}; + Vec2d point{0, 0}; + double dist{0.0}; +}; + +// Lower number = higher priority. +int tier(InferenceSnap::Kind k) +{ + switch (k) { + case InferenceSnap::Kind::Endpoint: return 0; + case InferenceSnap::Kind::Center: return 1; + case InferenceSnap::Kind::Origin: return 2; + case InferenceSnap::Kind::Midpoint: return 3; + case InferenceSnap::Kind::OnEdge: return 4; + default: return 9; + } +} +} // namespace + +InferenceSnap infer_point_snap(const std::vector& entities, + const Vec2d& query, double tol, + bool include_origin) +{ + Cand best; + best.kind = InferenceSnap::Kind::None; + best.point = query; + + auto offer = [&](InferenceSnap::Kind k, int ent, SketchPointRole r, const Vec2d& q) { + const double d = (q - query).norm(); + if (d > tol) return; + const bool better = (best.kind == InferenceSnap::Kind::None) || + (tier(k) < tier(best.kind)) || + (tier(k) == tier(best.kind) && d < best.dist); + if (better) { best.kind = k; best.entity = ent; best.role = r; best.point = q; best.dist = d; } + }; + + for (size_t i = 0; i < entities.size(); ++i) { + const SketchEntity& e = entities[i]; + const int ei = int(i); + switch (e.type) { + case SketchEntity::Type::Line: { + offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P0, e.p0); + offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P1, e.p1); + offer(InferenceSnap::Kind::Midpoint, ei, SketchPointRole::P0, 0.5 * (e.p0 + e.p1)); + // Projection onto the segment interior (PointOnObject candidate). + const Vec2d d = e.p1 - e.p0; + const double L2 = d.squaredNorm(); + if (L2 > 1e-12) { + double t = (query - e.p0).dot(d) / L2; + if (t > 0.02 && t < 0.98) + offer(InferenceSnap::Kind::OnEdge, ei, SketchPointRole::P0, e.p0 + t * d); + } + break; + } + case SketchEntity::Type::Arc: + offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P0, e.p0); + offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P1, e.p1); + offer(InferenceSnap::Kind::Center, ei, SketchPointRole::Center, e.center); + break; + case SketchEntity::Type::Circle: { + offer(InferenceSnap::Kind::Center, ei, SketchPointRole::Center, e.center); + // Nearest point on the circle rim (PointOnObject candidate). + const Vec2d v = query - e.center; + const double n = v.norm(); + if (n > 1e-9 && e.radius > 1e-9) + offer(InferenceSnap::Kind::OnEdge, ei, SketchPointRole::Center, + e.center + v * (e.radius / n)); + break; + } + case SketchEntity::Type::Point: + offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P0, e.p0); + break; + case SketchEntity::Type::EllipseArc: + offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P0, e.p0); + offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P1, e.p1); + offer(InferenceSnap::Kind::Center, ei, SketchPointRole::Center, e.center); + break; + case SketchEntity::Type::Ellipse: + offer(InferenceSnap::Kind::Center, ei, SketchPointRole::Center, e.center); + break; + case SketchEntity::Type::BSpline: + // Endpoints (first/last pole) snap for loop closure. + offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P0, e.p0); + offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P1, e.p1); + break; + } + } + + if (include_origin) + offer(InferenceSnap::Kind::Origin, -1, SketchPointRole::P0, Vec2d(0, 0)); + + InferenceSnap r; + r.kind = best.kind; r.entity = best.entity; r.role = best.role; r.point = best.point; + return r; +} + +std::optional +infer_axis_constraint(const Vec2d& anchor, const Vec2d& tip, double ang_tol_rad) +{ + const Vec2d d = tip - anchor; + if (d.squaredNorm() < 1e-12) return std::nullopt; + const double ang = std::atan2(std::abs(d.y()), std::abs(d.x())); // 0=horizontal, pi/2=vertical + if (ang <= ang_tol_rad) return SketchConstraintType::Horizontal; + if (ang >= M_PI / 2.0 - ang_tol_rad) return SketchConstraintType::Vertical; + return std::nullopt; +} + +} // namespace Slic3r diff --git a/src/libslic3r/SketchInference.hpp b/src/libslic3r/SketchInference.hpp new file mode 100644 index 0000000000..ddbe2087eb --- /dev/null +++ b/src/libslic3r/SketchInference.hpp @@ -0,0 +1,44 @@ +#ifndef slic3r_SketchInference_hpp_ +#define slic3r_SketchInference_hpp_ + +#include "SketchEngine.hpp" +#include +#include +#include + +namespace Slic3r { + +// Result of snapping a free cursor point onto the most relevant inference target +// among the committed sketch entities and the sketch origin. This is the backbone +// that lets geometry self-constrain as it is drawn: the GUI records the returned +// target at click time and, once the entity it belongs to exists, emits the +// matching constraint (Coincident onto an endpoint/centre, Fix onto the origin, +// PointOnObject onto an edge) so the relation survives a re-solve. +struct InferenceSnap { + enum class Kind { None, Endpoint, Center, Midpoint, OnEdge, Origin }; + Kind kind{Kind::None}; + int entity{-1}; // hit entity index (-1 = origin/none) + SketchPointRole role{SketchPointRole::P0}; // which point of `entity` (Endpoint/Center) + Vec2d point{0, 0}; // snapped coordinate (== query when None) + + bool snapped() const { return kind != Kind::None; } +}; + +// Snap `query` onto the best inference target within `tol` plane units. Priority, +// highest first: Endpoint, Center, Origin, Midpoint, OnEdge. Construction entities +// participate (you constrain to them too). Returns {None, query} when nothing is in +// range. Pure — no GUI / GL dependencies, so it is unit-testable in libslic3r. +InferenceSnap infer_point_snap(const std::vector& entities, + const Vec2d& query, double tol, + bool include_origin = true); + +// Relational inference for an in-progress segment anchor->tip. If its direction is +// within `ang_tol_rad` of an axis, returns Horizontal or Vertical (the constraint to +// auto-emit on the committed segment); std::nullopt otherwise. Degenerate (near-zero +// length) segments return nullopt. +std::optional +infer_axis_constraint(const Vec2d& anchor, const Vec2d& tip, double ang_tol_rad = 3.0 * M_PI / 180.0); + +} // namespace Slic3r + +#endif // slic3r_SketchInference_hpp_ diff --git a/src/libslic3r/SketchSolver.cpp b/src/libslic3r/SketchSolver.cpp new file mode 100644 index 0000000000..09666ea297 --- /dev/null +++ b/src/libslic3r/SketchSolver.cpp @@ -0,0 +1,361 @@ +#include "SketchSolver.hpp" + +#include + +#include +#include +#include + +namespace Slic3r { + +using CT = SketchConstraintType; +using Role = SketchPointRole; + +namespace { + +constexpr Slvs_hGroup G_FIXED = 1; // workplane / reference: held constant +constexpr Slvs_hGroup G_SK = 2; // sketch geometry: the group we solve + +// Per-entity slvs handles. p0/p1/center are point2d entity handles; prim is the +// line/arc/circle entity; rparam is the circle radius param. +struct Slots { + Slvs_hEntity prim{0}, p0{0}, p1{0}, center{0}; + Slvs_hParam rparam{0}; + std::vector pts; // BSpline control points (point2d handles) +}; + +struct Build { + std::vector params; + std::vector ents; + std::vector cons; + Slvs_hParam ph{0}; + Slvs_hEntity eh{0}; + Slvs_hConstraint ch{0}; + Slvs_hEntity wp{0}, normal{0}; + + Slvs_hParam P(Slvs_hGroup g, double v) { params.push_back(Slvs_MakeParam(++ph, g, v)); return ph; } + Slvs_hEntity E(Slvs_Entity e) { ents.push_back(e); return e.h; } + Slvs_hEntity pt2d(Slvs_hGroup g, double u, double v) + { return E(Slvs_MakePoint2d(++eh, g, wp, P(g, u), P(g, v))); } + + // Generic constraint (entityC unused by Slvs_MakeConstraint — set it manually below). + void C(int type, double val, Slvs_hEntity ptA, Slvs_hEntity ptB, + Slvs_hEntity eA, Slvs_hEntity eB, Slvs_hEntity eC = 0, int other = 0) + { + Slvs_Constraint c = Slvs_MakeConstraint(++ch, G_SK, type, wp, val, ptA, ptB, eA, eB); + c.entityC = eC; + c.other = other; + cons.push_back(c); + } +}; + +inline int role_idx(Role r) { return int(r); } + +} // namespace + +static SketchSolveResult solve_impl(std::vector& entities, + const std::vector& constraints, + int dragged_ei, Role dragged_role) +{ + SketchSolveResult out; + if (constraints.empty()) { out.ok = true; out.dof = -1; return out; } + + Build b; + + // ---- Fixed 2D XY workplane (origin at 0,0,0; identity normal) ------------------- + Slvs_hEntity origin = b.E(Slvs_MakePoint3d(++b.eh, G_FIXED, + b.P(G_FIXED, 0.0), b.P(G_FIXED, 0.0), b.P(G_FIXED, 0.0))); + double qw, qx, qy, qz; + Slvs_MakeQuaternion(1, 0, 0, 0, 1, 0, &qw, &qx, &qy, &qz); + b.normal = b.E(Slvs_MakeNormal3d(++b.eh, G_FIXED, + b.P(G_FIXED, qw), b.P(G_FIXED, qx), b.P(G_FIXED, qy), b.P(G_FIXED, qz))); + b.wp = b.E(Slvs_MakeWorkplane(++b.eh, G_FIXED, origin, b.normal)); + + // ---- Entities ------------------------------------------------------------------- + std::vector slot(entities.size()); + for (size_t i = 0; i < entities.size(); ++i) { + const SketchEntity& e = entities[i]; + Slots s; + switch (e.type) { + case SketchEntity::Type::Line: + s.p0 = b.pt2d(G_SK, e.p0.x(), e.p0.y()); + s.p1 = b.pt2d(G_SK, e.p1.x(), e.p1.y()); + s.prim = b.E(Slvs_MakeLineSegment(++b.eh, G_SK, b.wp, s.p0, s.p1)); + break; + case SketchEntity::Type::Point: + s.p0 = b.pt2d(G_SK, e.p0.x(), e.p0.y()); + break; + case SketchEntity::Type::Circle: { + s.center = b.pt2d(G_SK, e.center.x(), e.center.y()); + s.p0 = s.center; // p0 mirrors centre for circles + s.rparam = b.P(G_SK, e.radius > 1e-9 ? e.radius : 1.0); + Slvs_hEntity dist = b.E(Slvs_MakeDistance(++b.eh, G_SK, b.wp, s.rparam)); + s.prim = b.E(Slvs_MakeCircle(++b.eh, G_SK, b.wp, s.center, b.normal, dist)); + break; + } + case SketchEntity::Type::Arc: + s.center = b.pt2d(G_SK, e.center.x(), e.center.y()); + s.p0 = b.pt2d(G_SK, e.p0.x(), e.p0.y()); // start + s.p1 = b.pt2d(G_SK, e.p1.x(), e.p1.y()); // end + s.prim = b.E(Slvs_MakeArcOfCircle(++b.eh, G_SK, b.wp, b.normal, s.center, s.p0, s.p1)); + break; + // libslvs has no conic entity (scope note): register the ellipse's defining + // points only (center + arc endpoints) so center/endpoint constraints solve; + // the a/b/phi shape params pass through unsolved. + case SketchEntity::Type::Ellipse: + s.center = b.pt2d(G_SK, e.center.x(), e.center.y()); + s.p0 = s.center; // p0 mirrors centre (circle convention) + break; + case SketchEntity::Type::EllipseArc: + s.center = b.pt2d(G_SK, e.center.x(), e.center.y()); + s.p0 = b.pt2d(G_SK, e.p0.x(), e.p0.y()); // start + s.p1 = b.pt2d(G_SK, e.p1.x(), e.p1.y()); // end + break; + // No native slvs curve for an arbitrary-degree spline: register the control + // poles as point2d so endpoints (and any pole-targeted constraint) solve. The + // OCCT curve is rebuilt from the solved poles. p0/p1 mirror first/last pole so + // Coincident at the spline ends closes loops just like a Line. + case SketchEntity::Type::BSpline: + s.pts.reserve(e.ctrl.size()); + for (const Vec2d& cp : e.ctrl) + s.pts.push_back(b.pt2d(G_SK, cp.x(), cp.y())); + if (!s.pts.empty()) { s.p0 = s.pts.front(); s.p1 = s.pts.back(); } + break; + } + slot[i] = s; + } + + auto valid = [&](int ei) { return ei >= 0 && ei < int(entities.size()); }; + auto ptOf = [&](int ei, Role r) -> Slvs_hEntity { + if (!valid(ei)) return 0; + const Slots& s = slot[ei]; + switch (r) { + case Role::P0: return s.p0; + case Role::P1: return s.p1; + case Role::Center: return s.center ? s.center : s.p0; + } + return 0; + }; + auto primOf = [&](int ei) -> Slvs_hEntity { return valid(ei) ? slot[ei].prim : 0; }; + auto coordOf = [&](int ei, Role r) -> Vec2d { + if (!valid(ei)) return Vec2d(0, 0); + const SketchEntity& e = entities[ei]; + switch (r) { case Role::P0: return e.p0; case Role::P1: return e.p1; case Role::Center: return e.center; } + return e.p0; + }; + // A fixed reference point at (x,y) — used to pin coordinates (Fix / LockX / LockY). + auto fixedRef = [&](double x, double y) -> Slvs_hEntity { return b.pt2d(G_FIXED, x, y); }; + + // ---- Constraints ---------------------------------------------------------------- + for (const auto& c : constraints) { + // Robustness: never feed libslvs a null handle. A constraint that references an + // entity which produced no solver primitive (Point/Ellipse/EllipseArc/BSpline get + // no `prim`) or no point for the requested role would make Slvs FindById abort the + // whole process. Skip such a constraint instead of crashing. + bool ref_ok = true; + switch (c.type) { + case CT::Coincident: case CT::Horizontal: case CT::Vertical: case CT::Distance: + ref_ok = ptOf(c.ea, c.ra) && ptOf(c.eb, c.rb); break; + case CT::Concentric: + ref_ok = ptOf(c.ea, Role::Center) && ptOf(c.eb, Role::Center); break; + case CT::Fix: case CT::LockX: case CT::LockY: + ref_ok = ptOf(c.ea, c.ra) != 0; break; + case CT::EqualLength: case CT::Parallel: case CT::Perpendicular: + case CT::Angle: case CT::Tangent: + ref_ok = primOf(c.ea) && primOf(c.eb); break; + case CT::Radius: case CT::Diameter: + ref_ok = primOf(c.ea) != 0; break; + case CT::Midpoint: + ref_ok = ptOf(c.ea, c.ra) && primOf(c.eb); break; + case CT::Symmetric: + ref_ok = ptOf(c.ea, c.ra) && ptOf(c.eb, c.rb) && primOf(c.ec); break; + case CT::PointOnLine: case CT::PointOnObject: + ref_ok = ptOf(c.ea, c.ra) && primOf(c.eb); break; + } + if (!ref_ok) continue; + switch (c.type) { + case CT::Coincident: + b.C(SLVS_C_POINTS_COINCIDENT, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), 0, 0); + break; + case CT::Concentric: + b.C(SLVS_C_POINTS_COINCIDENT, 0, ptOf(c.ea, Role::Center), ptOf(c.eb, Role::Center), 0, 0); + break; + case CT::Horizontal: + b.C(SLVS_C_HORIZONTAL, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), 0, 0); + break; + case CT::Vertical: + b.C(SLVS_C_VERTICAL, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), 0, 0); + break; + case CT::Distance: + b.C(SLVS_C_PT_PT_DISTANCE, c.value, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), 0, 0); + break; + case CT::Fix: { + const Vec2d p = coordOf(c.ea, c.ra); + b.C(SLVS_C_POINTS_COINCIDENT, 0, ptOf(c.ea, c.ra), fixedRef(p.x(), p.y()), 0, 0); + break; + } + case CT::LockX: { + const Vec2d p = coordOf(c.ea, c.ra); + b.C(SLVS_C_VERTICAL, 0, ptOf(c.ea, c.ra), fixedRef(c.value, p.y()), 0, 0); + break; + } + case CT::LockY: { + const Vec2d p = coordOf(c.ea, c.ra); + b.C(SLVS_C_HORIZONTAL, 0, ptOf(c.ea, c.ra), fixedRef(p.x(), c.value), 0, 0); + break; + } + case CT::EqualLength: + b.C(SLVS_C_EQUAL_LENGTH_LINES, 0, 0, 0, primOf(c.ea), primOf(c.eb)); + break; + case CT::Parallel: + b.C(SLVS_C_PARALLEL, 0, 0, 0, primOf(c.ea), primOf(c.eb)); + break; + case CT::Perpendicular: + b.C(SLVS_C_PERPENDICULAR, 0, 0, 0, primOf(c.ea), primOf(c.eb)); + break; + case CT::Midpoint: + b.C(SLVS_C_AT_MIDPOINT, 0, ptOf(c.ea, c.ra), 0, primOf(c.eb), 0); + break; + case CT::Symmetric: + // ptA, ptB symmetric about the axis line (ec). + b.C(SLVS_C_SYMMETRIC_LINE, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), primOf(c.ec), 0); + break; + case CT::Angle: + // model stores radians; slvs angle is in degrees. + b.C(SLVS_C_ANGLE, c.value * 180.0 / M_PI, 0, 0, primOf(c.ea), primOf(c.eb)); + break; + case CT::Radius: + b.C(SLVS_C_DIAMETER, 2.0 * c.value, 0, 0, primOf(c.ea), 0); + break; + case CT::Diameter: + b.C(SLVS_C_DIAMETER, c.value, 0, 0, primOf(c.ea), 0); + break; + case CT::Tangent: { + const bool aCurve = valid(c.ea) && entities[c.ea].type != SketchEntity::Type::Line; + const bool bCurve = valid(c.eb) && entities[c.eb].type != SketchEntity::Type::Line; + if (aCurve && bCurve) + b.C(SLVS_C_CURVE_CURVE_TANGENT, 0, 0, 0, primOf(c.ea), primOf(c.eb)); + else { + const Slvs_hEntity arc = aCurve ? primOf(c.ea) : primOf(c.eb); + const Slvs_hEntity line = aCurve ? primOf(c.eb) : primOf(c.ea); + b.C(SLVS_C_ARC_LINE_TANGENT, 0, 0, 0, arc, line); + } + break; + } + case CT::PointOnLine: + if (std::abs(c.value) < 1e-9) + b.C(SLVS_C_PT_ON_LINE, 0, ptOf(c.ea, c.ra), 0, primOf(c.eb), 0); + else + b.C(SLVS_C_PT_LINE_DISTANCE, std::abs(c.value), ptOf(c.ea, c.ra), 0, primOf(c.eb), 0); + break; + case CT::PointOnObject: + // Point (ea,ra) lies on entity edge eb: a circle rim -> PT_ON_CIRCLE, + // otherwise the segment line -> PT_ON_LINE. + if (valid(c.eb) && entities[c.eb].type == SketchEntity::Type::Circle) + b.C(SLVS_C_PT_ON_CIRCLE, 0, ptOf(c.ea, c.ra), 0, primOf(c.eb), 0); + else + b.C(SLVS_C_PT_ON_LINE, 0, ptOf(c.ea, c.ra), 0, primOf(c.eb), 0); + break; + } + } + + // ---- Solve ---------------------------------------------------------------------- + Slvs_System sys; + std::memset(&sys, 0, sizeof(sys)); + sys.param = b.params.data(); sys.params = int(b.params.size()); + sys.entity = b.ents.data(); sys.entities = int(b.ents.size()); + sys.constraint = b.cons.data(); sys.constraints = int(b.cons.size()); + std::vector failed(b.cons.size() + 1, 0); + sys.failed = failed.data(); + sys.faileds = int(failed.size()); + sys.calculateFaileds = 1; + + // Drag pin: feed the dragged point's two params into sys.dragged[] so the solver + // favours keeping that point at the cursor and re-solves the rest around it. + if (dragged_ei >= 0) { + const Slvs_hEntity h = ptOf(dragged_ei, dragged_role); + for (const Slvs_Entity& en : b.ents) + if (en.h == h) { sys.dragged[0] = en.param[0]; sys.dragged[1] = en.param[1]; break; } + } + + Slvs_Solve(&sys, G_SK); + + out.result = sys.result; + out.dof = sys.dof; + out.ok = (sys.result == SLVS_RESULT_OKAY); + + // Map solved param handles -> values, then read points back. + std::unordered_map pv; + pv.reserve(sys.params * 2); + for (int i = 0; i < sys.params; ++i) pv[sys.param[i].h] = sys.param[i].val; + std::unordered_map byH; + byH.reserve(sys.entities * 2); + for (int i = 0; i < sys.entities; ++i) byH[sys.entity[i].h] = &sys.entity[i]; + auto coord = [&](Slvs_hEntity h) -> Vec2d { + auto it = byH.find(h); + if (it == byH.end()) return Vec2d(0, 0); + return Vec2d(pv[it->second->param[0]], pv[it->second->param[1]]); + }; + + // Map failed constraint handles back to indices into `constraints`. + if (!out.ok && sys.faileds > 0) { + std::unordered_map chToIdx; + // constraint handles were assigned in order starting after the fixed group; the + // i-th sketch constraint in b.cons has handle = its position. Rebuild by scanning. + for (size_t k = 0; k < b.cons.size(); ++k) chToIdx[b.cons[k].h] = int(k); + for (int i = 0; i < sys.faileds; ++i) { + auto it = chToIdx.find(failed[i]); + if (it != chToIdx.end() && it->second < int(constraints.size())) + out.bad.push_back(it->second); + } + } + + // ---- Read solved geometry back -------------------------------------------------- + for (size_t i = 0; i < entities.size(); ++i) { + SketchEntity& e = entities[i]; + const Slots& s = slot[i]; + if (s.p0) e.p0 = coord(s.p0); + if (s.p1) e.p1 = coord(s.p1); + if (s.center) e.center = coord(s.center); + + if (e.type == SketchEntity::Type::BSpline) { + for (size_t k = 0; k < s.pts.size() && k < e.ctrl.size(); ++k) + e.ctrl[k] = coord(s.pts[k]); + if (!e.ctrl.empty()) { e.p0 = e.ctrl.front(); e.p1 = e.ctrl.back(); } + } else if (e.type == SketchEntity::Type::Circle) { + if (s.rparam) { auto it = pv.find(s.rparam); if (it != pv.end()) e.radius = it->second; } + e.p0 = e.center; + } else if (e.type == SketchEntity::Type::Arc && s.center) { + // Reflow arc angles from solved centre + endpoints, preserving sweep sign. + const double old_sweep = e.end_angle - e.start_angle; + const double ns = std::atan2(e.p0.y() - e.center.y(), e.p0.x() - e.center.x()); + const double ne = std::atan2(e.p1.y() - e.center.y(), e.p1.x() - e.center.x()); + double sweep = ne - ns; + const double TWO_PI = 2.0 * M_PI; + while (sweep <= -TWO_PI) sweep += TWO_PI; + while (sweep >= TWO_PI) sweep -= TWO_PI; + if (old_sweep >= 0.0 && sweep < 0.0) sweep += TWO_PI; + if (old_sweep < 0.0 && sweep > 0.0) sweep -= TWO_PI; + e.start_angle = ns; + e.end_angle = ns + sweep; + e.radius = 0.5 * ((e.p0 - e.center).norm() + (e.p1 - e.center).norm()); + } + } + + return out; +} + +SketchSolveResult sketch_solve(std::vector& entities, + const std::vector& constraints) +{ + return solve_impl(entities, constraints, -1, Role::P0); +} + +SketchSolveResult sketch_solve_drag(std::vector& entities, + const std::vector& constraints, + int dragged_ei, SketchPointRole dragged_role) +{ + return solve_impl(entities, constraints, dragged_ei, dragged_role); +} + +} // namespace Slic3r diff --git a/src/libslic3r/SketchSolver.hpp b/src/libslic3r/SketchSolver.hpp new file mode 100644 index 0000000000..edf9fe8690 --- /dev/null +++ b/src/libslic3r/SketchSolver.hpp @@ -0,0 +1,37 @@ +#ifndef slic3r_SketchSolver_hpp_ +#define slic3r_SketchSolver_hpp_ + +// Bridge from SnapOrca's SketchEntity / SketchEntityConstraintDef model onto the +// vendored SolveSpace constraint solver (src/libslic3r/slvs, libslvs). Replaces the +// hand-rolled SketchConstraints: full constraint set, real DoF counting, and +// over-constrained (bad-constraint) detection. Solves on a fixed 2D XY workplane. + +#include "SketchEngine.hpp" +#include + +namespace Slic3r { + +struct SketchSolveResult { + bool ok{false}; // solver converged & consistent + int dof{-1}; // remaining degrees of freedom (>0 under-constrained) + int result{0}; // raw SLVS_RESULT_* code + std::vector bad; // indices (into `constraints`) of conflicting constraints +}; + +// Solve `constraints` over `entities` in place (writes solved coordinates back into the +// entities; arc angles are reflowed preserving sweep direction). No-op success when +// `constraints` is empty. +SketchSolveResult sketch_solve(std::vector& entities, + const std::vector& constraints); + +// Drag-aware solve: pins the (dragged_ei, dragged_role) point's parameters via the +// solver's `dragged[]` priority list so the solver keeps that point where the cursor +// placed it (caller must have moved it first) and moves the OTHER free geometry to +// re-satisfy the constraints. dragged_ei < 0 behaves identically to sketch_solve. +SketchSolveResult sketch_solve_drag(std::vector& entities, + const std::vector& constraints, + int dragged_ei, SketchPointRole dragged_role); + +} // namespace Slic3r + +#endif diff --git a/src/libslic3r/ThreadStandards.cpp b/src/libslic3r/ThreadStandards.cpp new file mode 100644 index 0000000000..d2909c5275 --- /dev/null +++ b/src/libslic3r/ThreadStandards.cpp @@ -0,0 +1,95 @@ +#include "ThreadStandards.hpp" + +namespace Slic3r { + +// Imperial helpers: convert nominal inch diameter / threads-per-inch to mm. +static constexpr double IN = 25.4; +static inline double tpi_pitch(double tpi) { return IN / tpi; } + +const std::vector& thread_standards() +{ + using S = ThreadSpec::Series; + static const std::vector table = { + // --- ISO metric, coarse pitch (ISO 261 preferred series) --- + {"M1", 1.0, 0.25, S::MetricCoarse}, + {"M1.2", 1.2, 0.25, S::MetricCoarse}, + {"M1.6", 1.6, 0.35, S::MetricCoarse}, + {"M2", 2.0, 0.40, S::MetricCoarse}, + {"M2.5", 2.5, 0.45, S::MetricCoarse}, + {"M3", 3.0, 0.50, S::MetricCoarse}, + {"M4", 4.0, 0.70, S::MetricCoarse}, + {"M5", 5.0, 0.80, S::MetricCoarse}, + {"M6", 6.0, 1.00, S::MetricCoarse}, + {"M8", 8.0, 1.25, S::MetricCoarse}, + {"M10", 10.0, 1.50, S::MetricCoarse}, + {"M12", 12.0, 1.75, S::MetricCoarse}, + {"M14", 14.0, 2.00, S::MetricCoarse}, + {"M16", 16.0, 2.00, S::MetricCoarse}, + {"M20", 20.0, 2.50, S::MetricCoarse}, + {"M24", 24.0, 3.00, S::MetricCoarse}, + {"M30", 30.0, 3.50, S::MetricCoarse}, + {"M36", 36.0, 4.00, S::MetricCoarse}, + {"M42", 42.0, 4.50, S::MetricCoarse}, + {"M48", 48.0, 5.00, S::MetricCoarse}, + {"M56", 56.0, 5.50, S::MetricCoarse}, + {"M64", 64.0, 6.00, S::MetricCoarse}, + + // --- ISO metric, common fine pitches (ISO 261 fine series) --- + {"M8x1", 8.0, 1.00, S::MetricFine}, + {"M10x1.25", 10.0, 1.25, S::MetricFine}, + {"M10x1", 10.0, 1.00, S::MetricFine}, + {"M12x1.5", 12.0, 1.50, S::MetricFine}, + {"M12x1.25", 12.0, 1.25, S::MetricFine}, + {"M16x1.5", 16.0, 1.50, S::MetricFine}, + {"M20x1.5", 20.0, 1.50, S::MetricFine}, + {"M24x2", 24.0, 2.00, S::MetricFine}, + + // --- Unified National Coarse (UTS / ASME B1.1) --- + {"#1-64 UNC", 0.073 * IN, tpi_pitch(64), S::UNC}, + {"#2-56 UNC", 0.086 * IN, tpi_pitch(56), S::UNC}, + {"#3-48 UNC", 0.099 * IN, tpi_pitch(48), S::UNC}, + {"#4-40 UNC", 0.112 * IN, tpi_pitch(40), S::UNC}, + {"#5-40 UNC", 0.125 * IN, tpi_pitch(40), S::UNC}, + {"#6-32 UNC", 0.138 * IN, tpi_pitch(32), S::UNC}, + {"#8-32 UNC", 0.164 * IN, tpi_pitch(32), S::UNC}, + {"#10-24 UNC", 0.190 * IN, tpi_pitch(24), S::UNC}, + {"#12-24 UNC", 0.216 * IN, tpi_pitch(24), S::UNC}, + {"1/4-20 UNC", 0.250 * IN, tpi_pitch(20), S::UNC}, + {"5/16-18 UNC", 0.3125 * IN, tpi_pitch(18), S::UNC}, + {"3/8-16 UNC", 0.375 * IN, tpi_pitch(16), S::UNC}, + {"7/16-14 UNC", 0.4375 * IN, tpi_pitch(14), S::UNC}, + {"1/2-13 UNC", 0.500 * IN, tpi_pitch(13), S::UNC}, + {"9/16-12 UNC", 0.5625 * IN, tpi_pitch(12), S::UNC}, + {"5/8-11 UNC", 0.625 * IN, tpi_pitch(11), S::UNC}, + {"3/4-10 UNC", 0.750 * IN, tpi_pitch(10), S::UNC}, + {"7/8-9 UNC", 0.875 * IN, tpi_pitch(9), S::UNC}, + {"1-8 UNC", 1.000 * IN, tpi_pitch(8), S::UNC}, + + // --- Unified National Fine (UTS / ASME B1.1) --- + {"#2-64 UNF", 0.086 * IN, tpi_pitch(64), S::UNF}, + {"#4-48 UNF", 0.112 * IN, tpi_pitch(48), S::UNF}, + {"#6-40 UNF", 0.138 * IN, tpi_pitch(40), S::UNF}, + {"#8-36 UNF", 0.164 * IN, tpi_pitch(36), S::UNF}, + {"#10-32 UNF", 0.190 * IN, tpi_pitch(32), S::UNF}, + {"1/4-28 UNF", 0.250 * IN, tpi_pitch(28), S::UNF}, + {"5/16-24 UNF", 0.3125 * IN, tpi_pitch(24), S::UNF}, + {"3/8-24 UNF", 0.375 * IN, tpi_pitch(24), S::UNF}, + {"7/16-20 UNF", 0.4375 * IN, tpi_pitch(20), S::UNF}, + {"1/2-20 UNF", 0.500 * IN, tpi_pitch(20), S::UNF}, + {"9/16-18 UNF", 0.5625 * IN, tpi_pitch(18), S::UNF}, + {"5/8-18 UNF", 0.625 * IN, tpi_pitch(18), S::UNF}, + {"3/4-16 UNF", 0.750 * IN, tpi_pitch(16), S::UNF}, + {"1-12 UNF", 1.000 * IN, tpi_pitch(12), S::UNF}, + }; + return table; +} + +const ThreadSpec* find_thread_standard(const std::string& name) +{ + for (const ThreadSpec& s : thread_standards()) + if (s.name == name) + return &s; + return nullptr; +} + +} // namespace Slic3r diff --git a/src/libslic3r/ThreadStandards.hpp b/src/libslic3r/ThreadStandards.hpp new file mode 100644 index 0000000000..e9bfcc314f --- /dev/null +++ b/src/libslic3r/ThreadStandards.hpp @@ -0,0 +1,39 @@ +#ifndef slic3r_ThreadStandards_hpp_ +#define slic3r_ThreadStandards_hpp_ + +#include +#include + +namespace Slic3r { + +// Canonical mechanical thread specifications (ISO metric + Unified imperial). +// All dimensions are stored in millimetres so the CAD kernel can consume them +// directly. The profile is the common 60deg V shared by ISO 261/965 and ASME +// B1.1 (UTS), so the cut/ridge depth used by the Design-tab Thread tool is the +// basic external thread height h = 0.6134 * pitch, and the internal (tapped) +// minor diameter is D1 = D - 1.0825 * pitch (= D - 2*5H/8). +struct ThreadSpec { + enum class Series { MetricCoarse, MetricFine, UNC, UNF }; + + std::string name; // designation, e.g. "M6", "1/4-20 UNC" + double major_diameter_mm; // nominal (crest) diameter + double pitch_mm; // axial advance per turn + Series series; + + // 60deg basic external thread height (radial crest-to-root engagement). + double thread_depth_mm() const { return 0.6134 * pitch_mm; } + // Internal/tapped minor (tap-drill) diameter for the same nominal thread. + double minor_diameter_mm() const { return major_diameter_mm - 1.0825 * pitch_mm; } + + bool imperial() const { return series == Series::UNC || series == Series::UNF; } +}; + +// Full ordered table (metric coarse, metric fine, UNC, UNF) for GUI listing. +const std::vector& thread_standards(); + +// Exact case-sensitive designation lookup; nullptr if not a known standard. +const ThreadSpec* find_thread_standard(const std::string& name); + +} // namespace Slic3r + +#endif diff --git a/src/libslic3r/slvs/CMakeLists.txt b/src/libslic3r/slvs/CMakeLists.txt new file mode 100644 index 0000000000..7fb7c95969 --- /dev/null +++ b/src/libslic3r/slvs/CMakeLists.txt @@ -0,0 +1,30 @@ +# Vendored SolveSpace geometric constraint solver (libslvs), isolated solver core. +# Source: github.com/JacobStoren/SolveSpaceLib (extraction of solvespace.com libslvs). +# GPLv3 — compatible with this AGPL fork. Built as a self-contained STATIC lib; only +# include/slvs.h (+ SolveSpaceSystem.h C++ wrapper) is exposed to libslic3r. +# No external deps: it ships its own dense linear solver (no Eigen). + +add_library(libslvs STATIC + constrainteq.cpp + entity.cpp + expr.cpp + system.cpp + util.cpp + platform/unixutil.cpp + lib.cpp + SolveSpaceSystem.cpp) + +target_compile_definitions(libslvs PRIVATE -DLIBRARY) + +# Public API headers (include/slvs.h, include/SolveSpaceSystem.h) propagate to consumers; +# the internal solvespace headers stay PRIVATE so they never leak into libslic3r TUs. +target_include_directories(libslvs + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + +# libslic3r is linked into shared targets → PIC required. Silence warnings for this +# vendored code (it predates the project's -Wall/-Werror=return-type cleanliness). +set_target_properties(libslvs PROPERTIES POSITION_INDEPENDENT_CODE ON) +if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + target_compile_options(libslvs PRIVATE -w -fno-strict-aliasing) +endif() diff --git a/src/libslic3r/slvs/LICENSE b/src/libslic3r/slvs/LICENSE new file mode 100644 index 0000000000..a737dcfed5 --- /dev/null +++ b/src/libslic3r/slvs/LICENSE @@ -0,0 +1,675 @@ + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/src/libslic3r/slvs/SolveSpaceSystem.cpp b/src/libslic3r/slvs/SolveSpaceSystem.cpp new file mode 100644 index 0000000000..ad7046534b --- /dev/null +++ b/src/libslic3r/slvs/SolveSpaceSystem.cpp @@ -0,0 +1,177 @@ + +#define EXPORT_DLL +#include "SolveSpaceSystem.h" +#include + + + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +SolveSpaceSystem::SolveSpaceSystem() + : m_paramsMemory (new std::vector ()) + , m_entityMemory (new std::vector ()) + , m_constraintMemory (new std::vector ()) + , m_failedConstrMemory(new std::vector()) + +{ + m_paramsMemory ->reserve(100); + m_entityMemory ->reserve(100); + m_constraintMemory->reserve(100); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +Slvs_hParam SolveSpaceSystem::addParam(Slvs_Param parameter) +{ + + parameter.h = static_cast(m_paramsMemory->size()+1); + m_paramsMemory->push_back(parameter); + + m_slvsSystem.param = m_paramsMemory->data(); + m_slvsSystem.params = static_cast(m_paramsMemory->size()); + + return parameter.h; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +Slvs_hEntity SolveSpaceSystem::addEntity(Slvs_Entity entity) +{ + entity.h = static_cast(m_entityMemory->size()+1); + m_entityMemory->push_back(entity); + + m_slvsSystem.entity = m_entityMemory->data(); + m_slvsSystem.entities = static_cast(m_entityMemory->size()); + + return entity.h; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +Slvs_hConstraint SolveSpaceSystem::addConstr(Slvs_Constraint constr) +{ + constr.h = static_cast(m_constraintMemory->size()+1); + m_constraintMemory->push_back(constr); + + m_slvsSystem.constraint = m_constraintMemory->data(); + m_slvsSystem.constraints = static_cast(m_constraintMemory->size()); + + return constr.h; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +SolveSpaceSystem::ResultStatus SolveSpaceSystem::solve(Slvs_hGroup groupId, bool reportFailedConstraints /*= true*/) +{ + m_failedConstrMemory->resize(m_constraintMemory->size()); + + m_slvsSystem.failed = m_failedConstrMemory->data(); + m_slvsSystem.faileds = static_cast(m_failedConstrMemory->size()); + + m_slvsSystem.calculateFaileds = reportFailedConstraints; + + Slvs_Solve(&m_slvsSystem, groupId); + + m_failedConstrMemory->resize(m_slvsSystem.faileds); + + return static_cast(m_slvsSystem.result); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +double SolveSpaceSystem::parameterValue(Slvs_hParam paramId) +{ + return (*m_paramsMemory)[paramId-1].val; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void SolveSpaceSystem::setParameterValue(Slvs_hParam paramId, double value) +{ + (*m_paramsMemory)[paramId-1].val = value; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::tuple< std::valarray, + std::valarray, + std::valarray > SolveSpaceSystem::orientationMx(Slvs_hEntity normalIn3dEntityId) +{ + Slvs_Entity e_CS = (*m_entityMemory)[normalIn3dEntityId -1]; + if ( e_CS.type == SLVS_E_NORMAL_IN_3D ) + { + std::valarray quat ={ 0.0, 0.0, 0.0, 0.0 }; + quat[0] = parameterValue(e_CS.param[0]); + quat[1] = parameterValue(e_CS.param[1]); + quat[2] = parameterValue(e_CS.param[2]); + quat[3] = parameterValue(e_CS.param[3]); + std::valarray Ex ={ 0.0,0.0,0.0 }; + std::valarray Ey ={ 0.0,0.0,0.0 }; + std::valarray Ez ={ 0.0,0.0,0.0 }; + + Slvs_QuaternionU(quat[0], quat[1], quat[2], quat[3], + &Ex[0], &Ex[1], &Ex[2]); + Slvs_QuaternionV(quat[0], quat[1], quat[2], quat[3], + &Ey[0], &Ey[1], &Ey[2]); + Slvs_QuaternionN(quat[0], quat[1], quat[2], quat[3], + &Ez[0], &Ez[1], &Ez[2]); + + return std::make_tuple(Ex, Ey, Ez); + } + assert(false); + return std::make_tuple(std::valarray(), std::valarray(), std::valarray()); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::valarray SolveSpaceSystem::global3DPos(Slvs_hEntity pointEntityId) +{ + std::valarray point ={ 0.0,0.0,0.0 }; + + Slvs_Entity pointEntity = (*m_entityMemory)[pointEntityId -1]; + if ( pointEntity.type == SLVS_E_POINT_IN_2D ) + { + std::valarray locPoint ={ 0.0,0.0,0.0 }; + locPoint[0] = parameterValue(pointEntity.param[0]); + locPoint[1] = parameterValue(pointEntity.param[1]); + + Slvs_Entity e_Plane = (*m_entityMemory)[pointEntity.wrkpl - 1]; + std::valarray origin = global3DPos(e_Plane.point[0]); + auto mx = orientationMx(e_Plane.normal); + point = origin + std::get<0>(mx)*locPoint[0] + std::get<1>(mx)*locPoint[1]; + + } + else if ( pointEntity.type == SLVS_E_POINT_IN_3D ) + { + point[0] = parameterValue(pointEntity.param[0]); + point[1] = parameterValue(pointEntity.param[1]); + point[2] = parameterValue(pointEntity.param[2]); + } + + return point; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +Slvs_Constraint& SolveSpaceSystem::constraint(Slvs_hConstraint constraintId) +{ + return (*m_constraintMemory)[constraintId-1]; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- + std::vector SolveSpaceSystem::failedConstraints() const +{ + return (*m_failedConstrMemory); +} diff --git a/src/libslic3r/slvs/constrainteq.cpp b/src/libslic3r/slvs/constrainteq.cpp new file mode 100644 index 0000000000..86ed312f7d --- /dev/null +++ b/src/libslic3r/slvs/constrainteq.cpp @@ -0,0 +1,799 @@ +//----------------------------------------------------------------------------- +// Given a constraint, generate one or more equations in our symbolic algebra +// system to represent that constraint; also various geometric helper +// functions for that. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +const hConstraint ConstraintBase::NO_CONSTRAINT = { 0 }; + +bool ConstraintBase::HasLabel() const { + switch(type) { + case Type::PT_LINE_DISTANCE: + case Type::PT_PLANE_DISTANCE: + case Type::PT_FACE_DISTANCE: + case Type::PT_PT_DISTANCE: + case Type::PROJ_PT_DISTANCE: + case Type::DIAMETER: + case Type::LENGTH_RATIO: + case Type::LENGTH_DIFFERENCE: + case Type::ANGLE: + case Type::COMMENT: + return true; + + default: + return false; + } +} + +ExprVector ConstraintBase::VectorsParallel3d(ExprVector a, ExprVector b, hParam p) { + return a.Minus(b.ScaledBy(Expr::From(p))); +} + +Expr *ConstraintBase::PointLineDistance(hEntity wrkpl, hEntity hpt, hEntity hln) +{ + EntityBase *ln = SK.GetEntity(hln); + EntityBase *a = SK.GetEntity(ln->point[0]); + EntityBase *b = SK.GetEntity(ln->point[1]); + + EntityBase *p = SK.GetEntity(hpt); + + if(wrkpl.v == EntityBase::FREE_IN_3D.v) { + ExprVector ep = p->PointGetExprs(); + + ExprVector ea = a->PointGetExprs(); + ExprVector eb = b->PointGetExprs(); + ExprVector eab = ea.Minus(eb); + Expr *m = eab.Magnitude(); + + return ((eab.Cross(ea.Minus(ep))).Magnitude())->Div(m); + } else { + Expr *ua, *va, *ub, *vb; + a->PointGetExprsInWorkplane(wrkpl, &ua, &va); + b->PointGetExprsInWorkplane(wrkpl, &ub, &vb); + + Expr *du = ua->Minus(ub); + Expr *dv = va->Minus(vb); + + Expr *u, *v; + p->PointGetExprsInWorkplane(wrkpl, &u, &v); + + Expr *m = ((du->Square())->Plus(dv->Square()))->Sqrt(); + + Expr *proj = (dv->Times(ua->Minus(u)))->Minus( + (du->Times(va->Minus(v)))); + + return proj->Div(m); + } +} + +Expr *ConstraintBase::PointPlaneDistance(ExprVector p, hEntity hpl) { + ExprVector n; + Expr *d; + SK.GetEntity(hpl)->WorkplaneGetPlaneExprs(&n, &d); + return (p.Dot(n))->Minus(d); +} + +Expr *ConstraintBase::Distance(hEntity wrkpl, hEntity hpa, hEntity hpb) { + EntityBase *pa = SK.GetEntity(hpa); + EntityBase *pb = SK.GetEntity(hpb); + ssassert(pa->IsPoint() && pb->IsPoint(), + "Expected two points to measure projected distance between"); + + if(wrkpl.v == EntityBase::FREE_IN_3D.v) { + // This is true distance + ExprVector ea, eb, eab; + ea = pa->PointGetExprs(); + eb = pb->PointGetExprs(); + eab = ea.Minus(eb); + + return eab.Magnitude(); + } else { + // This is projected distance, in the given workplane. + Expr *au, *av, *bu, *bv; + + pa->PointGetExprsInWorkplane(wrkpl, &au, &av); + pb->PointGetExprsInWorkplane(wrkpl, &bu, &bv); + + Expr *du = au->Minus(bu); + Expr *dv = av->Minus(bv); + + return ((du->Square())->Plus(dv->Square()))->Sqrt(); + } +} + +//----------------------------------------------------------------------------- +// Return the cosine of the angle between two vectors. If a workplane is +// specified, then it's the cosine of their projections into that workplane. +//----------------------------------------------------------------------------- +Expr *ConstraintBase::DirectionCosine(hEntity wrkpl, + ExprVector ae, ExprVector be) +{ + if(wrkpl.v == EntityBase::FREE_IN_3D.v) { + Expr *mags = (ae.Magnitude())->Times(be.Magnitude()); + return (ae.Dot(be))->Div(mags); + } else { + EntityBase *w = SK.GetEntity(wrkpl); + ExprVector u = w->Normal()->NormalExprsU(); + ExprVector v = w->Normal()->NormalExprsV(); + Expr *ua = u.Dot(ae); + Expr *va = v.Dot(ae); + Expr *ub = u.Dot(be); + Expr *vb = v.Dot(be); + Expr *maga = (ua->Square()->Plus(va->Square()))->Sqrt(); + Expr *magb = (ub->Square()->Plus(vb->Square()))->Sqrt(); + Expr *dot = (ua->Times(ub))->Plus(va->Times(vb)); + return dot->Div(maga->Times(magb)); + } +} + +ExprVector ConstraintBase::PointInThreeSpace(hEntity workplane, + Expr *u, Expr *v) +{ + EntityBase *w = SK.GetEntity(workplane); + + ExprVector ub = w->Normal()->NormalExprsU(); + ExprVector vb = w->Normal()->NormalExprsV(); + ExprVector ob = w->WorkplaneGetOffsetExprs(); + + return (ub.ScaledBy(u)).Plus(vb.ScaledBy(v)).Plus(ob); +} + +void ConstraintBase::ModifyToSatisfy() { + if(type == Type::ANGLE) { + Vector a = SK.GetEntity(entityA)->VectorGetNum(); + Vector b = SK.GetEntity(entityB)->VectorGetNum(); + if(other) a = a.ScaledBy(-1); + if(workplane.v != EntityBase::FREE_IN_3D.v) { + a = a.ProjectVectorInto(workplane); + b = b.ProjectVectorInto(workplane); + } + double c = (a.Dot(b))/(a.Magnitude() * b.Magnitude()); + valA = acos(c)*180/PI; + } else if(type == Type::PT_ON_LINE) { + EntityBase *eln = SK.GetEntity(entityA); + EntityBase *ea = SK.GetEntity(eln->point[0]); + EntityBase *eb = SK.GetEntity(eln->point[1]); + EntityBase *ep = SK.GetEntity(ptA); + ExprVector exp = ep->PointGetExprsInWorkplane(workplane); + ExprVector exa = ea->PointGetExprsInWorkplane(workplane); + ExprVector exb = eb->PointGetExprsInWorkplane(workplane); + ExprVector exba = exb.Minus(exa); + SK.GetParam(valP)->val = exba.Dot(exp.Minus(exa))->Eval() / exba.Dot(exba)->Eval(); + } else { + // We'll fix these ones up by looking at their symbolic equation; + // that means no extra work. + IdList l = {}; + // Generate the equations even if this is a reference dimension + GenerateEquations(&l, /*forReference=*/true); + ssassert(l.n == 1, "Expected constraint to generate a single equation"); + + // These equations are written in the form f(...) - d = 0, where + // d is the value of the valA. + valA += (l.elem[0].e)->Eval(); + + l.Clear(); + } +} + +void ConstraintBase::AddEq(IdList *l, Expr *expr, int index) const +{ + Equation eq; + eq.e = expr; + eq.h = h.equation(index); + l->Add(&eq); +} + +void ConstraintBase::AddEq(IdList *l, const ExprVector &v, + int baseIndex) const { + AddEq(l, v.x, baseIndex); + AddEq(l, v.y, baseIndex + 1); + if(workplane.v == EntityBase::FREE_IN_3D.v) { + AddEq(l, v.z, baseIndex + 2); + } +} + +void ConstraintBase::Generate(IdList *l) { + switch(type) { + case Type::PARALLEL: + case Type::CUBIC_LINE_TANGENT: + // Add new parameter only when we operate in 3d space + if(workplane.v != EntityBase::FREE_IN_3D.v) break; + // fallthrough + case Type::SAME_ORIENTATION: + case Type::PT_ON_LINE: { + Param p = {}; + valP = h.param(0); + p.h = valP; + l->Add(&p); + break; + } + + default: + break; + } +} + +void ConstraintBase::GenerateEquations(IdList *l, + bool forReference) const { + if(reference && !forReference) return; + + Expr *exA = Expr::From(valA); + switch(type) { + case Type::PT_PT_DISTANCE: + AddEq(l, Distance(workplane, ptA, ptB)->Minus(exA), 0); + return; + + case Type::PROJ_PT_DISTANCE: { + ExprVector pA = SK.GetEntity(ptA)->PointGetExprs(), + pB = SK.GetEntity(ptB)->PointGetExprs(), + dp = pB.Minus(pA); + + ExprVector pp = SK.GetEntity(entityA)->VectorGetExprs(); + pp = pp.WithMagnitude(Expr::From(1.0)); + + AddEq(l, (dp.Dot(pp))->Minus(exA), 0); + return; + } + + case Type::PT_LINE_DISTANCE: + AddEq(l, + PointLineDistance(workplane, ptA, entityA)->Minus(exA), 0); + return; + + case Type::PT_PLANE_DISTANCE: { + ExprVector pt = SK.GetEntity(ptA)->PointGetExprs(); + AddEq(l, (PointPlaneDistance(pt, entityA))->Minus(exA), 0); + return; + } + + case Type::PT_FACE_DISTANCE: { + ExprVector pt = SK.GetEntity(ptA)->PointGetExprs(); + EntityBase *f = SK.GetEntity(entityA); + ExprVector p0 = f->FaceGetPointExprs(); + ExprVector n = f->FaceGetNormalExprs(); + AddEq(l, (pt.Minus(p0)).Dot(n)->Minus(exA), 0); + return; + } + + case Type::EQUAL_LENGTH_LINES: { + EntityBase *a = SK.GetEntity(entityA); + EntityBase *b = SK.GetEntity(entityB); + AddEq(l, Distance(workplane, a->point[0], a->point[1])->Minus( + Distance(workplane, b->point[0], b->point[1])), 0); + return; + } + + // These work on distance squared, since the pt-line distances are + // signed, and we want the absolute value. + case Type::EQ_LEN_PT_LINE_D: { + EntityBase *forLen = SK.GetEntity(entityA); + Expr *d1 = Distance(workplane, forLen->point[0], forLen->point[1]); + Expr *d2 = PointLineDistance(workplane, ptA, entityB); + AddEq(l, (d1->Square())->Minus(d2->Square()), 0); + return; + } + case Type::EQ_PT_LN_DISTANCES: { + Expr *d1 = PointLineDistance(workplane, ptA, entityA); + Expr *d2 = PointLineDistance(workplane, ptB, entityB); + AddEq(l, (d1->Square())->Minus(d2->Square()), 0); + return; + } + + case Type::LENGTH_RATIO: { + EntityBase *a = SK.GetEntity(entityA); + EntityBase *b = SK.GetEntity(entityB); + Expr *la = Distance(workplane, a->point[0], a->point[1]); + Expr *lb = Distance(workplane, b->point[0], b->point[1]); + AddEq(l, (la->Div(lb))->Minus(exA), 0); + return; + } + + case Type::LENGTH_DIFFERENCE: { + EntityBase *a = SK.GetEntity(entityA); + EntityBase *b = SK.GetEntity(entityB); + Expr *la = Distance(workplane, a->point[0], a->point[1]); + Expr *lb = Distance(workplane, b->point[0], b->point[1]); + AddEq(l, (la->Minus(lb))->Minus(exA), 0); + return; + } + + case Type::DIAMETER: { + EntityBase *circle = SK.GetEntity(entityA); + Expr *r = circle->CircleGetRadiusExpr(); + AddEq(l, (r->Times(Expr::From(2)))->Minus(exA), 0); + return; + } + + case Type::EQUAL_RADIUS: { + EntityBase *c1 = SK.GetEntity(entityA); + EntityBase *c2 = SK.GetEntity(entityB); + AddEq(l, (c1->CircleGetRadiusExpr())->Minus( + c2->CircleGetRadiusExpr()), 0); + return; + } + + case Type::EQUAL_LINE_ARC_LEN: { + EntityBase *line = SK.GetEntity(entityA), + *arc = SK.GetEntity(entityB); + + // Get the line length + ExprVector l0 = SK.GetEntity(line->point[0])->PointGetExprs(), + l1 = SK.GetEntity(line->point[1])->PointGetExprs(); + Expr *ll = (l1.Minus(l0)).Magnitude(); + + // And get the arc radius, and the cosine of its angle + EntityBase *ao = SK.GetEntity(arc->point[0]), + *as = SK.GetEntity(arc->point[1]), + *af = SK.GetEntity(arc->point[2]); + + ExprVector aos = (as->PointGetExprs()).Minus(ao->PointGetExprs()), + aof = (af->PointGetExprs()).Minus(ao->PointGetExprs()); + Expr *r = aof.Magnitude(); + + ExprVector n = arc->Normal()->NormalExprsN(); + ExprVector u = aos.WithMagnitude(Expr::From(1.0)); + ExprVector v = n.Cross(u); + // so in our new csys, we start at (1, 0, 0) + Expr *costheta = aof.Dot(u)->Div(r); + Expr *sintheta = aof.Dot(v)->Div(r); + + double thetas, thetaf, dtheta; + arc->ArcGetAngles(&thetas, &thetaf, &dtheta); + Expr *theta; + if(dtheta < 3*PI/4) { + theta = costheta->ACos(); + } else if(dtheta < 5*PI/4) { + // As the angle crosses pi, cos theta is not invertible; + // so use the sine to stop blowing up + theta = Expr::From(PI)->Minus(sintheta->ASin()); + } else { + theta = (Expr::From(2*PI))->Minus(costheta->ACos()); + } + + // And write the equation; r*theta = L + AddEq(l, (r->Times(theta))->Minus(ll), 0); + return; + } + + case Type::POINTS_COINCIDENT: { + EntityBase *a = SK.GetEntity(ptA); + EntityBase *b = SK.GetEntity(ptB); + if(workplane.v == EntityBase::FREE_IN_3D.v) { + ExprVector pa = a->PointGetExprs(); + ExprVector pb = b->PointGetExprs(); + AddEq(l, pa.x->Minus(pb.x), 0); + AddEq(l, pa.y->Minus(pb.y), 1); + AddEq(l, pa.z->Minus(pb.z), 2); + } else { + Expr *au, *av; + Expr *bu, *bv; + a->PointGetExprsInWorkplane(workplane, &au, &av); + b->PointGetExprsInWorkplane(workplane, &bu, &bv); + AddEq(l, au->Minus(bu), 0); + AddEq(l, av->Minus(bv), 1); + } + return; + } + + case Type::PT_IN_PLANE: + // This one works the same, whether projected or not. + AddEq(l, PointPlaneDistance( + SK.GetEntity(ptA)->PointGetExprs(), entityA), 0); + return; + + case Type::PT_ON_FACE: { + // a plane, n dot (p - p0) = 0 + ExprVector p = SK.GetEntity(ptA)->PointGetExprs(); + EntityBase *f = SK.GetEntity(entityA); + ExprVector p0 = f->FaceGetPointExprs(); + ExprVector n = f->FaceGetNormalExprs(); + AddEq(l, (p.Minus(p0)).Dot(n), 0); + return; + } + + case Type::PT_ON_LINE: { + EntityBase *ln = SK.GetEntity(entityA); + EntityBase *a = SK.GetEntity(ln->point[0]); + EntityBase *b = SK.GetEntity(ln->point[1]); + EntityBase *p = SK.GetEntity(ptA); + + ExprVector ep = p->PointGetExprsInWorkplane(workplane); + ExprVector ea = a->PointGetExprsInWorkplane(workplane); + ExprVector eb = b->PointGetExprsInWorkplane(workplane); + + ExprVector ptOnLine = ea.Plus(eb.Minus(ea).ScaledBy(Expr::From(valP))); + ExprVector eq = ptOnLine.Minus(ep); + + AddEq(l, eq); + return; + } + + case Type::PT_ON_CIRCLE: { + // This actually constrains the point to lie on the cylinder. + EntityBase *circle = SK.GetEntity(entityA); + ExprVector center = SK.GetEntity(circle->point[0])->PointGetExprs(); + ExprVector pt = SK.GetEntity(ptA)->PointGetExprs(); + EntityBase *normal = SK.GetEntity(circle->normal); + ExprVector u = normal->NormalExprsU(), + v = normal->NormalExprsV(); + + Expr *du = (center.Minus(pt)).Dot(u), + *dv = (center.Minus(pt)).Dot(v); + + Expr *r = circle->CircleGetRadiusExpr(); + + AddEq(l, du->Square()->Plus(dv->Square())->Sqrt()->Minus(r), 0); + return; + } + + case Type::AT_MIDPOINT: + if(workplane.v == EntityBase::FREE_IN_3D.v) { + EntityBase *ln = SK.GetEntity(entityA); + ExprVector a = SK.GetEntity(ln->point[0])->PointGetExprs(); + ExprVector b = SK.GetEntity(ln->point[1])->PointGetExprs(); + ExprVector m = (a.Plus(b)).ScaledBy(Expr::From(0.5)); + + if(ptA.v) { + ExprVector p = SK.GetEntity(ptA)->PointGetExprs(); + AddEq(l, (m.x)->Minus(p.x), 0); + AddEq(l, (m.y)->Minus(p.y), 1); + AddEq(l, (m.z)->Minus(p.z), 2); + } else { + AddEq(l, PointPlaneDistance(m, entityB), 0); + } + } else { + EntityBase *ln = SK.GetEntity(entityA); + EntityBase *a = SK.GetEntity(ln->point[0]); + EntityBase *b = SK.GetEntity(ln->point[1]); + + Expr *au, *av, *bu, *bv; + a->PointGetExprsInWorkplane(workplane, &au, &av); + b->PointGetExprsInWorkplane(workplane, &bu, &bv); + Expr *mu = Expr::From(0.5)->Times(au->Plus(bu)); + Expr *mv = Expr::From(0.5)->Times(av->Plus(bv)); + + if(ptA.v) { + EntityBase *p = SK.GetEntity(ptA); + Expr *pu, *pv; + p->PointGetExprsInWorkplane(workplane, &pu, &pv); + AddEq(l, pu->Minus(mu), 0); + AddEq(l, pv->Minus(mv), 1); + } else { + ExprVector m = PointInThreeSpace(workplane, mu, mv); + AddEq(l, PointPlaneDistance(m, entityB), 0); + } + } + return; + + case Type::SYMMETRIC: + if(workplane.v == EntityBase::FREE_IN_3D.v) { + EntityBase *plane = SK.GetEntity(entityA); + EntityBase *ea = SK.GetEntity(ptA); + EntityBase *eb = SK.GetEntity(ptB); + ExprVector a = ea->PointGetExprs(); + ExprVector b = eb->PointGetExprs(); + + // The midpoint of the line connecting the symmetric points + // lies on the plane of the symmetry. + ExprVector m = (a.Plus(b)).ScaledBy(Expr::From(0.5)); + AddEq(l, PointPlaneDistance(m, plane->h), 0); + + // And projected into the plane of symmetry, the points are + // coincident. + Expr *au, *av, *bu, *bv; + ea->PointGetExprsInWorkplane(plane->h, &au, &av); + eb->PointGetExprsInWorkplane(plane->h, &bu, &bv); + AddEq(l, au->Minus(bu), 1); + AddEq(l, av->Minus(bv), 2); + } else { + EntityBase *plane = SK.GetEntity(entityA); + EntityBase *a = SK.GetEntity(ptA); + EntityBase *b = SK.GetEntity(ptB); + + Expr *au, *av, *bu, *bv; + a->PointGetExprsInWorkplane(workplane, &au, &av); + b->PointGetExprsInWorkplane(workplane, &bu, &bv); + Expr *mu = Expr::From(0.5)->Times(au->Plus(bu)); + Expr *mv = Expr::From(0.5)->Times(av->Plus(bv)); + + ExprVector m = PointInThreeSpace(workplane, mu, mv); + AddEq(l, PointPlaneDistance(m, plane->h), 0); + + // Construct a vector within the workplane that is normal + // to the symmetry pane's normal (i.e., that lies in the + // plane of symmetry). The line connecting the points is + // perpendicular to that constructed vector. + EntityBase *w = SK.GetEntity(workplane); + ExprVector u = w->Normal()->NormalExprsU(); + ExprVector v = w->Normal()->NormalExprsV(); + + ExprVector pa = a->PointGetExprs(); + ExprVector pb = b->PointGetExprs(); + ExprVector n; + Expr *d; + plane->WorkplaneGetPlaneExprs(&n, &d); + AddEq(l, (n.Cross(u.Cross(v))).Dot(pa.Minus(pb)), 1); + } + return; + + case Type::SYMMETRIC_HORIZ: + case Type::SYMMETRIC_VERT: { + ssassert(workplane.v != Entity::FREE_IN_3D.v, + "Unexpected horizontal/vertical symmetric constraint in 3d"); + + EntityBase *a = SK.GetEntity(ptA); + EntityBase *b = SK.GetEntity(ptB); + + Expr *au, *av, *bu, *bv; + a->PointGetExprsInWorkplane(workplane, &au, &av); + b->PointGetExprsInWorkplane(workplane, &bu, &bv); + + if(type == Type::SYMMETRIC_HORIZ) { + AddEq(l, av->Minus(bv), 0); + AddEq(l, au->Plus(bu), 1); + } else { + AddEq(l, au->Minus(bu), 0); + AddEq(l, av->Plus(bv), 1); + } + return; + } + + case Type::SYMMETRIC_LINE: { + EntityBase *pa = SK.GetEntity(ptA); + EntityBase *pb = SK.GetEntity(ptB); + + Expr *pau, *pav, *pbu, *pbv; + pa->PointGetExprsInWorkplane(workplane, &pau, &pav); + pb->PointGetExprsInWorkplane(workplane, &pbu, &pbv); + + EntityBase *ln = SK.GetEntity(entityA); + EntityBase *la = SK.GetEntity(ln->point[0]); + EntityBase *lb = SK.GetEntity(ln->point[1]); + Expr *lau, *lav, *lbu, *lbv; + la->PointGetExprsInWorkplane(workplane, &lau, &lav); + lb->PointGetExprsInWorkplane(workplane, &lbu, &lbv); + + Expr *dpu = pbu->Minus(pau), *dpv = pbv->Minus(pav); + Expr *dlu = lbu->Minus(lau), *dlv = lbv->Minus(lav); + + // The line through the points is perpendicular to the line + // of symmetry. + AddEq(l, (dlu->Times(dpu))->Plus(dlv->Times(dpv)), 0); + + // And the signed distances of the points to the line are + // equal in magnitude and opposite in sign, so sum to zero + Expr *dista = (dlv->Times(lau->Minus(pau)))->Minus( + (dlu->Times(lav->Minus(pav)))); + Expr *distb = (dlv->Times(lau->Minus(pbu)))->Minus( + (dlu->Times(lav->Minus(pbv)))); + AddEq(l, dista->Plus(distb), 1); + + return; + } + + case Type::HORIZONTAL: + case Type::VERTICAL: { + ssassert(workplane.v != Entity::FREE_IN_3D.v, + "Unexpected horizontal/vertical constraint in 3d"); + + hEntity ha, hb; + if(entityA.v) { + EntityBase *e = SK.GetEntity(entityA); + ha = e->point[0]; + hb = e->point[1]; + } else { + ha = ptA; + hb = ptB; + } + EntityBase *a = SK.GetEntity(ha); + EntityBase *b = SK.GetEntity(hb); + + Expr *au, *av, *bu, *bv; + a->PointGetExprsInWorkplane(workplane, &au, &av); + b->PointGetExprsInWorkplane(workplane, &bu, &bv); + + AddEq(l, (type == Type::HORIZONTAL) ? av->Minus(bv) : au->Minus(bu), 0); + return; + } + + case Type::SAME_ORIENTATION: { + EntityBase *a = SK.GetEntity(entityA); + EntityBase *b = SK.GetEntity(entityB); + + ExprVector au = a->NormalExprsU(), + an = a->NormalExprsN(); + ExprVector bu = b->NormalExprsU(), + bv = b->NormalExprsV(), + bn = b->NormalExprsN(); + + ExprVector eq = VectorsParallel3d(an, bn, valP); + AddEq(l, eq.x, 0); + AddEq(l, eq.y, 1); + AddEq(l, eq.z, 2); + Expr *d1 = au.Dot(bv); + Expr *d2 = au.Dot(bu); + // Allow either orientation for the coordinate system, depending + // on how it was drawn. + if(fabs(d1->Eval()) < fabs(d2->Eval())) { + AddEq(l, d1, 3); + } else { + AddEq(l, d2, 3); + } + return; + } + + case Type::PERPENDICULAR: + case Type::ANGLE: { + EntityBase *a = SK.GetEntity(entityA); + EntityBase *b = SK.GetEntity(entityB); + ExprVector ae = a->VectorGetExprs(); + ExprVector be = b->VectorGetExprs(); + if(other) ae = ae.ScaledBy(Expr::From(-1)); + Expr *c = DirectionCosine(workplane, ae, be); + + if(type == Type::ANGLE) { + // The direction cosine is equal to the cosine of the + // specified angle + Expr *rads = exA->Times(Expr::From(PI/180)), + *rc = rads->Cos(); + double arc = fabs(rc->Eval()); + // avoid false detection of inconsistent systems by gaining + // up as the difference in dot products gets small at small + // angles; doubles still have plenty of precision, only + // problem is that rank test + Expr *mult = Expr::From(arc > 0.99 ? 0.01/(1.00001 - arc) : 1); + AddEq(l, (c->Minus(rc))->Times(mult), 0); + } else { + // The dot product (and therefore the direction cosine) + // is equal to zero, perpendicular. + AddEq(l, c, 0); + } + return; + } + + case Type::EQUAL_ANGLE: { + EntityBase *a = SK.GetEntity(entityA); + EntityBase *b = SK.GetEntity(entityB); + EntityBase *c = SK.GetEntity(entityC); + EntityBase *d = SK.GetEntity(entityD); + ExprVector ae = a->VectorGetExprs(); + ExprVector be = b->VectorGetExprs(); + ExprVector ce = c->VectorGetExprs(); + ExprVector de = d->VectorGetExprs(); + + if(other) ae = ae.ScaledBy(Expr::From(-1)); + + Expr *cab = DirectionCosine(workplane, ae, be); + Expr *ccd = DirectionCosine(workplane, ce, de); + + AddEq(l, cab->Minus(ccd), 0); + return; + } + + case Type::ARC_LINE_TANGENT: { + EntityBase *arc = SK.GetEntity(entityA); + EntityBase *line = SK.GetEntity(entityB); + + ExprVector ac = SK.GetEntity(arc->point[0])->PointGetExprs(); + ExprVector ap = + SK.GetEntity(arc->point[other ? 2 : 1])->PointGetExprs(); + + ExprVector ld = line->VectorGetExprs(); + + // The line is perpendicular to the radius + AddEq(l, ld.Dot(ac.Minus(ap)), 0); + return; + } + + case Type::CUBIC_LINE_TANGENT: { + EntityBase *cubic = SK.GetEntity(entityA); + EntityBase *line = SK.GetEntity(entityB); + + ExprVector a; + if(other) { + a = cubic->CubicGetFinishTangentExprs(); + } else { + a = cubic->CubicGetStartTangentExprs(); + } + + ExprVector b = line->VectorGetExprs(); + + if(workplane.v == EntityBase::FREE_IN_3D.v) { + ExprVector eq = VectorsParallel3d(a, b, valP); + AddEq(l, eq); + } else { + EntityBase *w = SK.GetEntity(workplane); + ExprVector wn = w->Normal()->NormalExprsN(); + AddEq(l, (a.Cross(b)).Dot(wn), 0); + } + return; + } + + case Type::CURVE_CURVE_TANGENT: { + bool parallel = true; + int i; + ExprVector dir[2]; + for(i = 0; i < 2; i++) { + EntityBase *e = SK.GetEntity((i == 0) ? entityA : entityB); + bool oth = (i == 0) ? other : other2; + + if(e->type == Entity::Type::ARC_OF_CIRCLE) { + ExprVector center, endpoint; + center = SK.GetEntity(e->point[0])->PointGetExprs(); + endpoint = + SK.GetEntity(e->point[oth ? 2 : 1])->PointGetExprs(); + dir[i] = endpoint.Minus(center); + // We're using the vector from the center of the arc to + // an endpoint; so that's normal to the tangent, not + // parallel. + parallel = !parallel; + } else if(e->type == Entity::Type::CUBIC) { // BRANCH_ALWAYS_TAKEN + if(oth) { + dir[i] = e->CubicGetFinishTangentExprs(); + } else { + dir[i] = e->CubicGetStartTangentExprs(); + } + } else { + ssassert(false, "Unexpected entity types for CURVE_CURVE_TANGENT"); + } + } + if(parallel) { + EntityBase *w = SK.GetEntity(workplane); + ExprVector wn = w->Normal()->NormalExprsN(); + AddEq(l, ((dir[0]).Cross(dir[1])).Dot(wn), 0); + } else { + AddEq(l, (dir[0]).Dot(dir[1]), 0); + } + return; + } + + case Type::PARALLEL: { + EntityBase *ea = SK.GetEntity(entityA), *eb = SK.GetEntity(entityB); + ExprVector a = ea->VectorGetExprsInWorkplane(workplane); + ExprVector b = eb->VectorGetExprsInWorkplane(workplane); + + if(workplane.v == EntityBase::FREE_IN_3D.v) { + ExprVector eq = VectorsParallel3d(a, b, valP); + AddEq(l, eq); + } else { + // We use expressions written in workplane csys, so we can assume the workplane + // normal is (0, 0, 1). We can write the equation as: + // Expr *eq = a.Cross(b).Dot(ExprVector::From(0.0, 0.0, 1.0)); + // but this will just result in elimination of x and y terms after dot product. + // We can only use the z expression: + // Expr *eq = a.Cross(b).z; + // but it's more efficient to write it in the terms of pseudo-scalar product: + Expr *eq = (a.x->Times(b.y))->Minus(a.y->Times(b.x)); + AddEq(l, eq, 0); + } + + return; + } + + case Type::WHERE_DRAGGED: { + EntityBase *ep = SK.GetEntity(ptA); + if(workplane.v == EntityBase::FREE_IN_3D.v) { + ExprVector ev = ep->PointGetExprs(); + Vector v = ep->PointGetNum(); + + AddEq(l, ev.x->Minus(Expr::From(v.x)), 0); + AddEq(l, ev.y->Minus(Expr::From(v.y)), 1); + AddEq(l, ev.z->Minus(Expr::From(v.z)), 2); + } else { + Expr *u, *v; + ep->PointGetExprsInWorkplane(workplane, &u, &v); + AddEq(l, u->Minus(Expr::From(u->Eval())), 0); + AddEq(l, v->Minus(Expr::From(v->Eval())), 1); + } + return; + } + + case Type::COMMENT: + return; + } + ssassert(false, "Unexpected constraint ID"); +} + diff --git a/src/libslic3r/slvs/dsc.h b/src/libslic3r/slvs/dsc.h new file mode 100644 index 0000000000..bd5c7f35da --- /dev/null +++ b/src/libslic3r/slvs/dsc.h @@ -0,0 +1,578 @@ +//----------------------------------------------------------------------------- +// Data structures used frequently in the program, various kinds of vectors +// (of real numbers, not symbolic algebra stuff) and our templated lists. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#ifndef __DSC_H +#define __DSC_H + +#include "solvespace.h" + +class Vector; +class Vector4; +class Point2d; +class hEntity; +class hParam; + +class Quaternion { +public: + // a + (vx)*i + (vy)*j + (vz)*k + double w, vx, vy, vz; + + static const Quaternion IDENTITY; + + static Quaternion From(double w, double vx, double vy, double vz); + static Quaternion From(hParam w, hParam vx, hParam vy, hParam vz); + static Quaternion From(Vector u, Vector v); + static Quaternion From(Vector axis, double dtheta); + + Quaternion Plus(Quaternion b) const; + Quaternion Minus(Quaternion b) const; + Quaternion ScaledBy(double s) const; + double Magnitude() const; + Quaternion WithMagnitude(double s) const; + + // Call a rotation matrix [ u' v' n' ]'; this returns the first and + // second rows, where that matrix is generated by this quaternion + Vector RotationU() const; + Vector RotationV() const; + Vector RotationN() const; + Vector Rotate(Vector p) const; + + Quaternion ToThe(double p) const; + Quaternion Inverse() const; + Quaternion Times(Quaternion b) const; + Quaternion Mirror() const; +}; + +class Vector { +public: + double x, y, z; + + static Vector From(double x, double y, double z); + static Vector From(hParam x, hParam y, hParam z); + static Vector AtIntersectionOfPlanes(Vector n1, double d1, + Vector n2, double d2); + static Vector AtIntersectionOfLines(Vector a0, Vector a1, + Vector b0, Vector b1, + bool *skew, + double *pa=NULL, double *pb=NULL); + static Vector AtIntersectionOfPlaneAndLine(Vector n, double d, + Vector p0, Vector p1, + bool *parallel); + static Vector AtIntersectionOfPlanes(Vector na, double da, + Vector nb, double db, + Vector nc, double dc, bool *parallel); + static void ClosestPointBetweenLines(Vector pa, Vector da, + Vector pb, Vector db, + double *ta, double *tb); + + double Element(int i) const; + bool Equals(Vector v, double tol=LENGTH_EPS) const; + bool EqualsExactly(Vector v) const; + Vector Plus(Vector b) const; + Vector Minus(Vector b) const; + Vector Negated() const; + Vector Cross(Vector b) const; + double DirectionCosineWith(Vector b) const; + double Dot(Vector b) const; + Vector Normal(int which) const; + Vector RotatedAbout(Vector orig, Vector axis, double theta) const; + Vector RotatedAbout(Vector axis, double theta) const; + Vector DotInToCsys(Vector u, Vector v, Vector n) const; + Vector ScaleOutOfCsys(Vector u, Vector v, Vector n) const; + double DistanceToLine(Vector p0, Vector dp) const; + double DistanceToPlane(Vector normal, Vector origin) const; + bool OnLineSegment(Vector a, Vector b, double tol=LENGTH_EPS) const; + Vector ClosestPointOnLine(Vector p0, Vector deltal) const; + double Magnitude() const; + double MagSquared() const; + Vector WithMagnitude(double s) const; + Vector ScaledBy(double s) const; + Vector ProjectInto(hEntity wrkpl) const; + Vector ProjectVectorInto(hEntity wrkpl) const; + double DivPivoting(Vector delta) const; + Vector ClosestOrtho() const; + void MakeMaxMin(Vector *maxv, Vector *minv) const; + Vector ClampWithin(double minv, double maxv) const; + static bool BoundingBoxesDisjoint(Vector amax, Vector amin, + Vector bmax, Vector bmin); + static bool BoundingBoxIntersectsLine(Vector amax, Vector amin, + Vector p0, Vector p1, bool asSegment); + bool OutsideAndNotOn(Vector maxv, Vector minv) const; + Vector InPerspective(Vector u, Vector v, Vector n, + Vector origin, double cameraTan) const; + Point2d Project2d(Vector u, Vector v) const; + Point2d ProjectXy() const; + Vector4 Project4d() const; +}; + +struct VectorHash { + size_t operator()(const Vector &v) const; +}; + +struct VectorPred { + bool operator()(Vector a, Vector b) const; +}; + +class Vector4 { +public: + double w, x, y, z; + + static Vector4 From(double w, double x, double y, double z); + static Vector4 From(double w, Vector v3); + static Vector4 Blend(Vector4 a, Vector4 b, double t); + + Vector4 Plus(Vector4 b) const; + Vector4 Minus(Vector4 b) const; + Vector4 ScaledBy(double s) const; + Vector PerspectiveProject() const; +}; + +class Point2d { +public: + double x, y; + + static Point2d From(double x, double y); + static Point2d FromPolar(double r, double a); + + Point2d Plus(const Point2d &b) const; + Point2d Minus(const Point2d &b) const; + Point2d ScaledBy(double s) const; + double DivPivoting(Point2d delta) const; + double Dot(Point2d p) const; + double DistanceTo(const Point2d &p) const; + double DistanceToLine(const Point2d &p0, const Point2d &dp, bool asSegment) const; + double DistanceToLineSigned(const Point2d &p0, const Point2d &dp, bool asSegment) const; + double Angle() const; + double AngleTo(const Point2d &p) const; + double Magnitude() const; + double MagSquared() const; + Point2d WithMagnitude(double v) const; + Point2d Normal() const; + bool Equals(Point2d v, double tol=LENGTH_EPS) const; +}; + +// A simple list +template +class List { +public: + T *elem; + int n; + int elemsAllocated; + + void ReserveMore(int howMuch) { + if(n + howMuch > elemsAllocated) { + elemsAllocated = n + howMuch; + T *newElem = (T *)MemAlloc((size_t)elemsAllocated*sizeof(elem[0])); + for(int i = 0; i < n; i++) { + new(&newElem[i]) T(std::move(elem[i])); + elem[i].~T(); + } + MemFree(elem); + elem = newElem; + } + } + + void AllocForOneMore() { + if(n >= elemsAllocated) { + ReserveMore((elemsAllocated + 32)*2 - n); + } + } + + void Add(const T *t) { + AllocForOneMore(); + new(&elem[n++]) T(*t); + } + + void AddToBeginning(const T *t) { + AllocForOneMore(); + new(&elem[n]) T(); + std::move_backward(elem, elem + 1, elem + n + 1); + elem[0] = *t; + n++; + } + + T *First() { + return (n == 0) ? NULL : &(elem[0]); + } + const T *First() const { + return (n == 0) ? NULL : &(elem[0]); + } + T *NextAfter(T *prev) { + if(!prev) return NULL; + if(prev - elem == (n - 1)) return NULL; + return prev + 1; + } + const T *NextAfter(const T *prev) const { + if(!prev) return NULL; + if(prev - elem == (n - 1)) return NULL; + return prev + 1; + } + + T *begin() { return &elem[0]; } + T *end() { return &elem[n]; } + const T *begin() const { return &elem[0]; } + const T *end() const { return &elem[n]; } + + void ClearTags() { + int i; + for(i = 0; i < n; i++) { + elem[i].tag = 0; + } + } + + void Clear() { + for(int i = 0; i < n; i++) + elem[i].~T(); + if(elem) MemFree(elem); + elem = NULL; + n = elemsAllocated = 0; + } + + void RemoveTagged() { + int src, dest; + dest = 0; + for(src = 0; src < n; src++) { + if(elem[src].tag) { + // this item should be deleted + } else { + if(src != dest) { + elem[dest] = elem[src]; + } + dest++; + } + } + for(int i = dest; i < n; i++) + elem[i].~T(); + n = dest; + // and elemsAllocated is untouched, because we didn't resize + } + + void RemoveLast(int cnt) { + ssassert(n >= cnt, "Removing more elements than the list contains"); + for(int i = n - cnt; i < n; i++) + elem[i].~T(); + n -= cnt; + // and elemsAllocated is untouched, same as in RemoveTagged + } + + void Reverse() { + int i; + for(i = 0; i < (n/2); i++) { + swap(elem[i], elem[(n-1)-i]); + } + } +}; + +// A list, where each element has an integer identifier. The list is kept +// sorted by that identifier, and items can be looked up in log n time by +// id. +template +class IdList { +public: + T *elem; + int n; + int elemsAllocated; + + uint32_t MaximumId() { + if(n == 0) { + return 0; + } else { + return elem[n - 1].h.v; + } + } + + H AddAndAssignId(T *t) { + t->h.v = (MaximumId() + 1); + Add(t); + + return t->h; + } + + void ReserveMore(int howMuch) { + if(n + howMuch > elemsAllocated) { + elemsAllocated = n + howMuch; + T *newElem = (T *)MemAlloc((size_t)elemsAllocated*sizeof(elem[0])); + for(int i = 0; i < n; i++) { + new(&newElem[i]) T(std::move(elem[i])); + elem[i].~T(); + } + MemFree(elem); + elem = newElem; + } + } + + void Add(T *t) { + if(n >= elemsAllocated) { + ReserveMore((elemsAllocated + 32)*2 - n); + } + + int first = 0, last = n; + // We know that we must insert within the closed interval [first,last] + while(first != last) { + int mid = (first + last)/2; + H hm = elem[mid].h; + ssassert(hm.v != t->h.v, "Handle isn't unique"); + if(hm.v > t->h.v) { + last = mid; + } else if(hm.v < t->h.v) { + first = mid + 1; + } + } + + int i = first; + new(&elem[n]) T(); + std::move_backward(elem + i, elem + n, elem + n + 1); + elem[i] = *t; + n++; + } + + T *FindById(H h) { + T *t = FindByIdNoOops(h); + ssassert(t != NULL, "Cannot find handle"); + return t; + } + + int IndexOf(H h) { + int first = 0, last = n-1; + while(first <= last) { + int mid = (first + last)/2; + H hm = elem[mid].h; + if(hm.v > h.v) { + last = mid-1; // and first stays the same + } else if(hm.v < h.v) { + first = mid+1; // and last stays the same + } else { + return mid; + } + } + return -1; + } + + T *FindByIdNoOops(H h) { + int first = 0, last = n-1; + while(first <= last) { + int mid = (first + last)/2; + H hm = elem[mid].h; + if(hm.v > h.v) { + last = mid-1; // and first stays the same + } else if(hm.v < h.v) { + first = mid+1; // and last stays the same + } else { + return &(elem[mid]); + } + } + return NULL; + } + + T *First() { + return (n == 0) ? NULL : &(elem[0]); + } + T *NextAfter(T *prev) { + if(!prev) return NULL; + if(prev - elem == (n - 1)) return NULL; + return prev + 1; + } + + T *begin() { return &elem[0]; } + T *end() { return &elem[n]; } + const T *begin() const { return &elem[0]; } + const T *end() const { return &elem[n]; } + + void ClearTags() { + int i; + for(i = 0; i < n; i++) { + elem[i].tag = 0; + } + } + + void Tag(H h, int tag) { + int i; + for(i = 0; i < n; i++) { + if(elem[i].h.v == h.v) { + elem[i].tag = tag; + } + } + } + + void RemoveTagged() { + int src, dest; + dest = 0; + for(src = 0; src < n; src++) { + if(elem[src].tag) { + // this item should be deleted + elem[src].Clear(); + } else { + if(src != dest) { + elem[dest] = elem[src]; + } + dest++; + } + } + for(int i = dest; i < n; i++) + elem[i].~T(); + n = dest; + // and elemsAllocated is untouched, because we didn't resize + } + void RemoveById(H h) { + ClearTags(); + FindById(h)->tag = 1; + RemoveTagged(); + } + + void MoveSelfInto(IdList *l) { + l->Clear(); + *l = *this; + elemsAllocated = n = 0; + elem = NULL; + } + + void DeepCopyInto(IdList *l) { + l->Clear(); + l->elem = (T *)MemAlloc(elemsAllocated * sizeof(elem[0])); + for(int i = 0; i < n; i++) + new(&l->elem[i]) T(elem[i]); + l->elemsAllocated = elemsAllocated; + l->n = n; + } + + void Clear() { + for(int i = 0; i < n; i++) { + elem[i].Clear(); + elem[i].~T(); + } + elemsAllocated = n = 0; + if(elem) MemFree(elem); + elem = NULL; + } + +}; + +class BandedMatrix { +public: + enum { + MAX_UNKNOWNS = 16, + RIGHT_OF_DIAG = 1, + LEFT_OF_DIAG = 2 + }; + + double A[MAX_UNKNOWNS][MAX_UNKNOWNS]; + double B[MAX_UNKNOWNS]; + double X[MAX_UNKNOWNS]; + int n; + + void Solve(); +}; + +#define RGBi(r, g, b) RgbaColor::From((r), (g), (b)) +#define RGBf(r, g, b) RgbaColor::FromFloat((float)(r), (float)(g), (float)(b)) + +// Note: sizeof(class RgbaColor) should be exactly 4 +// +class RgbaColor { +public: + uint8_t red, green, blue, alpha; + + float redF() const { return (float)red / 255.0f; } + float greenF() const { return (float)green / 255.0f; } + float blueF() const { return (float)blue / 255.0f; } + float alphaF() const { return (float)alpha / 255.0f; } + + bool IsEmpty() const { return alpha == 0; } + + bool Equals(RgbaColor c) const { + return + c.red == red && + c.green == green && + c.blue == blue && + c.alpha == alpha; + } + + RgbaColor WithAlpha(uint8_t newAlpha) const { + RgbaColor color = *this; + color.alpha = newAlpha; + return color; + } + + uint32_t ToPackedIntBGRA() const { + return + blue | + (uint32_t)(green << 8) | + (uint32_t)(red << 16) | + (uint32_t)((255 - alpha) << 24); + } + + uint32_t ToPackedInt() const { + return + red | + (uint32_t)(green << 8) | + (uint32_t)(blue << 16) | + (uint32_t)((255 - alpha) << 24); + } + + uint32_t ToARGB32() const { + return + blue | + (uint32_t)(green << 8) | + (uint32_t)(red << 16) | + (uint32_t)(alpha << 24); + } + + static RgbaColor From(int r, int g, int b, int a = 255) { + RgbaColor c; + c.red = (uint8_t)r; + c.green = (uint8_t)g; + c.blue = (uint8_t)b; + c.alpha = (uint8_t)a; + return c; + } + + static RgbaColor FromFloat(float r, float g, float b, float a = 1.0) { + return From( + (int)(255.1f * r), + (int)(255.1f * g), + (int)(255.1f * b), + (int)(255.1f * a)); + } + + static RgbaColor FromPackedInt(uint32_t rgba) { + return From( + (int)((rgba) & 0xff), + (int)((rgba >> 8) & 0xff), + (int)((rgba >> 16) & 0xff), + (int)(255 - ((rgba >> 24) & 0xff))); + } + + static RgbaColor FromPackedIntBGRA(uint32_t bgra) { + return From( + (int)((bgra >> 16) & 0xff), + (int)((bgra >> 8) & 0xff), + (int)((bgra) & 0xff), + (int)(255 - ((bgra >> 24) & 0xff))); + } +}; + +struct RgbaColorCompare { + bool operator()(RgbaColor a, RgbaColor b) const { + return a.ToARGB32() < b.ToARGB32(); + } +}; + +class BBox { +public: + Vector minp; + Vector maxp; + + static BBox From(const Vector &p0, const Vector &p1); + + Vector GetOrigin() const; + Vector GetExtents() const; + + void Include(const Vector &v, double r = 0.0); + bool Overlaps(const BBox &b1) const; + bool Contains(const Point2d &p, double r = 0.0) const; +}; + +#endif diff --git a/src/libslic3r/slvs/entity.cpp b/src/libslic3r/slvs/entity.cpp new file mode 100644 index 0000000000..4f4eabe165 --- /dev/null +++ b/src/libslic3r/slvs/entity.cpp @@ -0,0 +1,875 @@ +//----------------------------------------------------------------------------- +// The implementation of our entities in the symbolic algebra system, methods +// to return a symbolic representation of the entity (line by its endpoints, +// circle by center and radius, etc.). +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +const hEntity EntityBase::FREE_IN_3D = { 0 }; +const hEntity EntityBase::NO_ENTITY = { 0 }; + +bool EntityBase::HasVector() const { + switch(type) { + case Type::LINE_SEGMENT: + case Type::NORMAL_IN_3D: + case Type::NORMAL_IN_2D: + case Type::NORMAL_N_COPY: + case Type::NORMAL_N_ROT: + case Type::NORMAL_N_ROT_AA: + return true; + + default: + return false; + } +} + +ExprVector EntityBase::VectorGetExprsInWorkplane(hEntity wrkpl) const { + switch(type) { + case Type::LINE_SEGMENT: + return (SK.GetEntity(point[0])->PointGetExprsInWorkplane(wrkpl)).Minus( + SK.GetEntity(point[1])->PointGetExprsInWorkplane(wrkpl)); + + case Type::NORMAL_IN_3D: + case Type::NORMAL_IN_2D: + case Type::NORMAL_N_COPY: + case Type::NORMAL_N_ROT: + case Type::NORMAL_N_ROT_AA: { + ExprVector ev = NormalExprsN(); + if(wrkpl.v == EntityBase::FREE_IN_3D.v) { + return ev; + } + // Get the offset and basis vectors for this weird exotic csys. + EntityBase *w = SK.GetEntity(wrkpl); + ExprVector wu = w->Normal()->NormalExprsU(); + ExprVector wv = w->Normal()->NormalExprsV(); + + // Get our coordinates in three-space, and project them into that + // coordinate system. + ExprVector result; + result.x = ev.Dot(wu); + result.y = ev.Dot(wv); + result.z = Expr::From(0.0); + return result; + } + default: ssassert(false, "Unexpected entity type"); + } +} + +ExprVector EntityBase::VectorGetExprs() const { + return VectorGetExprsInWorkplane(EntityBase::FREE_IN_3D); +} + +Vector EntityBase::VectorGetNum() const { + switch(type) { + case Type::LINE_SEGMENT: + return (SK.GetEntity(point[0])->PointGetNum()).Minus( + SK.GetEntity(point[1])->PointGetNum()); + + case Type::NORMAL_IN_3D: + case Type::NORMAL_IN_2D: + case Type::NORMAL_N_COPY: + case Type::NORMAL_N_ROT: + case Type::NORMAL_N_ROT_AA: + return NormalN(); + + default: ssassert(false, "Unexpected entity type"); + } +} + +Vector EntityBase::VectorGetRefPoint() const { + switch(type) { + case Type::LINE_SEGMENT: + return ((SK.GetEntity(point[0])->PointGetNum()).Plus( + SK.GetEntity(point[1])->PointGetNum())).ScaledBy(0.5); + + case Type::NORMAL_IN_3D: + case Type::NORMAL_IN_2D: + case Type::NORMAL_N_COPY: + case Type::NORMAL_N_ROT: + case Type::NORMAL_N_ROT_AA: + return SK.GetEntity(point[0])->PointGetNum(); + + default: ssassert(false, "Unexpected entity type"); + } +} + +Vector EntityBase::VectorGetStartPoint() const { + switch(type) { + case Type::LINE_SEGMENT: + return SK.GetEntity(point[1])->PointGetNum(); + + case Type::NORMAL_IN_3D: + case Type::NORMAL_IN_2D: + case Type::NORMAL_N_COPY: + case Type::NORMAL_N_ROT: + case Type::NORMAL_N_ROT_AA: + return SK.GetEntity(point[0])->PointGetNum(); + + default: ssassert(false, "Unexpected entity type"); + } +} + +bool EntityBase::IsCircle() const { + return (type == Type::CIRCLE) || (type == Type::ARC_OF_CIRCLE); +} + +Expr *EntityBase::CircleGetRadiusExpr() const { + if(type == Type::CIRCLE) { + return SK.GetEntity(distance)->DistanceGetExpr(); + } else if(type == Type::ARC_OF_CIRCLE) { + return Constraint::Distance(workplane, point[0], point[1]); + } else ssassert(false, "Unexpected entity type"); +} + +double EntityBase::CircleGetRadiusNum() const { + if(type == Type::CIRCLE) { + return SK.GetEntity(distance)->DistanceGetNum(); + } else if(type == Type::ARC_OF_CIRCLE) { + Vector c = SK.GetEntity(point[0])->PointGetNum(); + Vector pa = SK.GetEntity(point[1])->PointGetNum(); + return (pa.Minus(c)).Magnitude(); + } else ssassert(false, "Unexpected entity type"); +} + +void EntityBase::ArcGetAngles(double *thetaa, double *thetab, double *dtheta) const { + ssassert(type == Type::ARC_OF_CIRCLE, "Unexpected entity type"); + + Quaternion q = Normal()->NormalGetNum(); + Vector u = q.RotationU(), v = q.RotationV(); + + Vector c = SK.GetEntity(point[0])->PointGetNum(); + Vector pa = SK.GetEntity(point[1])->PointGetNum(); + Vector pb = SK.GetEntity(point[2])->PointGetNum(); + + Point2d c2 = c.Project2d(u, v); + Point2d pa2 = (pa.Project2d(u, v)).Minus(c2); + Point2d pb2 = (pb.Project2d(u, v)).Minus(c2); + + *thetaa = atan2(pa2.y, pa2.x); + *thetab = atan2(pb2.y, pb2.x); + *dtheta = *thetab - *thetaa; + // If the endpoints are coincident, call it a full arc, not a zero arc; + // useful concept to have when splitting + while(*dtheta < 1e-6) *dtheta += 2*PI; + while(*dtheta > (2*PI)) *dtheta -= 2*PI; +} + +Vector EntityBase::CubicGetStartNum() const { + return SK.GetEntity(point[0])->PointGetNum(); +} +Vector EntityBase::CubicGetFinishNum() const { + return SK.GetEntity(point[3+extraPoints])->PointGetNum(); +} +ExprVector EntityBase::CubicGetStartTangentExprs() const { + ExprVector pon = SK.GetEntity(point[0])->PointGetExprs(), + poff = SK.GetEntity(point[1])->PointGetExprs(); + return (pon.Minus(poff)); +} +ExprVector EntityBase::CubicGetFinishTangentExprs() const { + ExprVector pon = SK.GetEntity(point[3+extraPoints])->PointGetExprs(), + poff = SK.GetEntity(point[2+extraPoints])->PointGetExprs(); + return (pon.Minus(poff)); +} +Vector EntityBase::CubicGetStartTangentNum() const { + Vector pon = SK.GetEntity(point[0])->PointGetNum(), + poff = SK.GetEntity(point[1])->PointGetNum(); + return (pon.Minus(poff)); +} +Vector EntityBase::CubicGetFinishTangentNum() const { + Vector pon = SK.GetEntity(point[3+extraPoints])->PointGetNum(), + poff = SK.GetEntity(point[2+extraPoints])->PointGetNum(); + return (pon.Minus(poff)); +} + +bool EntityBase::IsWorkplane() const { + return (type == Type::WORKPLANE); +} + +ExprVector EntityBase::WorkplaneGetOffsetExprs() const { + return SK.GetEntity(point[0])->PointGetExprs(); +} + +Vector EntityBase::WorkplaneGetOffset() const { + return SK.GetEntity(point[0])->PointGetNum(); +} + +void EntityBase::WorkplaneGetPlaneExprs(ExprVector *n, Expr **dn) const { + if(type == Type::WORKPLANE) { + *n = Normal()->NormalExprsN(); + + ExprVector p0 = SK.GetEntity(point[0])->PointGetExprs(); + // The plane is n dot (p - p0) = 0, or + // n dot p - n dot p0 = 0 + // so dn = n dot p0 + *dn = p0.Dot(*n); + } else ssassert(false, "Unexpected entity type"); +} + +bool EntityBase::IsDistance() const { + return (type == Type::DISTANCE) || + (type == Type::DISTANCE_N_COPY); +} +double EntityBase::DistanceGetNum() const { + if(type == Type::DISTANCE) { + return SK.GetParam(param[0])->val; + } else if(type == Type::DISTANCE_N_COPY) { + return numDistance; + } else ssassert(false, "Unexpected entity type"); +} +Expr *EntityBase::DistanceGetExpr() const { + if(type == Type::DISTANCE) { + return Expr::From(param[0]); + } else if(type == Type::DISTANCE_N_COPY) { + return Expr::From(numDistance); + } else ssassert(false, "Unexpected entity type"); +} +void EntityBase::DistanceForceTo(double v) { + if(type == Type::DISTANCE) { + (SK.GetParam(param[0]))->val = v; + } else if(type == Type::DISTANCE_N_COPY) { + // do nothing, it's locked + } else ssassert(false, "Unexpected entity type"); +} + +EntityBase *EntityBase::Normal() const { + return SK.GetEntity(normal); +} + +bool EntityBase::IsPoint() const { + switch(type) { + case Type::POINT_IN_3D: + case Type::POINT_IN_2D: + case Type::POINT_N_COPY: + case Type::POINT_N_TRANS: + case Type::POINT_N_ROT_TRANS: + case Type::POINT_N_ROT_AA: + return true; + + default: + return false; + } +} + +bool EntityBase::IsNormal() const { + switch(type) { + case Type::NORMAL_IN_3D: + case Type::NORMAL_IN_2D: + case Type::NORMAL_N_COPY: + case Type::NORMAL_N_ROT: + case Type::NORMAL_N_ROT_AA: + return true; + + default: return false; + } +} + +Quaternion EntityBase::NormalGetNum() const { + Quaternion q; + switch(type) { + case Type::NORMAL_IN_3D: + q = Quaternion::From(param[0], param[1], param[2], param[3]); + break; + + case Type::NORMAL_IN_2D: { + EntityBase *wrkpl = SK.GetEntity(workplane); + EntityBase *norm = SK.GetEntity(wrkpl->normal); + q = norm->NormalGetNum(); + break; + } + case Type::NORMAL_N_COPY: + q = numNormal; + break; + + case Type::NORMAL_N_ROT: + q = Quaternion::From(param[0], param[1], param[2], param[3]); + q = q.Times(numNormal); + break; + + case Type::NORMAL_N_ROT_AA: { + q = GetAxisAngleQuaternion(0); + q = q.Times(numNormal); + break; + } + + default: ssassert(false, "Unexpected entity type"); + } + return q; +} + +void EntityBase::NormalForceTo(Quaternion q) { + switch(type) { + case Type::NORMAL_IN_3D: + SK.GetParam(param[0])->val = q.w; + SK.GetParam(param[1])->val = q.vx; + SK.GetParam(param[2])->val = q.vy; + SK.GetParam(param[3])->val = q.vz; + break; + + case Type::NORMAL_IN_2D: + case Type::NORMAL_N_COPY: + // There's absolutely nothing to do; these are locked. + break; + case Type::NORMAL_N_ROT: { + Quaternion qp = q.Times(numNormal.Inverse()); + + SK.GetParam(param[0])->val = qp.w; + SK.GetParam(param[1])->val = qp.vx; + SK.GetParam(param[2])->val = qp.vy; + SK.GetParam(param[3])->val = qp.vz; + break; + } + + case Type::NORMAL_N_ROT_AA: + // Not sure if I'll bother implementing this one + break; + + default: ssassert(false, "Unexpected entity type"); + } +} + +Vector EntityBase::NormalU() const { + return NormalGetNum().RotationU(); +} +Vector EntityBase::NormalV() const { + return NormalGetNum().RotationV(); +} +Vector EntityBase::NormalN() const { + return NormalGetNum().RotationN(); +} + +ExprVector EntityBase::NormalExprsU() const { + return NormalGetExprs().RotationU(); +} +ExprVector EntityBase::NormalExprsV() const { + return NormalGetExprs().RotationV(); +} +ExprVector EntityBase::NormalExprsN() const { + return NormalGetExprs().RotationN(); +} + +ExprQuaternion EntityBase::NormalGetExprs() const { + ExprQuaternion q; + switch(type) { + case Type::NORMAL_IN_3D: + q = ExprQuaternion::From(param[0], param[1], param[2], param[3]); + break; + + case Type::NORMAL_IN_2D: { + EntityBase *wrkpl = SK.GetEntity(workplane); + EntityBase *norm = SK.GetEntity(wrkpl->normal); + q = norm->NormalGetExprs(); + break; + } + case Type::NORMAL_N_COPY: + q = ExprQuaternion::From(numNormal); + break; + + case Type::NORMAL_N_ROT: { + ExprQuaternion orig = ExprQuaternion::From(numNormal); + q = ExprQuaternion::From(param[0], param[1], param[2], param[3]); + + q = q.Times(orig); + break; + } + + case Type::NORMAL_N_ROT_AA: { + ExprQuaternion orig = ExprQuaternion::From(numNormal); + q = GetAxisAngleQuaternionExprs(0); + q = q.Times(orig); + break; + } + + default: ssassert(false, "Unexpected entity type"); + } + return q; +} + +void EntityBase::PointForceParamTo(Vector p) { + switch(type) { + case Type::POINT_IN_3D: + SK.GetParam(param[0])->val = p.x; + SK.GetParam(param[1])->val = p.y; + SK.GetParam(param[2])->val = p.z; + break; + + case Type::POINT_IN_2D: + SK.GetParam(param[0])->val = p.x; + SK.GetParam(param[1])->val = p.y; + break; + + default: ssassert(false, "Unexpected entity type"); + } +} + +void EntityBase::PointForceTo(Vector p) { + switch(type) { + case Type::POINT_IN_3D: + SK.GetParam(param[0])->val = p.x; + SK.GetParam(param[1])->val = p.y; + SK.GetParam(param[2])->val = p.z; + break; + + case Type::POINT_IN_2D: { + EntityBase *c = SK.GetEntity(workplane); + p = p.Minus(c->WorkplaneGetOffset()); + SK.GetParam(param[0])->val = p.Dot(c->Normal()->NormalU()); + SK.GetParam(param[1])->val = p.Dot(c->Normal()->NormalV()); + break; + } + + case Type::POINT_N_TRANS: { + if(timesApplied == 0) break; + Vector trans = (p.Minus(numPoint)).ScaledBy(1.0/timesApplied); + SK.GetParam(param[0])->val = trans.x; + SK.GetParam(param[1])->val = trans.y; + SK.GetParam(param[2])->val = trans.z; + break; + } + + case Type::POINT_N_ROT_TRANS: { + // Force only the translation; leave the rotation unchanged. But + // remember that we're working with respect to the rotated + // point. + Vector trans = p.Minus(PointGetQuaternion().Rotate(numPoint)); + SK.GetParam(param[0])->val = trans.x; + SK.GetParam(param[1])->val = trans.y; + SK.GetParam(param[2])->val = trans.z; + break; + } + + case Type::POINT_N_ROT_AA: { + // Force only the angle; the axis and center of rotation stay + Vector offset = Vector::From(param[0], param[1], param[2]); + Vector normal = Vector::From(param[4], param[5], param[6]); + Vector u = normal.Normal(0), v = normal.Normal(1); + Vector po = p.Minus(offset), numo = numPoint.Minus(offset); + double thetap = atan2(v.Dot(po), u.Dot(po)); + double thetan = atan2(v.Dot(numo), u.Dot(numo)); + double thetaf = (thetap - thetan); + double thetai = (SK.GetParam(param[3])->val)*timesApplied*2; + double dtheta = thetaf - thetai; + // Take the smallest possible change in the actual step angle, + // in order to avoid jumps when you cross from +pi to -pi + while(dtheta < -PI) dtheta += 2*PI; + while(dtheta > PI) dtheta -= 2*PI; + SK.GetParam(param[3])->val = (thetai + dtheta)/(timesApplied*2); + break; + } + + case Type::POINT_N_COPY: + // Nothing to do; it's a static copy + break; + + default: ssassert(false, "Unexpected entity type"); + } +} + +Vector EntityBase::PointGetNum() const { + Vector p; + switch(type) { + case Type::POINT_IN_3D: + p = Vector::From(param[0], param[1], param[2]); + break; + + case Type::POINT_IN_2D: { + EntityBase *c = SK.GetEntity(workplane); + Vector u = c->Normal()->NormalU(); + Vector v = c->Normal()->NormalV(); + p = u.ScaledBy(SK.GetParam(param[0])->val); + p = p.Plus(v.ScaledBy(SK.GetParam(param[1])->val)); + p = p.Plus(c->WorkplaneGetOffset()); + break; + } + + case Type::POINT_N_TRANS: { + Vector trans = Vector::From(param[0], param[1], param[2]); + p = numPoint.Plus(trans.ScaledBy(timesApplied)); + break; + } + + case Type::POINT_N_ROT_TRANS: { + Vector offset = Vector::From(param[0], param[1], param[2]); + Quaternion q = PointGetQuaternion(); + p = q.Rotate(numPoint); + p = p.Plus(offset); + break; + } + + case Type::POINT_N_ROT_AA: { + Vector offset = Vector::From(param[0], param[1], param[2]); + Quaternion q = PointGetQuaternion(); + p = numPoint.Minus(offset); + p = q.Rotate(p); + p = p.Plus(offset); + break; + } + + case Type::POINT_N_COPY: + p = numPoint; + break; + + default: ssassert(false, "Unexpected entity type"); + } + return p; +} + +ExprVector EntityBase::PointGetExprs() const { + ExprVector r; + switch(type) { + case Type::POINT_IN_3D: + r = ExprVector::From(param[0], param[1], param[2]); + break; + + case Type::POINT_IN_2D: { + EntityBase *c = SK.GetEntity(workplane); + ExprVector u = c->Normal()->NormalExprsU(); + ExprVector v = c->Normal()->NormalExprsV(); + r = c->WorkplaneGetOffsetExprs(); + r = r.Plus(u.ScaledBy(Expr::From(param[0]))); + r = r.Plus(v.ScaledBy(Expr::From(param[1]))); + break; + } + case Type::POINT_N_TRANS: { + ExprVector orig = ExprVector::From(numPoint); + ExprVector trans = ExprVector::From(param[0], param[1], param[2]); + r = orig.Plus(trans.ScaledBy(Expr::From(timesApplied))); + break; + } + case Type::POINT_N_ROT_TRANS: { + ExprVector orig = ExprVector::From(numPoint); + ExprVector trans = ExprVector::From(param[0], param[1], param[2]); + ExprQuaternion q = + ExprQuaternion::From(param[3], param[4], param[5], param[6]); + orig = q.Rotate(orig); + r = orig.Plus(trans); + break; + } + case Type::POINT_N_ROT_AA: { + ExprVector orig = ExprVector::From(numPoint); + ExprVector trans = ExprVector::From(param[0], param[1], param[2]); + ExprQuaternion q = GetAxisAngleQuaternionExprs(3); + orig = orig.Minus(trans); + orig = q.Rotate(orig); + r = orig.Plus(trans); + break; + } + case Type::POINT_N_COPY: + r = ExprVector::From(numPoint); + break; + + default: ssassert(false, "Unexpected entity type"); + } + return r; +} + +void EntityBase::PointGetExprsInWorkplane(hEntity wrkpl, Expr **u, Expr **v) const { + if(type == Type::POINT_IN_2D && workplane.v == wrkpl.v) { + // They want our coordinates in the form that we've written them, + // very nice. + *u = Expr::From(param[0]); + *v = Expr::From(param[1]); + } else { + // Get the offset and basis vectors for this weird exotic csys. + EntityBase *w = SK.GetEntity(wrkpl); + ExprVector wp = w->WorkplaneGetOffsetExprs(); + ExprVector wu = w->Normal()->NormalExprsU(); + ExprVector wv = w->Normal()->NormalExprsV(); + + // Get our coordinates in three-space, and project them into that + // coordinate system. + ExprVector ev = PointGetExprs(); + ev = ev.Minus(wp); + *u = ev.Dot(wu); + *v = ev.Dot(wv); + } +} + +ExprVector EntityBase::PointGetExprsInWorkplane(hEntity wrkpl) const { + if(wrkpl.v == Entity::FREE_IN_3D.v) { + return PointGetExprs(); + } + + ExprVector r; + PointGetExprsInWorkplane(wrkpl, &r.x, &r.y); + r.z = Expr::From(0.0); + return r; +} + +void EntityBase::PointForceQuaternionTo(Quaternion q) { + ssassert(type == Type::POINT_N_ROT_TRANS, "Unexpected entity type"); + + SK.GetParam(param[3])->val = q.w; + SK.GetParam(param[4])->val = q.vx; + SK.GetParam(param[5])->val = q.vy; + SK.GetParam(param[6])->val = q.vz; +} + +Quaternion EntityBase::GetAxisAngleQuaternion(int param0) const { + Quaternion q; + double theta = timesApplied*SK.GetParam(param[param0+0])->val; + double s = sin(theta), c = cos(theta); + q.w = c; + q.vx = s*SK.GetParam(param[param0+1])->val; + q.vy = s*SK.GetParam(param[param0+2])->val; + q.vz = s*SK.GetParam(param[param0+3])->val; + return q; +} + +ExprQuaternion EntityBase::GetAxisAngleQuaternionExprs(int param0) const { + ExprQuaternion q; + + Expr *theta = Expr::From(timesApplied)->Times( + Expr::From(param[param0+0])); + Expr *c = theta->Cos(), *s = theta->Sin(); + q.w = c; + q.vx = s->Times(Expr::From(param[param0+1])); + q.vy = s->Times(Expr::From(param[param0+2])); + q.vz = s->Times(Expr::From(param[param0+3])); + return q; +} + +Quaternion EntityBase::PointGetQuaternion() const { + Quaternion q; + + if(type == Type::POINT_N_ROT_AA) { + q = GetAxisAngleQuaternion(3); + } else if(type == Type::POINT_N_ROT_TRANS) { + q = Quaternion::From(param[3], param[4], param[5], param[6]); + } else ssassert(false, "Unexpected entity type"); + + return q; +} + +bool EntityBase::IsFace() const { + switch(type) { + case Type::FACE_NORMAL_PT: + case Type::FACE_XPROD: + case Type::FACE_N_ROT_TRANS: + case Type::FACE_N_TRANS: + case Type::FACE_N_ROT_AA: + return true; + default: + return false; + } +} + +ExprVector EntityBase::FaceGetNormalExprs() const { + ExprVector r; + if(type == Type::FACE_NORMAL_PT) { + Vector v = Vector::From(numNormal.vx, numNormal.vy, numNormal.vz); + r = ExprVector::From(v.WithMagnitude(1)); + } else if(type == Type::FACE_XPROD) { + ExprVector vc = ExprVector::From(param[0], param[1], param[2]); + ExprVector vn = + ExprVector::From(numNormal.vx, numNormal.vy, numNormal.vz); + r = vc.Cross(vn); + r = r.WithMagnitude(Expr::From(1.0)); + } else if(type == Type::FACE_N_ROT_TRANS) { + // The numerical normal vector gets the rotation; the numerical + // normal has magnitude one, and the rotation doesn't change that, + // so there's no need to fix it up. + r = ExprVector::From(numNormal.vx, numNormal.vy, numNormal.vz); + ExprQuaternion q = + ExprQuaternion::From(param[3], param[4], param[5], param[6]); + r = q.Rotate(r); + } else if(type == Type::FACE_N_TRANS) { + r = ExprVector::From(numNormal.vx, numNormal.vy, numNormal.vz); + } else if(type == Type::FACE_N_ROT_AA) { + r = ExprVector::From(numNormal.vx, numNormal.vy, numNormal.vz); + ExprQuaternion q = GetAxisAngleQuaternionExprs(3); + r = q.Rotate(r); + } else ssassert(false, "Unexpected entity type"); + return r; +} + +Vector EntityBase::FaceGetNormalNum() const { + Vector r; + if(type == Type::FACE_NORMAL_PT) { + r = Vector::From(numNormal.vx, numNormal.vy, numNormal.vz); + } else if(type == Type::FACE_XPROD) { + Vector vc = Vector::From(param[0], param[1], param[2]); + Vector vn = Vector::From(numNormal.vx, numNormal.vy, numNormal.vz); + r = vc.Cross(vn); + } else if(type == Type::FACE_N_ROT_TRANS) { + // The numerical normal vector gets the rotation + r = Vector::From(numNormal.vx, numNormal.vy, numNormal.vz); + Quaternion q = Quaternion::From(param[3], param[4], param[5], param[6]); + r = q.Rotate(r); + } else if(type == Type::FACE_N_TRANS) { + r = Vector::From(numNormal.vx, numNormal.vy, numNormal.vz); + } else if(type == Type::FACE_N_ROT_AA) { + r = Vector::From(numNormal.vx, numNormal.vy, numNormal.vz); + Quaternion q = GetAxisAngleQuaternion(3); + r = q.Rotate(r); + } else ssassert(false, "Unexpected entity type"); + return r.WithMagnitude(1); +} + +ExprVector EntityBase::FaceGetPointExprs() const { + ExprVector r; + if(type == Type::FACE_NORMAL_PT) { + r = SK.GetEntity(point[0])->PointGetExprs(); + } else if(type == Type::FACE_XPROD) { + r = ExprVector::From(numPoint); + } else if(type == Type::FACE_N_ROT_TRANS) { + // The numerical point gets the rotation and translation. + ExprVector trans = ExprVector::From(param[0], param[1], param[2]); + ExprQuaternion q = + ExprQuaternion::From(param[3], param[4], param[5], param[6]); + r = ExprVector::From(numPoint); + r = q.Rotate(r); + r = r.Plus(trans); + } else if(type == Type::FACE_N_TRANS) { + ExprVector trans = ExprVector::From(param[0], param[1], param[2]); + r = ExprVector::From(numPoint); + r = r.Plus(trans.ScaledBy(Expr::From(timesApplied))); + } else if(type == Type::FACE_N_ROT_AA) { + ExprVector trans = ExprVector::From(param[0], param[1], param[2]); + ExprQuaternion q = GetAxisAngleQuaternionExprs(3); + r = ExprVector::From(numPoint); + r = r.Minus(trans); + r = q.Rotate(r); + r = r.Plus(trans); + } else ssassert(false, "Unexpected entity type"); + return r; +} + +Vector EntityBase::FaceGetPointNum() const { + Vector r; + if(type == Type::FACE_NORMAL_PT) { + r = SK.GetEntity(point[0])->PointGetNum(); + } else if(type == Type::FACE_XPROD) { + r = numPoint; + } else if(type == Type::FACE_N_ROT_TRANS) { + // The numerical point gets the rotation and translation. + Vector trans = Vector::From(param[0], param[1], param[2]); + Quaternion q = Quaternion::From(param[3], param[4], param[5], param[6]); + r = q.Rotate(numPoint); + r = r.Plus(trans); + } else if(type == Type::FACE_N_TRANS) { + Vector trans = Vector::From(param[0], param[1], param[2]); + r = numPoint.Plus(trans.ScaledBy(timesApplied)); + } else if(type == Type::FACE_N_ROT_AA) { + Vector trans = Vector::From(param[0], param[1], param[2]); + Quaternion q = GetAxisAngleQuaternion(3); + r = numPoint.Minus(trans); + r = q.Rotate(r); + r = r.Plus(trans); + } else ssassert(false, "Unexpected entity type"); + return r; +} + +bool EntityBase::HasEndpoints() const { + return (type == Type::LINE_SEGMENT) || + (type == Type::CUBIC) || + (type == Type::ARC_OF_CIRCLE); +} +Vector EntityBase::EndpointStart() const { + if(type == Type::LINE_SEGMENT) { + return SK.GetEntity(point[0])->PointGetNum(); + } else if(type == Type::CUBIC) { + return CubicGetStartNum(); + } else if(type == Type::ARC_OF_CIRCLE) { + return SK.GetEntity(point[1])->PointGetNum(); + } else ssassert(false, "Unexpected entity type"); +} +Vector EntityBase::EndpointFinish() const { + if(type == Type::LINE_SEGMENT) { + return SK.GetEntity(point[1])->PointGetNum(); + } else if(type == Type::CUBIC) { + return CubicGetFinishNum(); + } else if(type == Type::ARC_OF_CIRCLE) { + return SK.GetEntity(point[2])->PointGetNum(); + } else ssassert(false, "Unexpected entity type"); +} + +void EntityBase::RectGetPointsExprs(ExprVector *eb, ExprVector *ec) const { + ssassert(type == Type::TTF_TEXT || type == Type::IMAGE, + "Unexpected entity type"); + + EntityBase *a = SK.GetEntity(point[0]); + EntityBase *o = SK.GetEntity(point[1]); + + // Write equations for each point in the current workplane. + // This reduces the complexity of resulting equations. + ExprVector ea = a->PointGetExprsInWorkplane(workplane); + ExprVector eo = o->PointGetExprsInWorkplane(workplane); + + // Take perpendicular vector and scale it by aspect ratio. + ExprVector eu = ea.Minus(eo); + ExprVector ev = ExprVector::From(eu.y, eu.x->Negate(), eu.z).ScaledBy(Expr::From(aspectRatio)); + + *eb = eo.Plus(ev); + *ec = eo.Plus(eu).Plus(ev); +} + +void EntityBase::AddEq(IdList *l, Expr *expr, int index) const { + Equation eq; + eq.e = expr; + eq.h = h.equation(index); + l->Add(&eq); +} + +void EntityBase::GenerateEquations(IdList *l) const { + switch(type) { + case Type::NORMAL_IN_3D: { + ExprQuaternion q = NormalGetExprs(); + AddEq(l, (q.Magnitude())->Minus(Expr::From(1)), 0); + break; + } + + case Type::ARC_OF_CIRCLE: { + // If this is a copied entity, with its point already fixed + // with respect to each other, then we don't want to generate + // the distance constraint! + if(SK.GetEntity(point[0])->type != Type::POINT_IN_2D) break; + + // If the two endpoints of the arc are constrained coincident + // (to make a complete circle), then our distance constraint + // would be redundant and therefore overconstrain things. + int i; + for(i = 0; i < SK.constraint.n; i++) { + ConstraintBase *c = &(SK.constraint.elem[i]); + if(c->group.v != group.v) continue; + if(c->type != Constraint::Type::POINTS_COINCIDENT) continue; + + if((c->ptA.v == point[1].v && c->ptB.v == point[2].v) || + (c->ptA.v == point[2].v && c->ptB.v == point[1].v)) + { + break; + } + } + if(i < SK.constraint.n) break; + + Expr *ra = Constraint::Distance(workplane, point[0], point[1]); + Expr *rb = Constraint::Distance(workplane, point[0], point[2]); + AddEq(l, ra->Minus(rb), 0); + break; + } + + case Type::IMAGE: + case Type::TTF_TEXT: { + if(SK.GetEntity(point[0])->type != Type::POINT_IN_2D) break; + EntityBase *b = SK.GetEntity(point[2]); + EntityBase *c = SK.GetEntity(point[3]); + ExprVector eb = b->PointGetExprsInWorkplane(workplane); + ExprVector ec = c->PointGetExprsInWorkplane(workplane); + + ExprVector ebp, ecp; + RectGetPointsExprs(&ebp, &ecp); + + ExprVector beq = eb.Minus(ebp); + AddEq(l, beq.x, 0); + AddEq(l, beq.y, 1); + ExprVector ceq = ec.Minus(ecp); + AddEq(l, ceq.x, 2); + AddEq(l, ceq.y, 3); + break; + } + + default: // Most entities do not generate equations. + break; + } +} diff --git a/src/libslic3r/slvs/expr.cpp b/src/libslic3r/slvs/expr.cpp new file mode 100644 index 0000000000..cbff038515 --- /dev/null +++ b/src/libslic3r/slvs/expr.cpp @@ -0,0 +1,917 @@ +//----------------------------------------------------------------------------- +// The symbolic algebra system used to write our constraint equations; +// routines to build expressions in software or from a user-provided string, +// and to compute the partial derivatives that we'll use when write our +// Jacobian matrix. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +ExprVector ExprVector::From(Expr *x, Expr *y, Expr *z) { + ExprVector r = { x, y, z}; + return r; +} + +ExprVector ExprVector::From(Vector vn) { + ExprVector ve; + ve.x = Expr::From(vn.x); + ve.y = Expr::From(vn.y); + ve.z = Expr::From(vn.z); + return ve; +} + +ExprVector ExprVector::From(hParam x, hParam y, hParam z) { + ExprVector ve; + ve.x = Expr::From(x); + ve.y = Expr::From(y); + ve.z = Expr::From(z); + return ve; +} + +ExprVector ExprVector::From(double x, double y, double z) { + ExprVector ve; + ve.x = Expr::From(x); + ve.y = Expr::From(y); + ve.z = Expr::From(z); + return ve; +} + +ExprVector ExprVector::Minus(ExprVector b) const { + ExprVector r; + r.x = x->Minus(b.x); + r.y = y->Minus(b.y); + r.z = z->Minus(b.z); + return r; +} + +ExprVector ExprVector::Plus(ExprVector b) const { + ExprVector r; + r.x = x->Plus(b.x); + r.y = y->Plus(b.y); + r.z = z->Plus(b.z); + return r; +} + +Expr *ExprVector::Dot(ExprVector b) const { + Expr *r; + r = x->Times(b.x); + r = r->Plus(y->Times(b.y)); + r = r->Plus(z->Times(b.z)); + return r; +} + +ExprVector ExprVector::Cross(ExprVector b) const { + ExprVector r; + r.x = (y->Times(b.z))->Minus(z->Times(b.y)); + r.y = (z->Times(b.x))->Minus(x->Times(b.z)); + r.z = (x->Times(b.y))->Minus(y->Times(b.x)); + return r; +} + +ExprVector ExprVector::ScaledBy(Expr *s) const { + ExprVector r; + r.x = x->Times(s); + r.y = y->Times(s); + r.z = z->Times(s); + return r; +} + +ExprVector ExprVector::WithMagnitude(Expr *s) const { + Expr *m = Magnitude(); + return ScaledBy(s->Div(m)); +} + +Expr *ExprVector::Magnitude() const { + Expr *r; + r = x->Square(); + r = r->Plus(y->Square()); + r = r->Plus(z->Square()); + return r->Sqrt(); +} + +Vector ExprVector::Eval() const { + Vector r; + r.x = x->Eval(); + r.y = y->Eval(); + r.z = z->Eval(); + return r; +} + +ExprQuaternion ExprQuaternion::From(hParam w, hParam vx, hParam vy, hParam vz) { + ExprQuaternion q; + q.w = Expr::From(w); + q.vx = Expr::From(vx); + q.vy = Expr::From(vy); + q.vz = Expr::From(vz); + return q; +} + +ExprQuaternion ExprQuaternion::From(Expr *w, Expr *vx, Expr *vy, Expr *vz) +{ + ExprQuaternion q; + q.w = w; + q.vx = vx; + q.vy = vy; + q.vz = vz; + return q; +} + +ExprQuaternion ExprQuaternion::From(Quaternion qn) { + ExprQuaternion qe; + qe.w = Expr::From(qn.w); + qe.vx = Expr::From(qn.vx); + qe.vy = Expr::From(qn.vy); + qe.vz = Expr::From(qn.vz); + return qe; +} + +ExprVector ExprQuaternion::RotationU() const { + ExprVector u; + Expr *two = Expr::From(2); + + u.x = w->Square(); + u.x = (u.x)->Plus(vx->Square()); + u.x = (u.x)->Minus(vy->Square()); + u.x = (u.x)->Minus(vz->Square()); + + u.y = two->Times(w->Times(vz)); + u.y = (u.y)->Plus(two->Times(vx->Times(vy))); + + u.z = two->Times(vx->Times(vz)); + u.z = (u.z)->Minus(two->Times(w->Times(vy))); + + return u; +} + +ExprVector ExprQuaternion::RotationV() const { + ExprVector v; + Expr *two = Expr::From(2); + + v.x = two->Times(vx->Times(vy)); + v.x = (v.x)->Minus(two->Times(w->Times(vz))); + + v.y = w->Square(); + v.y = (v.y)->Minus(vx->Square()); + v.y = (v.y)->Plus(vy->Square()); + v.y = (v.y)->Minus(vz->Square()); + + v.z = two->Times(w->Times(vx)); + v.z = (v.z)->Plus(two->Times(vy->Times(vz))); + + return v; +} + +ExprVector ExprQuaternion::RotationN() const { + ExprVector n; + Expr *two = Expr::From(2); + + n.x = two->Times( w->Times(vy)); + n.x = (n.x)->Plus (two->Times(vx->Times(vz))); + + n.y = two->Times(vy->Times(vz)); + n.y = (n.y)->Minus(two->Times( w->Times(vx))); + + n.z = w->Square(); + n.z = (n.z)->Minus(vx->Square()); + n.z = (n.z)->Minus(vy->Square()); + n.z = (n.z)->Plus (vz->Square()); + + return n; +} + +ExprVector ExprQuaternion::Rotate(ExprVector p) const { + // Express the point in the new basis + return (RotationU().ScaledBy(p.x)).Plus( + RotationV().ScaledBy(p.y)).Plus( + RotationN().ScaledBy(p.z)); +} + +ExprQuaternion ExprQuaternion::Times(ExprQuaternion b) const { + Expr *sa = w, *sb = b.w; + ExprVector va = { vx, vy, vz }; + ExprVector vb = { b.vx, b.vy, b.vz }; + + ExprQuaternion r; + r.w = (sa->Times(sb))->Minus(va.Dot(vb)); + ExprVector vr = vb.ScaledBy(sa).Plus( + va.ScaledBy(sb).Plus( + va.Cross(vb))); + r.vx = vr.x; + r.vy = vr.y; + r.vz = vr.z; + return r; +} + +Expr *ExprQuaternion::Magnitude() const { + return ((w ->Square())->Plus( + (vx->Square())->Plus( + (vy->Square())->Plus( + (vz->Square())))))->Sqrt(); +} + + +Expr *Expr::From(hParam p) { + Expr *r = AllocExpr(); + r->op = Op::PARAM; + r->parh = p; + return r; +} + +Expr *Expr::From(double v) { + // Statically allocate common constants. + // Note: this is only valid because AllocExpr() uses AllocTemporary(), + // and Expr* is never explicitly freed. + + if(v == 0.0) { + static Expr zero(0.0); + return &zero; + } + + if(v == 1.0) { + static Expr one(1.0); + return &one; + } + + if(v == -1.0) { + static Expr mone(-1.0); + return &mone; + } + + if(v == 0.5) { + static Expr half(0.5); + return ½ + } + + if(v == -0.5) { + static Expr mhalf(-0.5); + return &mhalf; + } + + Expr *r = AllocExpr(); + r->op = Op::CONSTANT; + r->v = v; + return r; +} + +Expr *Expr::AnyOp(Op newOp, Expr *b) { + Expr *r = AllocExpr(); + r->op = newOp; + r->a = this; + r->b = b; + return r; +} + +int Expr::Children() const { + switch(op) { + case Op::PARAM: + case Op::PARAM_PTR: + case Op::CONSTANT: + case Op::VARIABLE: + return 0; + + case Op::PLUS: + case Op::MINUS: + case Op::TIMES: + case Op::DIV: + return 2; + + case Op::NEGATE: + case Op::SQRT: + case Op::SQUARE: + case Op::SIN: + case Op::COS: + case Op::ASIN: + case Op::ACOS: + return 1; + } + ssassert(false, "Unexpected operation"); +} + +int Expr::Nodes() const { + switch(Children()) { + case 0: return 1; + case 1: return 1 + a->Nodes(); + case 2: return 1 + a->Nodes() + b->Nodes(); + default: ssassert(false, "Unexpected children count"); + } +} + +Expr *Expr::DeepCopy() const { + Expr *n = AllocExpr(); + *n = *this; + int c = n->Children(); + if(c > 0) n->a = a->DeepCopy(); + if(c > 1) n->b = b->DeepCopy(); + return n; +} + +Expr *Expr::DeepCopyWithParamsAsPointers(IdList *firstTry, + IdList *thenTry) const +{ + Expr *n = AllocExpr(); + if(op == Op::PARAM) { + // A param that is referenced by its hParam gets rewritten to go + // straight in to the parameter table with a pointer, or simply + // into a constant if it's already known. + Param *p = firstTry->FindByIdNoOops(parh); + if(!p) p = thenTry->FindById(parh); + if(p->known) { + n->op = Op::CONSTANT; + n->v = p->val; + } else { + n->op = Op::PARAM_PTR; + n->parp = p; + } + return n; + } + + *n = *this; + int c = n->Children(); + if(c > 0) n->a = a->DeepCopyWithParamsAsPointers(firstTry, thenTry); + if(c > 1) n->b = b->DeepCopyWithParamsAsPointers(firstTry, thenTry); + return n; +} + +double Expr::Eval() const { + switch(op) { + case Op::PARAM: return SK.GetParam(parh)->val; + case Op::PARAM_PTR: return parp->val; + + case Op::CONSTANT: return v; + case Op::VARIABLE: ssassert(false, "Not supported yet"); + + case Op::PLUS: return a->Eval() + b->Eval(); + case Op::MINUS: return a->Eval() - b->Eval(); + case Op::TIMES: return a->Eval() * b->Eval(); + case Op::DIV: return a->Eval() / b->Eval(); + + case Op::NEGATE: return -(a->Eval()); + case Op::SQRT: return sqrt(a->Eval()); + case Op::SQUARE: { double r = a->Eval(); return r*r; } + case Op::SIN: return sin(a->Eval()); + case Op::COS: return cos(a->Eval()); + case Op::ACOS: return acos(a->Eval()); + case Op::ASIN: return asin(a->Eval()); + } + ssassert(false, "Unexpected operation"); +} + +Expr *Expr::PartialWrt(hParam p) const { + Expr *da, *db; + + switch(op) { + case Op::PARAM_PTR: return From(p.v == parp->h.v ? 1 : 0); + case Op::PARAM: return From(p.v == parh.v ? 1 : 0); + + case Op::CONSTANT: return From(0.0); + case Op::VARIABLE: ssassert(false, "Not supported yet"); + + case Op::PLUS: return (a->PartialWrt(p))->Plus(b->PartialWrt(p)); + case Op::MINUS: return (a->PartialWrt(p))->Minus(b->PartialWrt(p)); + + case Op::TIMES: + da = a->PartialWrt(p); + db = b->PartialWrt(p); + return (a->Times(db))->Plus(b->Times(da)); + + case Op::DIV: + da = a->PartialWrt(p); + db = b->PartialWrt(p); + return ((da->Times(b))->Minus(a->Times(db)))->Div(b->Square()); + + case Op::SQRT: + return (From(0.5)->Div(a->Sqrt()))->Times(a->PartialWrt(p)); + + case Op::SQUARE: + return (From(2.0)->Times(a))->Times(a->PartialWrt(p)); + + case Op::NEGATE: return (a->PartialWrt(p))->Negate(); + case Op::SIN: return (a->Cos())->Times(a->PartialWrt(p)); + case Op::COS: return ((a->Sin())->Times(a->PartialWrt(p)))->Negate(); + + case Op::ASIN: + return (From(1)->Div((From(1)->Minus(a->Square()))->Sqrt())) + ->Times(a->PartialWrt(p)); + case Op::ACOS: + return (From(-1)->Div((From(1)->Minus(a->Square()))->Sqrt())) + ->Times(a->PartialWrt(p)); + } + ssassert(false, "Unexpected operation"); +} + +uint64_t Expr::ParamsUsed() const { + uint64_t r = 0; + if(op == Op::PARAM) r |= ((uint64_t)1 << (parh.v % 61)); + if(op == Op::PARAM_PTR) r |= ((uint64_t)1 << (parp->h.v % 61)); + + int c = Children(); + if(c >= 1) r |= a->ParamsUsed(); + if(c >= 2) r |= b->ParamsUsed(); + return r; +} + +bool Expr::DependsOn(hParam p) const { + if(op == Op::PARAM) return (parh.v == p.v); + if(op == Op::PARAM_PTR) return (parp->h.v == p.v); + + int c = Children(); + if(c == 1) return a->DependsOn(p); + if(c == 2) return a->DependsOn(p) || b->DependsOn(p); + return false; +} + +bool Expr::Tol(double a, double b) { + return fabs(a - b) < 0.001; +} +Expr *Expr::FoldConstants() { + Expr *n = AllocExpr(); + *n = *this; + + int c = Children(); + if(c >= 1) n->a = a->FoldConstants(); + if(c >= 2) n->b = b->FoldConstants(); + + switch(op) { + case Op::PARAM_PTR: + case Op::PARAM: + case Op::CONSTANT: + case Op::VARIABLE: + break; + + case Op::MINUS: + case Op::TIMES: + case Op::DIV: + case Op::PLUS: + // If both ops are known, then we can evaluate immediately + if(n->a->op == Op::CONSTANT && n->b->op == Op::CONSTANT) { + double nv = n->Eval(); + n->op = Op::CONSTANT; + n->v = nv; + break; + } + // x + 0 = 0 + x = x + if(op == Op::PLUS && n->b->op == Op::CONSTANT && Tol(n->b->v, 0)) { + *n = *(n->a); break; + } + if(op == Op::PLUS && n->a->op == Op::CONSTANT && Tol(n->a->v, 0)) { + *n = *(n->b); break; + } + // 1*x = x*1 = x + if(op == Op::TIMES && n->b->op == Op::CONSTANT && Tol(n->b->v, 1)) { + *n = *(n->a); break; + } + if(op == Op::TIMES && n->a->op == Op::CONSTANT && Tol(n->a->v, 1)) { + *n = *(n->b); break; + } + // 0*x = x*0 = 0 + if(op == Op::TIMES && n->b->op == Op::CONSTANT && Tol(n->b->v, 0)) { + n->op = Op::CONSTANT; n->v = 0; break; + } + if(op == Op::TIMES && n->a->op == Op::CONSTANT && Tol(n->a->v, 0)) { + n->op = Op::CONSTANT; n->v = 0; break; + } + + break; + + case Op::SQRT: + case Op::SQUARE: + case Op::NEGATE: + case Op::SIN: + case Op::COS: + case Op::ASIN: + case Op::ACOS: + if(n->a->op == Op::CONSTANT) { + double nv = n->Eval(); + n->op = Op::CONSTANT; + n->v = nv; + } + break; + } + return n; +} + +void Expr::Substitute(hParam oldh, hParam newh) { + ssassert(op != Op::PARAM_PTR, "Expected an expression that refer to params via handles"); + + if(op == Op::PARAM && parh.v == oldh.v) { + parh = newh; + } + int c = Children(); + if(c >= 1) a->Substitute(oldh, newh); + if(c >= 2) b->Substitute(oldh, newh); +} + +//----------------------------------------------------------------------------- +// If the expression references only one parameter that appears in pl, then +// return that parameter. If no param is referenced, then return NO_PARAMS. +// If multiple params are referenced, then return MULTIPLE_PARAMS. +//----------------------------------------------------------------------------- +const hParam Expr::NO_PARAMS = { 0 }; +const hParam Expr::MULTIPLE_PARAMS = { 1 }; +hParam Expr::ReferencedParams(ParamList *pl) const { + if(op == Op::PARAM) { + if(pl->FindByIdNoOops(parh)) { + return parh; + } else { + return NO_PARAMS; + } + } + ssassert(op != Op::PARAM_PTR, "Expected an expression that refer to params via handles"); + + int c = Children(); + if(c == 0) { + return NO_PARAMS; + } else if(c == 1) { + return a->ReferencedParams(pl); + } else if(c == 2) { + hParam pa, pb; + pa = a->ReferencedParams(pl); + pb = b->ReferencedParams(pl); + if(pa.v == NO_PARAMS.v) { + return pb; + } else if(pb.v == NO_PARAMS.v) { + return pa; + } else if(pa.v == pb.v) { + return pa; // either, doesn't matter + } else { + return MULTIPLE_PARAMS; + } + } else ssassert(false, "Unexpected children count"); +} + + +//----------------------------------------------------------------------------- +// Routines to pretty-print an expression. Mostly for debugging. +//----------------------------------------------------------------------------- + +std::string Expr::Print() const { + char c; + switch(op) { + case Op::PARAM: return ssprintf("param(%08x)", parh.v); + case Op::PARAM_PTR: return ssprintf("param(p%08x)", parp->h.v); + + case Op::CONSTANT: return ssprintf("%.3f", v); + case Op::VARIABLE: return "(var)"; + + case Op::PLUS: c = '+'; goto p; + case Op::MINUS: c = '-'; goto p; + case Op::TIMES: c = '*'; goto p; + case Op::DIV: c = '/'; goto p; +p: + return "(" + a->Print() + " " + c + " " + b->Print() + ")"; + break; + + case Op::NEGATE: return "(- " + a->Print() + ")"; + case Op::SQRT: return "(sqrt " + a->Print() + ")"; + case Op::SQUARE: return "(square " + a->Print() + ")"; + case Op::SIN: return "(sin " + a->Print() + ")"; + case Op::COS: return "(cos " + a->Print() + ")"; + case Op::ASIN: return "(asin " + a->Print() + ")"; + case Op::ACOS: return "(acos " + a->Print() + ")"; + } + ssassert(false, "Unexpected operation"); +} + + +//----------------------------------------------------------------------------- +// A parser; convert a string to an expression. Infix notation, with the +// usual shift/reduce approach. I had great hopes for user-entered eq +// constraints, but those don't seem very useful, so right now this is just +// to provide calculator type functionality wherever numbers are entered. +//----------------------------------------------------------------------------- + +class ExprParser { +public: + enum class TokenType { + ERROR = 0, + + PAREN_LEFT, + PAREN_RIGHT, + BINARY_OP, + UNARY_OP, + OPERAND, + + END, + }; + + class Token { + public: + TokenType type; + Expr *expr; + + static Token From(TokenType type = TokenType::ERROR, Expr *expr = NULL); + static Token From(TokenType type, Expr::Op op); + bool IsError() const { return type == TokenType::ERROR; } + }; + + const char *input; + unsigned inputPos; + std::vector stack; + + char ReadChar(); + char PeekChar(); + + std::string ReadWord(); + void SkipSpace(); + + Token PopOperator(std::string *error); + Token PopOperand(std::string *error); + + int Precedence(Token token); + Token LexNumber(std::string *error); + Token Lex(std::string *error); + bool Reduce(std::string *error); + bool Parse(std::string *error, size_t reduceUntil = 0); + + static Expr *Parse(const char *input, std::string *error); +}; + +ExprParser::Token ExprParser::Token::From(TokenType type, Expr *expr) { + Token t; + t.type = type; + t.expr = expr; + return t; +} + +ExprParser::Token ExprParser::Token::From(TokenType type, Expr::Op op) { + Token t; + t.type = type; + t.expr = Expr::AllocExpr(); + t.expr->op = op; + return t; +} + +char ExprParser::ReadChar() { + return input[inputPos++]; +} + +char ExprParser::PeekChar() { + return input[inputPos]; +} + +std::string ExprParser::ReadWord() { + std::string s; + + while(char c = PeekChar()) { + if(!isalnum(c)) break; + s.push_back(ReadChar()); + } + + return s; +} + +void ExprParser::SkipSpace() { + while(char c = PeekChar()) { + if(!isspace(c)) break; + ReadChar(); + } +} + +ExprParser::Token ExprParser::LexNumber(std::string *error) { + std::string s; + + while(char c = PeekChar()) { + if(!((c >= '0' && c <= '9') || c == 'e' || c == 'E' || c == '.' || c == '_')) break; + if(c == '_') { + ReadChar(); + continue; + } + s.push_back(ReadChar()); + } + + char *endptr; + double d = strtod(s.c_str(), &endptr); + + Token t = Token::From(); + if(endptr == s.c_str() + s.size()) { + t = Token::From(TokenType::OPERAND, Expr::Op::CONSTANT); + t.expr->v = d; + } else { + *error = "'" + s + "' is not a valid number"; + } + return t; +} + +ExprParser::Token ExprParser::Lex(std::string *error) { + SkipSpace(); + + Token t = Token::From(); + char c = PeekChar(); + if(isupper(c)) { + std::string n = ReadWord(); + t = Token::From(TokenType::OPERAND, Expr::Op::VARIABLE); + } else if(isalpha(c)) { + std::string s = ReadWord(); + if(s == "sqrt") { + t = Token::From(TokenType::UNARY_OP, Expr::Op::SQRT); + } else if(s == "square") { + t = Token::From(TokenType::UNARY_OP, Expr::Op::SQUARE); + } else if(s == "sin") { + t = Token::From(TokenType::UNARY_OP, Expr::Op::SIN); + } else if(s == "cos") { + t = Token::From(TokenType::UNARY_OP, Expr::Op::COS); + } else if(s == "asin") { + t = Token::From(TokenType::UNARY_OP, Expr::Op::ASIN); + } else if(s == "acos") { + t = Token::From(TokenType::UNARY_OP, Expr::Op::ACOS); + } else if(s == "pi") { + t = Token::From(TokenType::OPERAND, Expr::Op::CONSTANT); + t.expr->v = PI; + } else { + *error = "'" + s + "' is not a valid variable, function or constant"; + } + } else if(isdigit(c) || c == '.') { + return LexNumber(error); + } else if(ispunct(c)) { + ReadChar(); + if(c == '+') { + t = Token::From(TokenType::BINARY_OP, Expr::Op::PLUS); + } else if(c == '-') { + t = Token::From(TokenType::BINARY_OP, Expr::Op::MINUS); + } else if(c == '*') { + t = Token::From(TokenType::BINARY_OP, Expr::Op::TIMES); + } else if(c == '/') { + t = Token::From(TokenType::BINARY_OP, Expr::Op::DIV); + } else if(c == '(') { + t = Token::From(TokenType::PAREN_LEFT); + } else if(c == ')') { + t = Token::From(TokenType::PAREN_RIGHT); + } else { + *error = "'" + std::string(1, c) + "' is not a valid operator"; + } + } else if(c == '\0') { + t = Token::From(TokenType::END); + } else { + *error = "Unexpected character '" + std::string(1, c) + "'"; + } + + return t; +} + +ExprParser::Token ExprParser::PopOperand(std::string *error) { + Token t = Token::From(); + if(stack.empty() || stack.back().type != TokenType::OPERAND) { + *error = "Expected an operand"; + } else { + t = stack.back(); + stack.pop_back(); + } + return t; +} + +ExprParser::Token ExprParser::PopOperator(std::string *error) { + Token t = Token::From(); + if(stack.empty() || (stack.back().type != TokenType::UNARY_OP && + stack.back().type != TokenType::BINARY_OP)) { + *error = "Expected an operator"; + } else { + t = stack.back(); + stack.pop_back(); + } + return t; +} + +int ExprParser::Precedence(Token t) { + ssassert(t.type == TokenType::BINARY_OP || + t.type == TokenType::UNARY_OP || + t.type == TokenType::OPERAND, + "Unexpected token type"); + + if(t.type == TokenType::UNARY_OP) { + return 30; + } else if(t.expr->op == Expr::Op::TIMES || + t.expr->op == Expr::Op::DIV) { + return 20; + } else if(t.expr->op == Expr::Op::PLUS || + t.expr->op == Expr::Op::MINUS) { + return 10; + } else if(t.type == TokenType::OPERAND) { + return 0; + } else ssassert(false, "Unexpected operator"); +} + +bool ExprParser::Reduce(std::string *error) { + Token a = PopOperand(error); + if(a.IsError()) return false; + + Token op = PopOperator(error); + if(op.IsError()) return false; + + Token r = Token::From(TokenType::OPERAND); + switch(op.type) { + case TokenType::BINARY_OP: { + Token b = PopOperand(error); + if(b.IsError()) return false; + r.expr = b.expr->AnyOp(op.expr->op, a.expr); + break; + } + + case TokenType::UNARY_OP: { + Expr *e = a.expr; + switch(op.expr->op) { + case Expr::Op::NEGATE: e = e->Negate(); break; + case Expr::Op::SQRT: e = e->Sqrt(); break; + case Expr::Op::SQUARE: e = e->Times(e); break; + case Expr::Op::SIN: e = e->Times(Expr::From(PI/180))->Sin(); break; + case Expr::Op::COS: e = e->Times(Expr::From(PI/180))->Cos(); break; + case Expr::Op::ASIN: e = e->ASin()->Times(Expr::From(180/PI)); break; + case Expr::Op::ACOS: e = e->ACos()->Times(Expr::From(180/PI)); break; + default: ssassert(false, "Unexpected unary operator"); + } + r.expr = e; + break; + } + + default: ssassert(false, "Unexpected operator"); + } + stack.push_back(r); + + return true; +} + +bool ExprParser::Parse(std::string *error, size_t reduceUntil) { + while(true) { + Token t = Lex(error); + switch(t.type) { + case TokenType::ERROR: + return false; + + case TokenType::END: + case TokenType::PAREN_RIGHT: + while(stack.size() > 1 + reduceUntil) { + if(!Reduce(error)) return false; + } + + if(t.type == TokenType::PAREN_RIGHT) { + stack.push_back(t); + } + return true; + + case TokenType::PAREN_LEFT: { + // sub-expression + if(!Parse(error, /*reduceUntil=*/stack.size())) return false; + + if(stack.empty() || stack.back().type != TokenType::PAREN_RIGHT) { + *error = "Expected ')'"; + return false; + } + stack.pop_back(); + break; + } + + case TokenType::BINARY_OP: + if((stack.size() > reduceUntil && stack.back().type != TokenType::OPERAND) || + stack.size() == reduceUntil) { + if(t.expr->op == Expr::Op::MINUS) { + t.type = TokenType::UNARY_OP; + t.expr->op = Expr::Op::NEGATE; + stack.push_back(t); + break; + } + } + + while(stack.size() > 1 + reduceUntil && + Precedence(t) <= Precedence(stack[stack.size() - 2])) { + if(!Reduce(error)) return false; + } + + stack.push_back(t); + break; + + case TokenType::UNARY_OP: + case TokenType::OPERAND: + stack.push_back(t); + break; + } + } + + return true; +} + +Expr *ExprParser::Parse(const char *input, std::string *error) { + ExprParser parser; + parser.input = input; + parser.inputPos = 0; + if(!parser.Parse(error)) return NULL; + + Token r = parser.PopOperand(error); + if(r.IsError()) return NULL; + return r.expr; +} + +Expr *Expr::Parse(const char *input, std::string *error) { + return ExprParser::Parse(input, error); +} + +Expr *Expr::From(const char *input, bool popUpError) { + std::string error; + Expr *e = ExprParser::Parse(input, &error); + if(!e) { + dbp("Parse/lex error: %s", error.c_str()); + if(popUpError) { + Error("Not a valid number or expression: '%s'.\n%s.", input, error.c_str()); + } + } + return e; +} diff --git a/src/libslic3r/slvs/expr.h b/src/libslic3r/slvs/expr.h new file mode 100644 index 0000000000..ea11310537 --- /dev/null +++ b/src/libslic3r/slvs/expr.h @@ -0,0 +1,140 @@ +//----------------------------------------------------------------------------- +// An expression in our symbolic algebra system, used to write, linearize, +// and solve our constraint equations. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#ifndef __EXPR_H +#define __EXPR_H + +class Expr { +public: + + enum class Op : uint32_t { + // A parameter, by the hParam handle + PARAM = 0, + // A parameter, by a pointer straight in to the param table (faster, + // if we know that the param table won't move around) + PARAM_PTR = 1, + + // Operands + CONSTANT = 20, + VARIABLE = 21, + + // Binary ops + PLUS = 100, + MINUS = 101, + TIMES = 102, + DIV = 103, + // Unary ops + NEGATE = 104, + SQRT = 105, + SQUARE = 106, + SIN = 107, + COS = 108, + ASIN = 109, + ACOS = 110, + }; + + Op op; + Expr *a; + union { + double v; + hParam parh; + Param *parp; + Expr *b; + }; + + Expr() { } + Expr(double val) : op(Op::CONSTANT) { v = val; } + + static inline Expr *AllocExpr() + { return (Expr *)AllocTemporary(sizeof(Expr)); } + + static Expr *From(hParam p); + static Expr *From(double v); + + Expr *AnyOp(Op op, Expr *b); + inline Expr *Plus (Expr *b_) { return AnyOp(Op::PLUS, b_); } + inline Expr *Minus(Expr *b_) { return AnyOp(Op::MINUS, b_); } + inline Expr *Times(Expr *b_) { return AnyOp(Op::TIMES, b_); } + inline Expr *Div (Expr *b_) { return AnyOp(Op::DIV, b_); } + + inline Expr *Negate() { return AnyOp(Op::NEGATE, NULL); } + inline Expr *Sqrt () { return AnyOp(Op::SQRT, NULL); } + inline Expr *Square() { return AnyOp(Op::SQUARE, NULL); } + inline Expr *Sin () { return AnyOp(Op::SIN, NULL); } + inline Expr *Cos () { return AnyOp(Op::COS, NULL); } + inline Expr *ASin () { return AnyOp(Op::ASIN, NULL); } + inline Expr *ACos () { return AnyOp(Op::ACOS, NULL); } + + Expr *PartialWrt(hParam p) const; + double Eval() const; + uint64_t ParamsUsed() const; + bool DependsOn(hParam p) const; + static bool Tol(double a, double b); + Expr *FoldConstants(); + void Substitute(hParam oldh, hParam newh); + + static const hParam NO_PARAMS, MULTIPLE_PARAMS; + hParam ReferencedParams(ParamList *pl) const; + + void ParamsToPointers(); + + std::string Print() const; + + // number of child nodes: 0 (e.g. constant), 1 (sqrt), or 2 (+) + int Children() const; + // total number of nodes in the tree + int Nodes() const; + + // Make a simple copy + Expr *DeepCopy() const; + // Make a copy, with the parameters (usually referenced by hParam) + // resolved to pointers to the actual value. This speeds things up + // considerably. + Expr *DeepCopyWithParamsAsPointers(IdList *firstTry, + IdList *thenTry) const; + + static Expr *Parse(const char *input, std::string *error); + static Expr *From(const char *in, bool popUpError); +}; + +class ExprVector { +public: + Expr *x, *y, *z; + + static ExprVector From(Expr *x, Expr *y, Expr *z); + static ExprVector From(Vector vn); + static ExprVector From(hParam x, hParam y, hParam z); + static ExprVector From(double x, double y, double z); + + ExprVector Plus(ExprVector b) const; + ExprVector Minus(ExprVector b) const; + Expr *Dot(ExprVector b) const; + ExprVector Cross(ExprVector b) const; + ExprVector ScaledBy(Expr *s) const; + ExprVector WithMagnitude(Expr *s) const; + Expr *Magnitude() const; + + Vector Eval() const; +}; + +class ExprQuaternion { +public: + Expr *w, *vx, *vy, *vz; + + static ExprQuaternion From(Expr *w, Expr *vx, Expr *vy, Expr *vz); + static ExprQuaternion From(Quaternion qn); + static ExprQuaternion From(hParam w, hParam vx, hParam vy, hParam vz); + + ExprVector RotationU() const; + ExprVector RotationV() const; + ExprVector RotationN() const; + + ExprVector Rotate(ExprVector p) const; + ExprQuaternion Times(ExprQuaternion b) const; + + Expr *Magnitude() const; +}; +#endif diff --git a/src/libslic3r/slvs/include/SolveSpaceSystem.h b/src/libslic3r/slvs/include/SolveSpaceSystem.h new file mode 100644 index 0000000000..54cc2ee709 --- /dev/null +++ b/src/libslic3r/slvs/include/SolveSpaceSystem.h @@ -0,0 +1,52 @@ +#pragma once + +#include "slvs.h" + +#include +#include +#include + +class DLL SolveSpaceSystem +{ +public: + SolveSpaceSystem(); + + Slvs_hParam addParam(Slvs_Param parameter); + + Slvs_hEntity addEntity(Slvs_Entity entity); + + Slvs_hConstraint addConstr(Slvs_Constraint constr); + + enum ResultStatus { + RESULT_OKAY = SLVS_RESULT_OKAY , + RESULT_INCONSISTENT = SLVS_RESULT_INCONSISTENT , + RESULT_DIDNT_CONVERGE = SLVS_RESULT_DIDNT_CONVERGE , + RESULT_TOO_MANY_UNKNOWNS = SLVS_RESULT_TOO_MANY_UNKNOWNS, + }; + + ResultStatus solve(Slvs_hGroup groupId, bool reportFailedConstraints = true); + + double parameterValue(Slvs_hParam paramId); + void setParameterValue(Slvs_hParam paramId, double value); + + std::tuple< std::valarray, + std::valarray, + std::valarray > + orientationMx(Slvs_hEntity normalIn3dEntityId); + + // Returns point as x, y, z values + std::valarray global3DPos (Slvs_hEntity pointEntityId); + + Slvs_Constraint & constraint(Slvs_hConstraint constraintId); + std::vector failedConstraints() const; + +private: + Slvs_System m_slvsSystem; + + std::vector * m_paramsMemory; + std::vector * m_entityMemory; + std::vector * m_constraintMemory; + std::vector* m_failedConstrMemory; +}; + + diff --git a/src/libslic3r/slvs/include/slvs.h b/src/libslic3r/slvs/include/slvs.h new file mode 100644 index 0000000000..4fd07681de --- /dev/null +++ b/src/libslic3r/slvs/include/slvs.h @@ -0,0 +1,409 @@ +/*----------------------------------------------------------------------------- + * Data structures and prototypes for slvs.lib, a geometric constraint solver. + * + * See the comments in this file, the accompanying sample code that uses + * this library, and the accompanying documentation (DOC.txt). + * + * Copyright 2009-2013 Jonathan Westhues. + *---------------------------------------------------------------------------*/ + +#ifndef __SLVS_H +#define __SLVS_H + +#ifdef SLVS_LIB_SHARED +#ifdef WIN32 +# ifdef EXPORT_DLL +# define DLL __declspec( dllexport ) +# else +# define DLL __declspec( dllimport ) +# endif +#else +# define DLL +#endif +#else +# define DLL +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef _MSC_VER +typedef unsigned __int32 uint32_t; +#else +#include +#endif +#include + +typedef uint32_t Slvs_hParam; +typedef uint32_t Slvs_hEntity; +typedef uint32_t Slvs_hConstraint; +typedef uint32_t Slvs_hGroup; + +/* To obtain the 3d (not projected into a workplane) of a constraint or + * an entity, specify this instead of the workplane. */ +#define SLVS_FREE_IN_3D 0 + + +typedef struct { + Slvs_hParam h; + Slvs_hGroup group; + double val; +} Slvs_Param; + + +#define SLVS_E_POINT_IN_3D 50000 +#define SLVS_E_POINT_IN_2D 50001 + +#define SLVS_E_NORMAL_IN_3D 60000 +#define SLVS_E_NORMAL_IN_2D 60001 + +#define SLVS_E_DISTANCE 70000 + +/* The special point, normal, and distance types used for parametric step + * and repeat, extrude, and assembly are currently not exposed. Please + * contact us if you are interested in using these. */ + +#define SLVS_E_WORKPLANE 80000 +#define SLVS_E_LINE_SEGMENT 80001 +#define SLVS_E_CUBIC 80002 +#define SLVS_E_CIRCLE 80003 +#define SLVS_E_ARC_OF_CIRCLE 80004 + +typedef struct { + Slvs_hEntity h; + Slvs_hGroup group; + + int type; + + Slvs_hEntity wrkpl; + Slvs_hEntity point[4]; + Slvs_hEntity normal; + Slvs_hEntity distance; + + Slvs_hParam param[4]; +} Slvs_Entity; + +#define SLVS_C_POINTS_COINCIDENT 100000 +#define SLVS_C_PT_PT_DISTANCE 100001 +#define SLVS_C_PT_PLANE_DISTANCE 100002 +#define SLVS_C_PT_LINE_DISTANCE 100003 +#define SLVS_C_PT_FACE_DISTANCE 100004 +#define SLVS_C_PT_IN_PLANE 100005 +#define SLVS_C_PT_ON_LINE 100006 +#define SLVS_C_PT_ON_FACE 100007 +#define SLVS_C_EQUAL_LENGTH_LINES 100008 +#define SLVS_C_LENGTH_RATIO 100009 +#define SLVS_C_EQ_LEN_PT_LINE_D 100010 +#define SLVS_C_EQ_PT_LN_DISTANCES 100011 +#define SLVS_C_EQUAL_ANGLE 100012 +#define SLVS_C_EQUAL_LINE_ARC_LEN 100013 +#define SLVS_C_SYMMETRIC 100014 +#define SLVS_C_SYMMETRIC_HORIZ 100015 +#define SLVS_C_SYMMETRIC_VERT 100016 +#define SLVS_C_SYMMETRIC_LINE 100017 +#define SLVS_C_AT_MIDPOINT 100018 +#define SLVS_C_HORIZONTAL 100019 +#define SLVS_C_VERTICAL 100020 +#define SLVS_C_DIAMETER 100021 +#define SLVS_C_PT_ON_CIRCLE 100022 +#define SLVS_C_SAME_ORIENTATION 100023 +#define SLVS_C_ANGLE 100024 +#define SLVS_C_PARALLEL 100025 +#define SLVS_C_PERPENDICULAR 100026 +#define SLVS_C_ARC_LINE_TANGENT 100027 +#define SLVS_C_CUBIC_LINE_TANGENT 100028 +#define SLVS_C_EQUAL_RADIUS 100029 +#define SLVS_C_PROJ_PT_DISTANCE 100030 +#define SLVS_C_WHERE_DRAGGED 100031 +#define SLVS_C_CURVE_CURVE_TANGENT 100032 +#define SLVS_C_LENGTH_DIFFERENCE 100033 + +typedef struct { + Slvs_hConstraint h; + Slvs_hGroup group; + + int type; + + Slvs_hEntity wrkpl; + + double valA; + Slvs_hEntity ptA; + Slvs_hEntity ptB; + Slvs_hEntity entityA; + Slvs_hEntity entityB; + Slvs_hEntity entityC; + Slvs_hEntity entityD; + + int other; + int other2; +} Slvs_Constraint; + + +typedef struct { + /*** INPUT VARIABLES + * + * Here, we specify the parameters and their initial values, the entities, + * and the constraints. For example, param[] points to the array of + * parameters, which has length params, so that the last valid element + * is param[params-1]. + * + * param[] is actually an in/out variable; if the solver is successful, + * then the new values (that satisfy the constraints) are written to it. */ + Slvs_Param *param; + int params; + Slvs_Entity *entity; + int entities; + Slvs_Constraint *constraint; + int constraints; + + /* If a parameter corresponds to a point (distance, normal, etc.) being + * dragged, then specify it here. This will cause the solver to favor + * that parameter, and attempt to change it as little as possible even + * if that requires it to change other parameters more. + * + * Unused members of this array should be set to zero. */ + Slvs_hParam dragged[4]; + + /* If the solver fails, then it can determine which constraints are + * causing the problem. But this is a relatively slow process (for + * a system with n constraints, about n times as long as just solving). + * If calculateFaileds is true, then the solver will do so, otherwise + * not. */ + int calculateFaileds; + + /*** OUTPUT VARIABLES + * + * If the solver fails, then it can report which constraints are causing + * the problem. The caller should allocate the array failed[], and pass + * its size in faileds. + * + * The solver will set faileds equal to the number of problematic + * constraints, and write their Slvs_hConstraints into failed[]. To + * ensure that there is sufficient space for any possible set of + * failing constraints, faileds should be greater than or equal to + * constraints. */ + Slvs_hConstraint *failed; + int faileds; + + /* The solver indicates the number of unconstrained degrees of freedom. */ + int dof; + + /* The solver indicates whether the solution succeeded. */ +#define SLVS_RESULT_OKAY 0 +#define SLVS_RESULT_INCONSISTENT 1 +#define SLVS_RESULT_DIDNT_CONVERGE 2 +#define SLVS_RESULT_TOO_MANY_UNKNOWNS 3 + int result; +} Slvs_System; + +DLL void Slvs_Solve(Slvs_System *sys, Slvs_hGroup hg); + + +/* Our base coordinate system has basis vectors + * (1, 0, 0) (0, 1, 0) (0, 0, 1) + * A unit quaternion defines a rotation to a new coordinate system with + * basis vectors + * U V N + * which these functions compute from the quaternion. */ +DLL void Slvs_QuaternionU(double qw, double qx, double qy, double qz, + double *x, double *y, double *z); +DLL void Slvs_QuaternionV(double qw, double qx, double qy, double qz, + double *x, double *y, double *z); +DLL void Slvs_QuaternionN(double qw, double qx, double qy, double qz, + double *x, double *y, double *z); + +/* Similarly, compute a unit quaternion in terms of two basis vectors. */ +DLL void Slvs_MakeQuaternion(double ux, double uy, double uz, + double vx, double vy, double vz, + double *qw, double *qx, double *qy, double *qz); + + +/*------------------------------------- + * These are just convenience functions, to save you the trouble of filling + * out the structures by hand. The code is included in the header file to + * let the compiler inline them if possible. */ + +static inline Slvs_Param Slvs_MakeParam(Slvs_hParam h, Slvs_hGroup group, double val) +{ + Slvs_Param r; + r.h = h; + r.group = group; + r.val = val; + return r; +} +static inline Slvs_Entity Slvs_MakePoint2d(Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity wrkpl, + Slvs_hParam u, Slvs_hParam v) +{ + Slvs_Entity r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = SLVS_E_POINT_IN_2D; + r.wrkpl = wrkpl; + r.param[0] = u; + r.param[1] = v; + return r; +} +static inline Slvs_Entity Slvs_MakePoint3d(Slvs_hEntity h, Slvs_hGroup group, + Slvs_hParam x, Slvs_hParam y, Slvs_hParam z) +{ + Slvs_Entity r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = SLVS_E_POINT_IN_3D; + r.wrkpl = SLVS_FREE_IN_3D; + r.param[0] = x; + r.param[1] = y; + r.param[2] = z; + return r; +} +static inline Slvs_Entity Slvs_MakeNormal3d(Slvs_hEntity h, Slvs_hGroup group, + Slvs_hParam qw, Slvs_hParam qx, + Slvs_hParam qy, Slvs_hParam qz) +{ + Slvs_Entity r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = SLVS_E_NORMAL_IN_3D; + r.wrkpl = SLVS_FREE_IN_3D; + r.param[0] = qw; + r.param[1] = qx; + r.param[2] = qy; + r.param[3] = qz; + return r; +} +static inline Slvs_Entity Slvs_MakeNormal2d(Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity wrkpl) +{ + Slvs_Entity r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = SLVS_E_NORMAL_IN_2D; + r.wrkpl = wrkpl; + return r; +} +static inline Slvs_Entity Slvs_MakeDistance(Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity wrkpl, Slvs_hParam d) +{ + Slvs_Entity r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = SLVS_E_DISTANCE; + r.wrkpl = wrkpl; + r.param[0] = d; + return r; +} +static inline Slvs_Entity Slvs_MakeLineSegment(Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity wrkpl, + Slvs_hEntity ptA, Slvs_hEntity ptB) +{ + Slvs_Entity r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = SLVS_E_LINE_SEGMENT; + r.wrkpl = wrkpl; + r.point[0] = ptA; + r.point[1] = ptB; + return r; +} +static inline Slvs_Entity Slvs_MakeCubic(Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity wrkpl, + Slvs_hEntity pt0, Slvs_hEntity pt1, + Slvs_hEntity pt2, Slvs_hEntity pt3) +{ + Slvs_Entity r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = SLVS_E_CUBIC; + r.wrkpl = wrkpl; + r.point[0] = pt0; + r.point[1] = pt1; + r.point[2] = pt2; + r.point[3] = pt3; + return r; +} +static inline Slvs_Entity Slvs_MakeArcOfCircle(Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity wrkpl, + Slvs_hEntity normal, + Slvs_hEntity center, + Slvs_hEntity start, Slvs_hEntity end) +{ + Slvs_Entity r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = SLVS_E_ARC_OF_CIRCLE; + r.wrkpl = wrkpl; + r.normal = normal; + r.point[0] = center; + r.point[1] = start; + r.point[2] = end; + return r; +} +static inline Slvs_Entity Slvs_MakeCircle(Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity wrkpl, + Slvs_hEntity center, + Slvs_hEntity normal, Slvs_hEntity radius) +{ + Slvs_Entity r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = SLVS_E_CIRCLE; + r.wrkpl = wrkpl; + r.point[0] = center; + r.normal = normal; + r.distance = radius; + return r; +} +static inline Slvs_Entity Slvs_MakeWorkplane(Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity origin, Slvs_hEntity normal) +{ + Slvs_Entity r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = SLVS_E_WORKPLANE; + r.wrkpl = SLVS_FREE_IN_3D; + r.point[0] = origin; + r.normal = normal; + return r; +} + +static inline Slvs_Constraint Slvs_MakeConstraint(Slvs_hConstraint h, + Slvs_hGroup group, + int type, + Slvs_hEntity wrkpl, + double valA, + Slvs_hEntity ptA, + Slvs_hEntity ptB, + Slvs_hEntity entityA, + Slvs_hEntity entityB) +{ + Slvs_Constraint r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = type; + r.wrkpl = wrkpl; + r.valA = valA; + r.ptA = ptA; + r.ptB = ptB; + r.entityA = entityA; + r.entityB = entityB; + return r; +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/libslic3r/slvs/lib.cpp b/src/libslic3r/slvs/lib.cpp new file mode 100644 index 0000000000..4e5e7fdf81 --- /dev/null +++ b/src/libslic3r/slvs/lib.cpp @@ -0,0 +1,272 @@ +//----------------------------------------------------------------------------- +// A library wrapper around SolveSpace, to permit someone to use its constraint +// solver without coupling their program too much to SolveSpace's internals. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" +#define EXPORT_DLL +#include + +Sketch SolveSpace::SK = {}; +static System SYS; + +static int IsInit = 0; + +void Group::GenerateEquations(IdList *) { + // Nothing to do for now. +} + +void SolveSpace::CnfFreezeInt(uint32_t, const std::string &) +{ + abort(); +} + +uint32_t SolveSpace::CnfThawInt(uint32_t, const std::string &) +{ + abort(); + return 0; +} + +void SolveSpace::DoMessageBox(const char *, int, int, bool) +{ + abort(); +} + +extern "C" { + +void Slvs_QuaternionU(double qw, double qx, double qy, double qz, + double *x, double *y, double *z) +{ + Quaternion q = Quaternion::From(qw, qx, qy, qz); + Vector v = q.RotationU(); + *x = v.x; + *y = v.y; + *z = v.z; +} + +void Slvs_QuaternionV(double qw, double qx, double qy, double qz, + double *x, double *y, double *z) +{ + Quaternion q = Quaternion::From(qw, qx, qy, qz); + Vector v = q.RotationV(); + *x = v.x; + *y = v.y; + *z = v.z; +} + +void Slvs_QuaternionN(double qw, double qx, double qy, double qz, + double *x, double *y, double *z) +{ + Quaternion q = Quaternion::From(qw, qx, qy, qz); + Vector v = q.RotationN(); + *x = v.x; + *y = v.y; + *z = v.z; +} + +void Slvs_MakeQuaternion(double ux, double uy, double uz, + double vx, double vy, double vz, + double *qw, double *qx, double *qy, double *qz) +{ + Vector u = Vector::From(ux, uy, uz), + v = Vector::From(vx, vy, vz); + Quaternion q = Quaternion::From(u, v); + *qw = q.w; + *qx = q.vx; + *qy = q.vy; + *qz = q.vz; +} + +void Slvs_Solve(Slvs_System *ssys, Slvs_hGroup shg) +{ + if(!IsInit) { + InitPlatform(0, NULL); + IsInit = 1; + } + + int i; + for(i = 0; i < ssys->params; i++) { + Slvs_Param *sp = &(ssys->param[i]); + Param p = {}; + + p.h.v = sp->h; + p.val = sp->val; + SK.param.Add(&p); + if(sp->group == shg) { + SYS.param.Add(&p); + } + } + + for(i = 0; i < ssys->entities; i++) { + Slvs_Entity *se = &(ssys->entity[i]); + EntityBase e = {}; + + switch(se->type) { +case SLVS_E_POINT_IN_3D: e.type = Entity::Type::POINT_IN_3D; break; +case SLVS_E_POINT_IN_2D: e.type = Entity::Type::POINT_IN_2D; break; +case SLVS_E_NORMAL_IN_3D: e.type = Entity::Type::NORMAL_IN_3D; break; +case SLVS_E_NORMAL_IN_2D: e.type = Entity::Type::NORMAL_IN_2D; break; +case SLVS_E_DISTANCE: e.type = Entity::Type::DISTANCE; break; +case SLVS_E_WORKPLANE: e.type = Entity::Type::WORKPLANE; break; +case SLVS_E_LINE_SEGMENT: e.type = Entity::Type::LINE_SEGMENT; break; +case SLVS_E_CUBIC: e.type = Entity::Type::CUBIC; break; +case SLVS_E_CIRCLE: e.type = Entity::Type::CIRCLE; break; +case SLVS_E_ARC_OF_CIRCLE: e.type = Entity::Type::ARC_OF_CIRCLE; break; + +default: dbp("bad entity type %d", se->type); return; + } + e.h.v = se->h; + e.group.v = se->group; + e.workplane.v = se->wrkpl; + e.point[0].v = se->point[0]; + e.point[1].v = se->point[1]; + e.point[2].v = se->point[2]; + e.point[3].v = se->point[3]; + e.normal.v = se->normal; + e.distance.v = se->distance; + e.param[0].v = se->param[0]; + e.param[1].v = se->param[1]; + e.param[2].v = se->param[2]; + e.param[3].v = se->param[3]; + + SK.entity.Add(&e); + } + IdList params = {}; + for(i = 0; i < ssys->constraints; i++) { + Slvs_Constraint *sc = &(ssys->constraint[i]); + ConstraintBase c = {}; + + Constraint::Type t; + switch(sc->type) { +case SLVS_C_POINTS_COINCIDENT: t = Constraint::Type::POINTS_COINCIDENT; break; +case SLVS_C_PT_PT_DISTANCE: t = Constraint::Type::PT_PT_DISTANCE; break; +case SLVS_C_PT_PLANE_DISTANCE: t = Constraint::Type::PT_PLANE_DISTANCE; break; +case SLVS_C_PT_LINE_DISTANCE: t = Constraint::Type::PT_LINE_DISTANCE; break; +case SLVS_C_PT_FACE_DISTANCE: t = Constraint::Type::PT_FACE_DISTANCE; break; +case SLVS_C_PT_IN_PLANE: t = Constraint::Type::PT_IN_PLANE; break; +case SLVS_C_PT_ON_LINE: t = Constraint::Type::PT_ON_LINE; break; +case SLVS_C_PT_ON_FACE: t = Constraint::Type::PT_ON_FACE; break; +case SLVS_C_EQUAL_LENGTH_LINES: t = Constraint::Type::EQUAL_LENGTH_LINES; break; +case SLVS_C_LENGTH_RATIO: t = Constraint::Type::LENGTH_RATIO; break; +case SLVS_C_EQ_LEN_PT_LINE_D: t = Constraint::Type::EQ_LEN_PT_LINE_D; break; +case SLVS_C_EQ_PT_LN_DISTANCES: t = Constraint::Type::EQ_PT_LN_DISTANCES; break; +case SLVS_C_EQUAL_ANGLE: t = Constraint::Type::EQUAL_ANGLE; break; +case SLVS_C_EQUAL_LINE_ARC_LEN: t = Constraint::Type::EQUAL_LINE_ARC_LEN; break; +case SLVS_C_LENGTH_DIFFERENCE: t = Constraint::Type::LENGTH_DIFFERENCE; break; +case SLVS_C_SYMMETRIC: t = Constraint::Type::SYMMETRIC; break; +case SLVS_C_SYMMETRIC_HORIZ: t = Constraint::Type::SYMMETRIC_HORIZ; break; +case SLVS_C_SYMMETRIC_VERT: t = Constraint::Type::SYMMETRIC_VERT; break; +case SLVS_C_SYMMETRIC_LINE: t = Constraint::Type::SYMMETRIC_LINE; break; +case SLVS_C_AT_MIDPOINT: t = Constraint::Type::AT_MIDPOINT; break; +case SLVS_C_HORIZONTAL: t = Constraint::Type::HORIZONTAL; break; +case SLVS_C_VERTICAL: t = Constraint::Type::VERTICAL; break; +case SLVS_C_DIAMETER: t = Constraint::Type::DIAMETER; break; +case SLVS_C_PT_ON_CIRCLE: t = Constraint::Type::PT_ON_CIRCLE; break; +case SLVS_C_SAME_ORIENTATION: t = Constraint::Type::SAME_ORIENTATION; break; +case SLVS_C_ANGLE: t = Constraint::Type::ANGLE; break; +case SLVS_C_PARALLEL: t = Constraint::Type::PARALLEL; break; +case SLVS_C_PERPENDICULAR: t = Constraint::Type::PERPENDICULAR; break; +case SLVS_C_ARC_LINE_TANGENT: t = Constraint::Type::ARC_LINE_TANGENT; break; +case SLVS_C_CUBIC_LINE_TANGENT: t = Constraint::Type::CUBIC_LINE_TANGENT; break; +case SLVS_C_EQUAL_RADIUS: t = Constraint::Type::EQUAL_RADIUS; break; +case SLVS_C_PROJ_PT_DISTANCE: t = Constraint::Type::PROJ_PT_DISTANCE; break; +case SLVS_C_WHERE_DRAGGED: t = Constraint::Type::WHERE_DRAGGED; break; +case SLVS_C_CURVE_CURVE_TANGENT:t = Constraint::Type::CURVE_CURVE_TANGENT; break; + +default: dbp("bad constraint type %d", sc->type); return; + } + + c.type = t; + + c.h.v = sc->h; + c.group.v = sc->group; + c.workplane.v = sc->wrkpl; + c.valA = sc->valA; + c.ptA.v = sc->ptA; + c.ptB.v = sc->ptB; + c.entityA.v = sc->entityA; + c.entityB.v = sc->entityB; + c.entityC.v = sc->entityC; + c.entityD.v = sc->entityD; + c.other = (sc->other) ? true : false; + c.other2 = (sc->other2) ? true : false; + + c.Generate(¶ms); + if(params.n > 0) { + for(Param &p : params) { + p.h = SK.param.AddAndAssignId(&p); + c.valP = p.h; + SYS.param.Add(&p); + } + params.Clear(); + c.ModifyToSatisfy(); + } + + SK.constraint.Add(&c); + } + + for(i = 0; i < (int)arraylen(ssys->dragged); i++) { + if(ssys->dragged[i]) { + hParam hp = { ssys->dragged[i] }; + SYS.dragged.Add(&hp); + } + } + + Group g = {}; + g.h.v = shg; + + List bad = {}; + + // Now we're finally ready to solve! + bool andFindBad = ssys->calculateFaileds ? true : false; + SolveResult how = SYS.Solve(&g, &(ssys->dof), &bad, andFindBad, /*andFindFree=*/false); + + switch(how) { + case SolveResult::OKAY: + ssys->result = SLVS_RESULT_OKAY; + break; + + case SolveResult::DIDNT_CONVERGE: + ssys->result = SLVS_RESULT_DIDNT_CONVERGE; + break; + + case SolveResult::REDUNDANT_DIDNT_CONVERGE: + case SolveResult::REDUNDANT_OKAY: + ssys->result = SLVS_RESULT_INCONSISTENT; + break; + + case SolveResult::TOO_MANY_UNKNOWNS: + ssys->result = SLVS_RESULT_TOO_MANY_UNKNOWNS; + break; + } + + // Write the new parameter values back to our caller. + for(i = 0; i < ssys->params; i++) { + Slvs_Param *sp = &(ssys->param[i]); + hParam hp = { sp->h }; + sp->val = SK.GetParam(hp)->val; + } + + if(ssys->failed) { + // Copy over any the list of problematic constraints. + for(i = 0; i < ssys->faileds && i < bad.n; i++) { + ssys->failed[i] = bad.elem[i].v; + } + ssys->faileds = bad.n; + } + + bad.Clear(); + SYS.param.Clear(); + SYS.entity.Clear(); + SYS.eq.Clear(); + SYS.dragged.Clear(); + + SK.param.Clear(); + SK.entity.Clear(); + SK.constraint.Clear(); + + FreeAllTemporary(); +} + +} /* extern "C" */ diff --git a/src/libslic3r/slvs/platform/platform.h b/src/libslic3r/slvs/platform/platform.h new file mode 100644 index 0000000000..06ae19ffc4 --- /dev/null +++ b/src/libslic3r/slvs/platform/platform.h @@ -0,0 +1,68 @@ +//----------------------------------------------------------------------------- +// Platform-dependent functionality. +// +// Copyright 2017 whitequark +//----------------------------------------------------------------------------- + +#ifndef SOLVESPACE_PLATFORM_H +#define SOLVESPACE_PLATFORM_H + +namespace Platform { + +// UTF-8 ⟷ UTF-16 conversion, for Windows. +#if defined(WIN32) +std::string Narrow(const wchar_t *s); +std::wstring Widen(const char *s); +std::string Narrow(const std::wstring &s); +std::wstring Widen(const std::string &s); +#endif + +// A filesystem path, respecting the conventions of the current platform. +// Transformation functions return an empty path on error. +class Path { +public: + std::string raw; + + static Path From(std::string raw); + static Path CurrentDirectory(); + + void Clear() { raw.clear(); } + + bool Equals(const Path &other) const; + bool IsEmpty() const { return raw.empty(); } + bool IsAbsolute() const; + bool HasExtension(std::string ext) const; + + std::string FileName() const; + std::string FileStem() const; + std::string Extension() const; + + Path WithExtension(std::string ext) const; + Path Parent() const; + Path Join(const std::string &component) const; + Path Join(const Path &other) const; + Path Expand(bool fromCurrentDirectory = false) const; + Path RelativeTo(const Path &base) const; + + // Converting to and from a platform-independent representation + // (conventionally, the Unix one). + static Path FromPortable(const std::string &repr); + std::string ToPortable() const; +}; + +struct PathLess { + bool operator()(const Path &a, const Path &b) const { return a.raw < b.raw; } +}; + +// File manipulation functions. +FILE *OpenFile(const Platform::Path &filename, const char *mode); +bool ReadFile(const Platform::Path &filename, std::string *data); +bool WriteFile(const Platform::Path &filename, const std::string &data); +void RemoveFile(const Platform::Path &filename); + +// Resource loading function. +const void *LoadResource(const std::string &name, size_t *size); + +} + +#endif diff --git a/src/libslic3r/slvs/platform/unixutil.cpp b/src/libslic3r/slvs/platform/unixutil.cpp new file mode 100644 index 0000000000..3c8f62d0f9 --- /dev/null +++ b/src/libslic3r/slvs/platform/unixutil.cpp @@ -0,0 +1,124 @@ +//----------------------------------------------------------------------------- +// Utility functions used by the Unix port. Notably, our memory allocation; +// we use two separate allocators, one for long-lived stuff and one for +// stuff that gets freed after every regeneration of the model, to save us +// the trouble of freeing the latter explicitly. +// +// Copyright 2008-2013 Jonathan Westhues. +// Copyright 2013 Daniel Richard G. +//----------------------------------------------------------------------------- +#ifndef LIBRARY +#include +#endif + +#include "solvespace.h" + +namespace SolveSpace { + +void dbp(const char *str, ...) +{ + va_list f; + static char buf[1024*50]; + va_start(f, str); + vsnprintf(buf, sizeof(buf), str, f); + va_end(f); + + fputs(buf, stderr); + fputc('\n', stderr); +} + +void assert_failure(const char *file, unsigned line, const char *function, + const char *condition, const char *message) { + fprintf(stderr, "File %s, line %u, function %s:\n", file, line, function); + fprintf(stderr, "Assertion '%s' failed: ((%s) == false).\n", message, condition); + +#ifndef LIBRARY + static void *ptrs[1024] = {}; + size_t nptrs = backtrace(ptrs, sizeof(ptrs) / sizeof(ptrs[0])); + char **syms = backtrace_symbols(ptrs, nptrs); + + fprintf(stderr, "Backtrace:\n"); + if(syms != NULL) { + for(size_t i = 0; i < nptrs; i++) { + fprintf(stderr, "%2zu: %s\n", i, syms[i]); + } + } else { + for(size_t i = 0; i < nptrs; i++) { + fprintf(stderr, "%2zu: %p\n", i, ptrs[i]); + } + } +#endif + + abort(); +} + +//----------------------------------------------------------------------------- +// A separate heap, on which we allocate expressions. Maybe a bit faster, +// since fragmentation is less of a concern, and it also makes it possible +// to be sloppy with our memory management, and just free everything at once +// at the end. +//----------------------------------------------------------------------------- + +typedef struct _AllocTempHeader AllocTempHeader; + +typedef struct _AllocTempHeader { + AllocTempHeader *prev; + AllocTempHeader *next; +} AllocTempHeader; + +static AllocTempHeader *Head = NULL; + +void *AllocTemporary(size_t n) +{ + AllocTempHeader *h = + (AllocTempHeader *)malloc(n + sizeof(AllocTempHeader)); + h->prev = NULL; + h->next = Head; + if(Head) Head->prev = h; + Head = h; + memset(&h[1], 0, n); + return (void *)&h[1]; +} + +void FreeTemporary(void *p) +{ + AllocTempHeader *h = (AllocTempHeader *)p - 1; + if(h->prev) { + h->prev->next = h->next; + } else { + Head = h->next; + } + if(h->next) h->next->prev = h->prev; + free(h); +} + +void FreeAllTemporary(void) +{ + AllocTempHeader *h = Head; + while(h) { + AllocTempHeader *f = h; + h = h->next; + free(f); + } + Head = NULL; +} + +void *MemAlloc(size_t n) { + void *p = malloc(n); + ssassert(p != NULL, "Cannot allocate memory"); + return p; +} + +void MemFree(void *p) { + free(p); +} + +std::vector InitPlatform(int argc, char **argv) { + std::vector args; + for(int i = 0; i < argc; i++) { + args.push_back(argv[i]); + } + return args; +} + +}; diff --git a/src/libslic3r/slvs/polygon.h b/src/libslic3r/slvs/polygon.h new file mode 100644 index 0000000000..5767dc0359 --- /dev/null +++ b/src/libslic3r/slvs/polygon.h @@ -0,0 +1,415 @@ +//----------------------------------------------------------------------------- +// Anything relating to plane polygons and triangles, and (generally, non- +// planar) meshes thereof. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- + +#ifndef __POLYGON_H +#define __POLYGON_H + +class SPointList; +class SPolygon; +class SContour; +class SMesh; +class SBsp3; +class SOutlineList; + +enum class EarType : uint32_t { + UNKNOWN = 0, + NOT_EAR = 1, + EAR = 2 +}; + +enum class BspClass : uint32_t { + POS = 100, + NEG = 101, + COPLANAR = 200 +}; + +enum class EdgeKind : uint32_t { + NAKED_OR_SELF_INTER = 100, + SELF_INTER = 200, + TURNING = 300, + EMPHASIZED = 400, + SHARP = 500, +}; + +class SEdge { +public: + int tag; + int auxA, auxB; + Vector a, b; + + static SEdge From(Vector a, Vector b); + bool EdgeCrosses(Vector a, Vector b, Vector *pi=NULL, SPointList *spl=NULL) const; +}; + +class SEdgeList { +public: + List l; + + void Clear(); + void AddEdge(Vector a, Vector b, int auxA=0, int auxB=0, int tag=0); + bool AssemblePolygon(SPolygon *dest, SEdge *errorAt, bool keepDir=false) const; + bool AssembleContour(Vector first, Vector last, SContour *dest, + SEdge *errorAt, bool keepDir) const; + int AnyEdgeCrossings(Vector a, Vector b, + Vector *pi=NULL, SPointList *spl=NULL) const; + bool ContainsEdgeFrom(const SEdgeList *sel) const; + bool ContainsEdge(const SEdge *se) const; + void CullExtraneousEdges(); + void MergeCollinearSegments(Vector a, Vector b); +}; + +// A kd-tree element needs to go on a side of a node if it's when KDTREE_EPS +// of the boundary. So increasing this number never breaks anything, but may +// result in more duplicated elements. So it's conservative to be sloppy here. +#define KDTREE_EPS (20*LENGTH_EPS) + +class SEdgeLl { +public: + SEdge *se; + SEdgeLl *next; + + static SEdgeLl *Alloc(); +}; + +class SKdNodeEdges { +public: + int which; // whether c is x, y, or z + double c; + SKdNodeEdges *gt; + SKdNodeEdges *lt; + + SEdgeLl *edges; + + static SKdNodeEdges *From(SEdgeList *sel); + static SKdNodeEdges *From(SEdgeLl *sell); + static SKdNodeEdges *Alloc(); + int AnyEdgeCrossings(Vector a, Vector b, int cnt, + Vector *pi=NULL, SPointList *spl=NULL) const; +}; + +class SPoint { +public: + int tag; + + EarType ear; + + Vector p; + Vector auxv; +}; + +class SPointList { +public: + List l; + + void Clear(); + bool ContainsPoint(Vector pt) const; + int IndexForPoint(Vector pt) const; + void IncrementTagFor(Vector pt); + void Add(Vector pt); +}; + +class SContour { +public: + int tag; + int timesEnclosed; + Vector xminPt; + List l; + + void AddPoint(Vector p); + void MakeEdgesInto(SEdgeList *el) const; + void Reverse(); + Vector ComputeNormal() const; + double SignedAreaProjdToNormal(Vector n) const; + bool IsClockwiseProjdToNormal(Vector n) const; + bool ContainsPointProjdToNormal(Vector n, Vector p) const; + void OffsetInto(SContour *dest, double r) const; + void CopyInto(SContour *dest) const; + void FindPointWithMinX(); + Vector AnyEdgeMidpoint() const; + + bool IsEar(int bp, double scaledEps) const; + bool BridgeToContour(SContour *sc, SEdgeList *el, List *vl); + void ClipEarInto(SMesh *m, int bp, double scaledEps); + void UvTriangulateInto(SMesh *m, SSurface *srf); +}; + +typedef struct { + uint32_t face; + RgbaColor color; +} STriMeta; + +class SPolygon { +public: + List l; + Vector normal; + + Vector ComputeNormal() const; + void AddEmptyContour(); + int WindingNumberForPoint(Vector p) const; + double SignedArea() const; + bool ContainsPoint(Vector p) const; + void MakeEdgesInto(SEdgeList *el) const; + void FixContourDirections(); + void Clear(); + bool SelfIntersecting(Vector *intersectsAt) const; + bool IsEmpty() const; + Vector AnyPoint() const; + void OffsetInto(SPolygon *dest, double r) const; + void UvTriangulateInto(SMesh *m, SSurface *srf); + void UvGridTriangulateInto(SMesh *m, SSurface *srf); + void TriangulateInto(SMesh *m) const; + void InverseTransformInto(SPolygon *sp, Vector u, Vector v, Vector n) const; +}; + +class STriangle { +public: + int tag; + STriMeta meta; + + union { + struct { Vector a, b, c; }; + Vector vertices[3]; + }; + + union { + struct { Vector an, bn, cn; }; + Vector normals[3]; + }; + + static STriangle From(STriMeta meta, Vector a, Vector b, Vector c); + Vector Normal() const; + void FlipNormal(); + double MinAltitude() const; + int WindingNumberForPoint(Vector p) const; + bool ContainsPoint(Vector p) const; + bool ContainsPointProjd(Vector n, Vector p) const; + STriangle Transform(Vector o, Vector u, Vector v) const; + bool Raytrace(const Vector &rayPoint, const Vector &rayDir, + double *t, Vector *inters) const; + double SignedVolume() const; +}; + +class SBsp2 { +public: + Vector np; // normal to the plane + + Vector no; // outer normal to the edge + double d; + SEdge edge; + + SBsp2 *pos; + SBsp2 *neg; + + SBsp2 *more; + + void InsertTriangleHow(BspClass how, STriangle *tr, SMesh *m, SBsp3 *bsp3); + void InsertTriangle(STriangle *tr, SMesh *m, SBsp3 *bsp3); + Vector IntersectionWith(Vector a, Vector b) const; + void InsertEdge(SEdge *nedge, Vector nnp, Vector out); + static SBsp2 *InsertOrCreateEdge(SBsp2 *where, SEdge *nedge, + Vector nnp, Vector out); + static SBsp2 *Alloc(); +}; + +class SBsp3 { +public: + Vector n; + double d; + + STriangle tri; + SBsp3 *pos; + SBsp3 *neg; + + SBsp3 *more; + + SBsp2 *edges; + + static SBsp3 *Alloc(); + static SBsp3 *FromMesh(const SMesh *m); + + Vector IntersectionWith(Vector a, Vector b) const; + + void InsertHow(BspClass how, STriangle *str, SMesh *instead); + void Insert(STriangle *str, SMesh *instead); + static SBsp3 *InsertOrCreate(SBsp3 *where, STriangle *str, SMesh *instead); + + void InsertConvexHow(BspClass how, STriMeta meta, Vector *vertex, size_t n, + SMesh *instead); + SBsp3 *InsertConvex(STriMeta meta, Vector *vertex, size_t n, SMesh *instead); + + void InsertInPlane(bool pos2, STriangle *tr, SMesh *m); + + void GenerateInPaintOrder(SMesh *m) const; +}; + +class SMesh { +public: + List l; + + bool flipNormal; + bool keepCoplanar; + bool atLeastOneDiscarded; + bool isTransparent; + + void Clear(); + void AddTriangle(const STriangle *st); + void AddTriangle(STriMeta meta, Vector a, Vector b, Vector c); + void AddTriangle(STriMeta meta, Vector n, + Vector a, Vector b, Vector c); + void DoBounding(Vector v, Vector *vmax, Vector *vmin) const; + void GetBounding(Vector *vmax, Vector *vmin) const; + + void Simplify(int start); + + void AddAgainstBsp(SMesh *srcm, SBsp3 *bsp3); + void MakeFromUnionOf(SMesh *a, SMesh *b); + void MakeFromDifferenceOf(SMesh *a, SMesh *b); + + void MakeFromCopyOf(SMesh *a); + void MakeFromTransformationOf(SMesh *a, Vector trans, + Quaternion q, double scale); + void MakeFromAssemblyOf(SMesh *a, SMesh *b); + + void MakeEdgesInPlaneInto(SEdgeList *sel, Vector n, double d); + void MakeOutlinesInto(SOutlineList *sol, EdgeKind type); + + void PrecomputeTransparency(); + void RemoveDegenerateTriangles(); + + bool IsEmpty() const; + void RemapFaces(Group *g, int remap); + + uint32_t FirstIntersectionWith(Point2d mp) const; + + Vector GetCenterOfMass() const; +}; + +// A linked list of triangles +class STriangleLl { +public: + STriangle *tri; + + STriangleLl *next; + + static STriangleLl *Alloc(); +}; + +class SOutline { +public: + int tag; + Vector a, b, nl, nr; + + bool IsVisible(Vector projDir) const; +}; + +class SOutlineList { +public: + List l; + + void Clear(); + void AddEdge(Vector a, Vector b, Vector nl, Vector nr, int tag = 0); + void ListTaggedInto(SEdgeList *el, int auxA = 0, int auxB = 0); + + void MakeFromCopyOf(SOutlineList *ol); +}; + +class SKdNode { +public: + struct EdgeOnInfo { + int count; + bool frontFacing; + bool intersectsMesh; + STriangle *tr; + int ai; + int bi; + }; + + int which; // whether c is x, y, or z + double c; + + SKdNode *gt; + SKdNode *lt; + + STriangleLl *tris; + + static SKdNode *Alloc(); + static SKdNode *From(SMesh *m); + static SKdNode *From(STriangleLl *tll); + + void AddTriangle(STriangle *tr); + void MakeMeshInto(SMesh *m) const; + void ListTrianglesInto(std::vector *tl) const; + void ClearTags() const; + + void FindEdgeOn(Vector a, Vector b, int cnt, bool coplanarIsInter, EdgeOnInfo *info) const; + void MakeCertainEdgesInto(SEdgeList *sel, EdgeKind how, bool coplanarIsInter, + bool *inter, bool *leaky, int auxA = 0) const; + void MakeOutlinesInto(SOutlineList *sel, EdgeKind tagKind) const; + + void OcclusionTestLine(SEdge orig, SEdgeList *sel, int cnt) const; + void SplitLinesAgainstTriangle(SEdgeList *sel, STriangle *tr) const; + + void SnapToMesh(SMesh *m); + void SnapToVertex(Vector v, SMesh *extras); +}; + +class PolylineBuilder { +public: + struct Edge; + + struct Vertex { + Vector pos; + std::vector edges; + + bool GetNext(uint32_t kind, Vertex **next, Edge **nextEdge); + bool GetNext(uint32_t kind, Vector plane, double d, Vertex **next, Edge **nextEdge); + size_t CountEdgesWithTagAndKind(int tag, uint32_t kind) const; + }; + + struct VertexPairHash { + size_t operator()(const std::pair &v) const; + }; + + struct Edge { + Vertex *a; + Vertex *b; + uint32_t kind; + int tag; + + union { + uintptr_t data; + SOutline *outline; + SEdge *edge; + }; + + Vertex *GetOtherVertex(Vertex *v) const; + bool GetStartAndNext(Vertex **start, Vertex **next, bool loop) const; + }; + + std::unordered_map vertices; + std::unordered_map, Edge *, VertexPairHash> edgeMap; + std::vector edges; + + ~PolylineBuilder(); + void Clear(); + + Vertex *AddVertex(const Vector &pos); + Edge *AddEdge(const Vector &p0, const Vector &p1, uint32_t kind, uintptr_t data = 0); + void Generate( + std::function startFunc, + std::function nextFunc, + std::function aloneFunc, + std::function endFunc = [](){}); + + void MakeFromEdges(const SEdgeList &sel); + void MakeFromOutlines(const SOutlineList &sol); + void GenerateEdges(SEdgeList *sel); + void GenerateOutlines(SOutlineList *sol); +}; + +#endif + diff --git a/src/libslic3r/slvs/render/render.h b/src/libslic3r/slvs/render/render.h new file mode 100644 index 0000000000..07eeca4db6 --- /dev/null +++ b/src/libslic3r/slvs/render/render.h @@ -0,0 +1,366 @@ +//----------------------------------------------------------------------------- +// Backend-agnostic rendering interface, and various backends we use. +// +// Copyright 2016 whitequark +//----------------------------------------------------------------------------- + +#ifndef SOLVESPACE_RENDER_H +#define SOLVESPACE_RENDER_H + +//----------------------------------------------------------------------------- +// Interfaces and utilities common for all renderers. +//----------------------------------------------------------------------------- + +enum class StipplePattern : uint32_t; + +// A mapping from 3d sketch coordinates to 2d screen coordinates, using +// an axonometric projection. +class Camera { +public: + size_t width, height; + Vector offset; + Vector projRight; + Vector projUp; + double scale; + double tangent; + bool hasPixels; + + bool IsPerspective() const { return tangent != 0.0; } + + Point2d ProjectPoint(Vector p) const; + Vector ProjectPoint3(Vector p) const; + Vector ProjectPoint4(Vector p, double *w) const; + Vector UnProjectPoint(Point2d p) const; + Vector UnProjectPoint3(Vector p) const; + Vector VectorFromProjs(Vector rightUpForward) const; + Vector AlignToPixelGrid(Vector v) const; + + SBezier ProjectBezier(SBezier b) const; + + void LoadIdentity(); + void NormalizeProjectionVectors(); +}; + +// A description of scene lighting. +class Lighting { +public: + RgbaColor backgroundColor; + double ambientIntensity; + double lightIntensity[2]; + Vector lightDirection[2]; +}; + +class BatchCanvas; + +// An interface for populating a drawing area with geometry. +class Canvas { +public: + // Stroke and fill styles are addressed with handles to be able to quickly + // group geometry into indexed draw calls. + class hStroke { + public: + uint32_t v; + }; + + class hFill { + public: + uint32_t v; + }; + + // The layer of a geometry describes how it occludes other geometry. + // Within a layer, geometry with higher z-index occludes geometry with lower z-index, + // or geometry drawn earlier if z-indexes match. + enum class Layer { + NORMAL, // Occluded by geometry with lower Z coordinate + OCCLUDED, // Only drawn over geometry with lower Z coordinate + DEPTH_ONLY, // Like NORMAL, but only affects future occlusion, not color + BACK, // Always drawn below all other geometry + FRONT, // Always drawn above all other geometry + LAST = FRONT + }; + + // The outlines are the collection of all edges that may be drawn. + // Outlines can be classified as emphasized or not; emphasized outlines indicate an abrupt + // change in the surface curvature. These are indicated by the SOutline tag. + // Outlines can also be classified as contour or not; contour outlines indicate the boundary + // of the filled mesh. Whether an outline is a part of contour or not depends on point of view. + enum class DrawOutlinesAs { + EMPHASIZED_AND_CONTOUR = 0, // Both emphasized and contour outlines + EMPHASIZED_WITHOUT_CONTOUR = 1, // Emphasized outlines except those also belonging to contour + CONTOUR_ONLY = 2 // Contour outlines only + }; + + // Stroke widths, etc, can be scale-invariant (in pixels) or scale-dependent (in millimeters). + enum class Unit { + MM, + PX + }; + + class Stroke { + public: + hStroke h; + + Layer layer; + int zIndex; + RgbaColor color; + double width; + Unit unit; + StipplePattern stipplePattern; + double stippleScale; + + void Clear() { *this = {}; } + bool Equals(const Stroke &other) const; + + double WidthMm(const Camera &camera) const; + double WidthPx(const Camera &camera) const; + double StippleScaleMm(const Camera &camera) const; + double StippleScalePx(const Camera &camera) const; + }; + + enum class FillPattern { + SOLID, CHECKERED_A, CHECKERED_B + }; + + class Fill { + public: + hFill h; + + Layer layer; + int zIndex; + RgbaColor color; + FillPattern pattern; + std::shared_ptr texture; + + void Clear() { *this = {}; } + bool Equals(const Fill &other) const; + }; + + IdList strokes; + IdList fills; + BitmapFont bitmapFont; + + Canvas() : strokes(), fills(), bitmapFont() {} + virtual void Clear(); + + hStroke GetStroke(const Stroke &stroke); + hFill GetFill(const Fill &fill); + BitmapFont *GetBitmapFont(); + + virtual const Camera &GetCamera() const = 0; + + virtual void DrawLine(const Vector &a, const Vector &b, hStroke hcs) = 0; + virtual void DrawEdges(const SEdgeList &el, hStroke hcs) = 0; + virtual bool DrawBeziers(const SBezierList &bl, hStroke hcs) = 0; + virtual void DrawOutlines(const SOutlineList &ol, hStroke hcs, DrawOutlinesAs drawAs) = 0; + virtual void DrawVectorText(const std::string &text, double height, + const Vector &o, const Vector &u, const Vector &v, + hStroke hcs) = 0; + + virtual void DrawQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d, + hFill hcf) = 0; + virtual void DrawPoint(const Vector &o, hStroke hcs) = 0; + virtual void DrawPolygon(const SPolygon &p, hFill hcf) = 0; + virtual void DrawMesh(const SMesh &m, hFill hcfFront, hFill hcfBack = {}) = 0; + virtual void DrawFaces(const SMesh &m, const std::vector &faces, hFill hcf) = 0; + + virtual void DrawPixmap(std::shared_ptr pm, + const Vector &o, const Vector &u, const Vector &v, + const Point2d &ta, const Point2d &tb, hFill hcf) = 0; + virtual void InvalidatePixmap(std::shared_ptr pm) = 0; + + virtual std::shared_ptr CreateBatch(); +}; + +// An interface for view-dependent visualization. +class ViewportCanvas : public Canvas { +public: + virtual void SetCamera(const Camera &camera) = 0; + virtual void SetLighting(const Lighting &lighting) = 0; + + virtual void NewFrame() = 0; + virtual void FlushFrame() = 0; + virtual std::shared_ptr ReadFrame() = 0; + + virtual void GetIdent(const char **vendor, const char **renderer, const char **version) = 0; +}; + +// An interface for view-independent visualization. +class BatchCanvas : public Canvas { +public: + const Camera &GetCamera() const override; + + virtual void Finalize() = 0; + virtual void Draw() = 0; +}; + +// A wrapper around Canvas that simplifies drawing UI in screen coordinates. +class UiCanvas { +public: + std::shared_ptr canvas; + bool flip; + + void DrawLine(int x1, int y1, int x2, int y2, RgbaColor color, int width = 1, + int zIndex = 0); + void DrawRect(int l, int r, int t, int b, RgbaColor fillColor, RgbaColor outlineColor, + int zIndex = 0); + void DrawPixmap(std::shared_ptr pm, int x, int y, + int zIndex = 0); + void DrawBitmapChar(char32_t codepoint, int x, int y, RgbaColor color, + int zIndex = 0); + void DrawBitmapText(const std::string &str, int x, int y, RgbaColor color, + int zIndex = 0); + + int Flip(int y) const { return flip ? (int)canvas->GetCamera().height - y : y; } +}; + +// A canvas that performs picking against drawn geometry. +class ObjectPicker : public Canvas { +public: + Camera camera; + // Configuration. + Point2d point; + double selRadius; + // Picking state. + double minDistance; + int maxZIndex; + uint32_t position; + + ObjectPicker() : camera(), point(), selRadius(), + minDistance(), maxZIndex(), position() {} + + const Camera &GetCamera() const override { return camera; } + + void DrawLine(const Vector &a, const Vector &b, hStroke hcs) override; + void DrawEdges(const SEdgeList &el, hStroke hcs) override; + bool DrawBeziers(const SBezierList &bl, hStroke hcs) override { return false; } + void DrawOutlines(const SOutlineList &ol, hStroke hcs, DrawOutlinesAs drawAs) override; + void DrawVectorText(const std::string &text, double height, + const Vector &o, const Vector &u, const Vector &v, + hStroke hcs) override; + + void DrawQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d, + hFill hcf) override; + void DrawPoint(const Vector &o, hStroke hcs) override; + void DrawPolygon(const SPolygon &p, hFill hcf) override; + void DrawMesh(const SMesh &m, hFill hcfFront, hFill hcfBack) override; + void DrawFaces(const SMesh &m, const std::vector &faces, hFill hcf) override; + + void DrawPixmap(std::shared_ptr pm, + const Vector &o, const Vector &u, const Vector &v, + const Point2d &ta, const Point2d &tb, hFill hcf) override; + void InvalidatePixmap(std::shared_ptr pm) override {} + + void DoCompare(double distance, int zIndex, int comparePosition = 0); + void DoQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d, + int zIndex, int comparePosition = 0); + + bool Pick(std::function drawFn); +}; + +// A canvas that renders onto a 2d surface, performing z-index sorting, occlusion testing, etc, +// on the CPU. +class SurfaceRenderer : public Canvas { +public: + Camera camera; + Lighting lighting; + // Chord tolerance, for converting beziers to pwl. + double chordTolerance; + // Render lists. + handle_map edges; + handle_map beziers; + SMesh mesh; + // State. + BBox bbox; + + SurfaceRenderer() : camera(), lighting(), chordTolerance(), mesh(), bbox() {} + void Clear() override; + + // Canvas interface. + const Camera &GetCamera() const override { return camera; } + + void DrawLine(const Vector &a, const Vector &b, hStroke hcs) override; + void DrawEdges(const SEdgeList &el, hStroke hcs) override; + bool DrawBeziers(const SBezierList &bl, hStroke hcs) override; + void DrawOutlines(const SOutlineList &ol, hStroke hcs, DrawOutlinesAs drawAs) override; + void DrawVectorText(const std::string &text, double height, + const Vector &o, const Vector &u, const Vector &v, + hStroke hcs) override; + + void DrawQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d, + hFill hcf) override; + void DrawPoint(const Vector &o, hStroke hcs) override; + void DrawPolygon(const SPolygon &p, hFill hcf) override; + void DrawMesh(const SMesh &m, hFill hcfFront, hFill hcfBack) override; + void DrawFaces(const SMesh &m, const std::vector &faces, hFill hcf) override; + + void DrawPixmap(std::shared_ptr pm, + const Vector &o, const Vector &u, const Vector &v, + const Point2d &ta, const Point2d &tb, hFill hcf) override; + void InvalidatePixmap(std::shared_ptr pm) override; + + // Geometry manipulation. + void CalculateBBox(); + void ConvertBeziersToEdges(); + void CullOccludedStrokes(); + + // Renderer operations. + void OutputInPaintOrder(); + + virtual bool CanOutputCurves() const = 0; + virtual bool CanOutputTriangles() const = 0; + + virtual void OutputStart() = 0; + virtual void OutputBezier(const SBezier &b, hStroke hcs) = 0; + virtual void OutputTriangle(const STriangle &tr) = 0; + virtual void OutputEnd() = 0; + + void OutputBezierAsNonrationalCubic(const SBezier &b, hStroke hcs); +}; + +//----------------------------------------------------------------------------- +// 2d renderers. +//----------------------------------------------------------------------------- + +class CairoRenderer : public SurfaceRenderer { +public: + cairo_t *context; + // Renderer configuration. + bool antialias; + // Renderer state. + struct { + hStroke hcs; + } current; + + CairoRenderer() : context(), current() {} + + void SelectStroke(hStroke hcs); + void MoveTo(Vector p); + void FinishPath(); + + bool CanOutputCurves() const override { return true; } + bool CanOutputTriangles() const override { return true; } + + void OutputStart() override; + void OutputBezier(const SBezier &b, hStroke hcs) override; + void OutputTriangle(const STriangle &tr) override; + void OutputEnd() override; +}; + +//----------------------------------------------------------------------------- +// 3d renderers. +//----------------------------------------------------------------------------- + +// An offscreen renderer based on OpenGL framebuffers. +class GlOffscreen { +public: + unsigned int framebuffer; + unsigned int colorRenderbuffer, depthRenderbuffer; + std::vector data; + + bool Render(int width, int height, std::function renderFn); + void Clear(); +}; + +std::shared_ptr CreateRenderer(); + +#endif diff --git a/src/libslic3r/slvs/resource.h b/src/libslic3r/slvs/resource.h new file mode 100644 index 0000000000..20bf6bd4ea --- /dev/null +++ b/src/libslic3r/slvs/resource.h @@ -0,0 +1,109 @@ +//----------------------------------------------------------------------------- +// Discovery and loading of our resources (icons, fonts, templates, etc). +// +// Copyright 2016 whitequark +//----------------------------------------------------------------------------- + +#ifndef __RESOURCE_H +#define __RESOURCE_H + +class Camera; +class Point2d; +class Pixmap; +class Vector; + +std::string LoadString(const std::string &name); +std::string LoadStringFromGzip(const std::string &name); +std::shared_ptr LoadPng(const std::string &name); + +class Pixmap { +public: + enum class Format { BGRA, RGBA, BGR, RGB, A }; + + Format format; + size_t width; + size_t height; + size_t stride; + std::vector data; + + static std::shared_ptr Create(Format format, size_t width, size_t height); + static std::shared_ptr FromPng(const uint8_t *data, size_t size, bool flip = false); + + static std::shared_ptr ReadPng(FILE *f, bool flip = false); + static std::shared_ptr ReadPng(const Platform::Path &filename, bool flip = false); + bool WritePng(FILE *f, bool flip = false); + bool WritePng(const Platform::Path &filename, bool flip = false); + + size_t GetBytesPerPixel() const; + RgbaColor GetPixel(size_t x, size_t y) const; + bool Equals(const Pixmap &other) const; + + void ConvertTo(Format newFormat); + void SetPixel(size_t x, size_t y, RgbaColor color); +}; + +class BitmapFont { +public: + struct Glyph { + uint8_t advanceCells; + uint16_t position; + }; + + std::string unifontData; + std::map glyphs; + std::shared_ptr texture; + bool textureUpdated; + uint16_t nextPosition; + + static BitmapFont From(std::string &&unifontData); + static BitmapFont Create(); + + bool IsEmpty() const { return unifontData.empty(); } + const Glyph &GetGlyph(char32_t codepoint); + void LocateGlyph(char32_t codepoint, double *s0, double *t0, double *s1, double *t1, + size_t *advanceWidth, size_t *boundingHeight); + + void AddGlyph(char32_t codepoint, std::shared_ptr pixmap); + + size_t GetWidth(char32_t codepoint); + size_t GetWidth(const std::string &str); +}; + +class VectorFont { +public: + struct Contour { + std::vector points; + }; + + struct Glyph { + std::vector contours; + double leftSideBearing; + double boundingWidth; + double advanceWidth; + }; + + std::string lffData; + std::map glyphs; + double rightSideBearing; + double capHeight; + double ascender; + double descender; + + static VectorFont From(std::string &&lffData); + static VectorFont *Builtin(); + + bool IsEmpty() const { return lffData.empty(); } + const Glyph &GetGlyph(char32_t codepoint); + + double GetCapHeight(double forCapHeight) const; + double GetHeight(double forCapHeight) const; + double GetWidth(double forCapHeight, const std::string &str); + Vector GetExtents(double forCapHeight, const std::string &str); + + void Trace(double forCapHeight, Vector o, Vector u, Vector v, const std::string &str, + std::function traceEdge); + void Trace(double forCapHeight, Vector o, Vector u, Vector v, const std::string &str, + std::function traceEdge, const Camera &camera); +}; + +#endif diff --git a/src/libslic3r/slvs/sketch.h b/src/libslic3r/slvs/sketch.h new file mode 100644 index 0000000000..65cfd2ec06 --- /dev/null +++ b/src/libslic3r/slvs/sketch.h @@ -0,0 +1,926 @@ +//----------------------------------------------------------------------------- +// The parametric structure of our sketch, in multiple groups, that generate +// geometric entities and surfaces. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- + +#ifndef __SKETCH_H +#define __SKETCH_H + +class hGroup; +class hRequest; +class hEntity; +class hParam; +class hStyle; +class hConstraint; +class hEquation; + +class Entity; +class Param; +class Equation; +class Style; + +enum class PolyError : uint32_t { + GOOD = 0, + NOT_CLOSED = 1, + NOT_COPLANAR = 2, + SELF_INTERSECTING = 3, + ZERO_LEN_EDGE = 4 +}; + +enum class StipplePattern : uint32_t { + CONTINUOUS = 0, + SHORT_DASH = 1, + DASH = 2, + LONG_DASH = 3, + DASH_DOT = 4, + DASH_DOT_DOT = 5, + DOT = 6, + FREEHAND = 7, + ZIGZAG = 8, + + LAST = ZIGZAG +}; + +const std::vector &StipplePatternDashes(StipplePattern pattern); +double StipplePatternLength(StipplePattern pattern); + +enum class Command : uint32_t; + +// All of the hWhatever handles are a 32-bit ID, that is used to represent +// some data structure in the sketch. +class hGroup { +public: + // bits 15: 0 -- group index + uint32_t v; + + inline hEntity entity(int i) const; + inline hParam param(int i) const; + inline hEquation equation(int i) const; +}; +class hRequest { +public: + // bits 15: 0 -- request index + uint32_t v; + + inline hEntity entity(int i) const; + inline hParam param(int i) const; + + inline bool IsFromReferences() const; +}; +class hEntity { +public: + // bits 15: 0 -- entity index + // 31:16 -- request index + uint32_t v; + + inline bool isFromRequest() const; + inline hRequest request() const; + inline hGroup group() const; + inline hEquation equation(int i) const; +}; +class hParam { +public: + // bits 15: 0 -- param index + // 31:16 -- request index + uint32_t v; + + inline hRequest request() const; +}; + +class hStyle { +public: + uint32_t v; +}; + +class EntityId { +public: + uint32_t v; // entity ID, starting from 0 +}; +class EntityMap { +public: + int tag; + + EntityId h; + hEntity input; + int copyNumber; + // (input, copyNumber) gets mapped to ((Request)xxx).entity(h.v) + + void Clear() {} +}; + +// A set of requests. Every request must have an associated group. +class Group { +public: + static const hGroup HGROUP_REFERENCES; + + int tag; + hGroup h; + + enum class CopyAs { + NUMERIC, + N_TRANS, + N_ROT_AA, + N_ROT_TRANS, + }; + + enum class Type : uint32_t { + DRAWING_3D = 5000, + DRAWING_WORKPLANE = 5001, + EXTRUDE = 5100, + LATHE = 5101, + ROTATE = 5200, + TRANSLATE = 5201, + LINKED = 5300 + }; + Group::Type type; + + int order; + + hGroup opA; + hGroup opB; + bool visible; + bool suppress; + bool relaxConstraints; + bool allowRedundant; + bool allDimsReference; + double scale; + + bool clean; + bool dofCheckOk; + hEntity activeWorkplane; + double valA; + double valB; + double valC; + RgbaColor color; + + struct { + SolveResult how; + int dof; + List remove; + } solved; + + enum class Subtype : uint32_t { + // For drawings in 2d + WORKPLANE_BY_POINT_ORTHO = 6000, + WORKPLANE_BY_LINE_SEGMENTS = 6001, + // For extrudes, translates, and rotates + ONE_SIDED = 7000, + TWO_SIDED = 7001 + }; + Group::Subtype subtype; + + bool skipFirst; // for step and repeat ops + + struct { + Quaternion q; + hEntity origin; + hEntity entityB; + hEntity entityC; + bool swapUV; + bool negateU; + bool negateV; + } predef; + + SPolygon polyLoops; + SBezierLoopSetSet bezierLoops; + SBezierList bezierOpens; + + struct { + PolyError how; + SEdge notClosedAt; + Vector errorPointAt; + } polyError; + + bool booleanFailed; + + SShell thisShell; + SShell runningShell; + + SMesh thisMesh; + SMesh runningMesh; + + bool displayDirty; + SMesh displayMesh; + SOutlineList displayOutlines; + + enum class CombineAs : uint32_t { + UNION = 0, + DIFFERENCE = 1, + ASSEMBLE = 2 + }; + CombineAs meshCombine; + + bool forceToMesh; + + IdList remap; + enum { REMAP_PRIME = 19477 }; + int remapCache[REMAP_PRIME]; + + Platform::Path linkFile; + SMesh impMesh; + SShell impShell; + EntityList impEntity; + + std::string name; + + + void Activate(); + std::string DescriptionString(); + void Clear(); + + static void AddParam(ParamList *param, hParam hp, double v); + void Generate(EntityList *entity, ParamList *param); + bool IsSolvedOkay(); + void TransformImportedBy(Vector t, Quaternion q); + bool IsForcedToMeshBySource() const; + bool IsForcedToMesh() const; + // When a request generates entities from entities, and the source + // entities may have come from multiple requests, it's necessary to + // remap the entity ID so that it's still unique. We do this with a + // mapping list. + enum { + REMAP_LAST = 1000, + REMAP_TOP = 1001, + REMAP_BOTTOM = 1002, + REMAP_PT_TO_LINE = 1003, + REMAP_LINE_TO_FACE = 1004, + REMAP_LATHE_START = 1006, + REMAP_LATHE_END = 1007, + REMAP_PT_TO_ARC = 1008, + REMAP_PT_TO_NORMAL = 1009, + }; + hEntity Remap(hEntity in, int copyNumber); + void MakeExtrusionLines(EntityList *el, hEntity in); + void MakeLatheCircles(IdList *el, IdList *param, hEntity in, Vector pt, Vector axis, int ai); + void MakeExtrusionTopBottomFaces(EntityList *el, hEntity pt); + void CopyEntity(EntityList *el, + Entity *ep, int timesApplied, int remap, + hParam dx, hParam dy, hParam dz, + hParam qw, hParam qvx, hParam qvy, hParam qvz, + CopyAs as); + + void AddEq(IdList *l, Expr *expr, int index); + void GenerateEquations(IdList *l); + bool IsVisible(); + int GetNumConstraints(); + Vector ExtrusionGetVector(); + void ExtrusionForceVectorTo(const Vector &v); + + // Assembling the curves into loops, and into a piecewise linear polygon + // at the same time. + void AssembleLoops(bool *allClosed, bool *allCoplanar, bool *allNonZeroLen); + void GenerateLoops(); + // And the mesh stuff + Group *PreviousGroup() const; + Group *RunningMeshGroup() const; + bool IsMeshGroup(); + + void GenerateShellAndMesh(); + template void GenerateForStepAndRepeat(T *steps, T *outs, Group::CombineAs forWhat); + template void GenerateForBoolean(T *a, T *b, T *o, Group::CombineAs how); + void GenerateDisplayItems(); + + enum class DrawMeshAs { DEFAULT, HOVERED, SELECTED }; + void DrawMesh(DrawMeshAs how, Canvas *canvas); + void Draw(Canvas *canvas); + void DrawPolyError(Canvas *canvas); + void DrawFilledPaths(Canvas *canvas); + void DrawContourAreaLabels(Canvas *canvas); + + SPolygon GetPolygon(); + + static void MenuGroup(Command id); +}; + +// A user request for some primitive or derived operation; for example a +// line, or a step and repeat. +class Request { +public: + // Some predefined requests, that are present in every sketch. + static const hRequest HREQUEST_REFERENCE_XY; + static const hRequest HREQUEST_REFERENCE_YZ; + static const hRequest HREQUEST_REFERENCE_ZX; + + int tag; + hRequest h; + + // Types of requests + enum class Type : uint32_t { + WORKPLANE = 100, + DATUM_POINT = 101, + LINE_SEGMENT = 200, + CUBIC = 300, + CUBIC_PERIODIC = 301, + CIRCLE = 400, + ARC_OF_CIRCLE = 500, + TTF_TEXT = 600, + IMAGE = 700 + }; + + Request::Type type; + int extraPoints; + + hEntity workplane; // or Entity::FREE_IN_3D + hGroup group; + hStyle style; + + bool construction; + + std::string str; + std::string font; + Platform::Path file; + double aspectRatio; + + static hParam AddParam(ParamList *param, hParam hp); + void Generate(EntityList *entity, ParamList *param); + + std::string DescriptionString() const; + int IndexOfPoint(hEntity he) const; + + void Clear() {} +}; + +#define MAX_POINTS_IN_ENTITY (12) +class EntityBase { +public: + int tag; + hEntity h; + + static const hEntity FREE_IN_3D; + static const hEntity NO_ENTITY; + + enum class Type : uint32_t { + POINT_IN_3D = 2000, + POINT_IN_2D = 2001, + POINT_N_TRANS = 2010, + POINT_N_ROT_TRANS = 2011, + POINT_N_COPY = 2012, + POINT_N_ROT_AA = 2013, + + NORMAL_IN_3D = 3000, + NORMAL_IN_2D = 3001, + NORMAL_N_COPY = 3010, + NORMAL_N_ROT = 3011, + NORMAL_N_ROT_AA = 3012, + + DISTANCE = 4000, + DISTANCE_N_COPY = 4001, + + FACE_NORMAL_PT = 5000, + FACE_XPROD = 5001, + FACE_N_ROT_TRANS = 5002, + FACE_N_TRANS = 5003, + FACE_N_ROT_AA = 5004, + + WORKPLANE = 10000, + LINE_SEGMENT = 11000, + CUBIC = 12000, + CUBIC_PERIODIC = 12001, + CIRCLE = 13000, + ARC_OF_CIRCLE = 14000, + TTF_TEXT = 15000, + IMAGE = 16000 + }; + + Type type; + + hGroup group; + hEntity workplane; // or Entity::FREE_IN_3D + + // When it comes time to draw an entity, we look here to get the + // defining variables. + hEntity point[MAX_POINTS_IN_ENTITY]; + int extraPoints; + hEntity normal; + hEntity distance; + // The only types that have their own params are points, normals, + // and directions. + hParam param[7]; + + // Transformed points/normals/distances have their numerical base + Vector numPoint; + Quaternion numNormal; + double numDistance; + + std::string str; + std::string font; + Platform::Path file; + double aspectRatio; + + // For entities that are derived by a transformation, the number of + // times to apply the transformation. + int timesApplied; + + Quaternion GetAxisAngleQuaternion(int param0) const; + ExprQuaternion GetAxisAngleQuaternionExprs(int param0) const; + + bool IsCircle() const; + Expr *CircleGetRadiusExpr() const; + double CircleGetRadiusNum() const; + void ArcGetAngles(double *thetaa, double *thetab, double *dtheta) const; + + bool HasVector() const; + ExprVector VectorGetExprs() const; + ExprVector VectorGetExprsInWorkplane(hEntity wrkpl) const; + Vector VectorGetNum() const; + Vector VectorGetRefPoint() const; + Vector VectorGetStartPoint() const; + + // For distances + bool IsDistance() const; + double DistanceGetNum() const; + Expr *DistanceGetExpr() const; + void DistanceForceTo(double v); + + bool IsWorkplane() const; + // The plane is points P such that P dot (xn, yn, zn) - d = 0 + void WorkplaneGetPlaneExprs(ExprVector *n, Expr **d) const; + ExprVector WorkplaneGetOffsetExprs() const; + Vector WorkplaneGetOffset() const; + EntityBase *Normal() const; + + bool IsFace() const; + ExprVector FaceGetNormalExprs() const; + Vector FaceGetNormalNum() const; + ExprVector FaceGetPointExprs() const; + Vector FaceGetPointNum() const; + + bool IsPoint() const; + // Applies for any of the point types + Vector PointGetNum() const; + ExprVector PointGetExprs() const; + void PointGetExprsInWorkplane(hEntity wrkpl, Expr **u, Expr **v) const; + ExprVector PointGetExprsInWorkplane(hEntity wrkpl) const; + void PointForceTo(Vector v); + void PointForceParamTo(Vector v); + // These apply only the POINT_N_ROT_TRANS, which has an assoc rotation + Quaternion PointGetQuaternion() const; + void PointForceQuaternionTo(Quaternion q); + + bool IsNormal() const; + // Applies for any of the normal types + Quaternion NormalGetNum() const; + ExprQuaternion NormalGetExprs() const; + void NormalForceTo(Quaternion q); + + Vector NormalU() const; + Vector NormalV() const; + Vector NormalN() const; + ExprVector NormalExprsU() const; + ExprVector NormalExprsV() const; + ExprVector NormalExprsN() const; + + Vector CubicGetStartNum() const; + Vector CubicGetFinishNum() const; + ExprVector CubicGetStartTangentExprs() const; + ExprVector CubicGetFinishTangentExprs() const; + Vector CubicGetStartTangentNum() const; + Vector CubicGetFinishTangentNum() const; + + bool HasEndpoints() const; + Vector EndpointStart() const; + Vector EndpointFinish() const; + + void RectGetPointsExprs(ExprVector *eap, ExprVector *ebp) const; + + void AddEq(IdList *l, Expr *expr, int index) const; + void GenerateEquations(IdList *l) const; + + void Clear() {} +}; + +class Entity : public EntityBase { +public: + // Necessary for Entity e = {} to zero-initialize, since + // classes with base classes are not aggregates and + // the default constructor does not initialize members. + // + // Note EntityBase({}); without explicitly value-initializing + // the base class, MSVC2013 will default-initialize it, leaving + // POD members with indeterminate value. + Entity() : EntityBase({}), forceHidden(), actPoint(), actNormal(), + actDistance(), actVisible(), style(), construction(), + beziers(), edges(), edgesChordTol(), screenBBox(), screenBBoxValid() {}; + + // A linked entity that was hidden in the source file ends up hidden + // here too. + bool forceHidden; + + // All points/normals/distances have their numerical value; this is + // a convenience, to simplify the link/assembly code, so that the + // part is entirely described by the entities. + Vector actPoint; + Quaternion actNormal; + double actDistance; + // and the shown state also gets saved here, for later import + bool actVisible; + + hStyle style; + bool construction; + + SBezierList beziers; + SEdgeList edges; + double edgesChordTol; + BBox screenBBox; + bool screenBBoxValid; + + bool IsStylable() const; + bool IsVisible() const; + + enum class DrawAs { DEFAULT, OVERLAY, HIDDEN, HOVERED, SELECTED }; + void Draw(DrawAs how, Canvas *canvas); + void GetReferencePoints(std::vector *refs); + int GetPositionOfPoint(const Camera &camera, Point2d p); + + void ComputeInterpolatingSpline(SBezierList *sbl, bool periodic) const; + void GenerateBezierCurves(SBezierList *sbl) const; + void GenerateEdges(SEdgeList *el); + + SBezierList *GetOrGenerateBezierCurves(); + SEdgeList *GetOrGenerateEdges(); + BBox GetOrGenerateScreenBBox(bool *hasBBox); + + void CalculateNumerical(bool forExport); + + std::string DescriptionString() const; + + void Clear() { + beziers.l.Clear(); + edges.l.Clear(); + } +}; + +class EntReqTable { +public: + static bool GetRequestInfo(Request::Type req, int extraPoints, + EntityBase::Type *ent, int *pts, bool *hasNormal, bool *hasDistance); + static bool GetEntityInfo(EntityBase::Type ent, int extraPoints, + Request::Type *req, int *pts, bool *hasNormal, bool *hasDistance); + static Request::Type GetRequestForEntity(EntityBase::Type ent); +}; + +class Param { +public: + int tag; + hParam h; + + double val; + bool known; + bool free; + + // Used only in the solver + hParam substd; + + static const hParam NO_PARAM; + + void Clear() {} +}; + + +class hConstraint { +public: + uint32_t v; + + inline hEquation equation(int i) const; + inline hParam param(int i) const; +}; + +class ConstraintBase { +public: + int tag; + hConstraint h; + + static const hConstraint NO_CONSTRAINT; + + enum class Type : uint32_t { + POINTS_COINCIDENT = 20, + PT_PT_DISTANCE = 30, + PT_PLANE_DISTANCE = 31, + PT_LINE_DISTANCE = 32, + PT_FACE_DISTANCE = 33, + PROJ_PT_DISTANCE = 34, + PT_IN_PLANE = 41, + PT_ON_LINE = 42, + PT_ON_FACE = 43, + EQUAL_LENGTH_LINES = 50, + LENGTH_RATIO = 51, + EQ_LEN_PT_LINE_D = 52, + EQ_PT_LN_DISTANCES = 53, + EQUAL_ANGLE = 54, + EQUAL_LINE_ARC_LEN = 55, + LENGTH_DIFFERENCE = 56, + SYMMETRIC = 60, + SYMMETRIC_HORIZ = 61, + SYMMETRIC_VERT = 62, + SYMMETRIC_LINE = 63, + AT_MIDPOINT = 70, + HORIZONTAL = 80, + VERTICAL = 81, + DIAMETER = 90, + PT_ON_CIRCLE = 100, + SAME_ORIENTATION = 110, + ANGLE = 120, + PARALLEL = 121, + PERPENDICULAR = 122, + ARC_LINE_TANGENT = 123, + CUBIC_LINE_TANGENT = 124, + CURVE_CURVE_TANGENT = 125, + EQUAL_RADIUS = 130, + WHERE_DRAGGED = 200, + + COMMENT = 1000 + }; + + Type type; + + hGroup group; + hEntity workplane; + + // These are the parameters for the constraint. + double valA; + hParam valP; + hEntity ptA; + hEntity ptB; + hEntity entityA; + hEntity entityB; + hEntity entityC; + hEntity entityD; + bool other; + bool other2; + + bool reference; // a ref dimension, that generates no eqs + std::string comment; // since comments are represented as constraints + + bool HasLabel() const; + + void Generate(IdList *param); + + void GenerateEquations(IdList *entity, + bool forReference = false) const; + // Some helpers when generating symbolic constraint equations + void ModifyToSatisfy(); + void AddEq(IdList *l, Expr *expr, int index) const; + void AddEq(IdList *l, const ExprVector &v, int baseIndex = 0) const; + static Expr *DirectionCosine(hEntity wrkpl, ExprVector ae, ExprVector be); + static Expr *Distance(hEntity workplane, hEntity pa, hEntity pb); + static Expr *PointLineDistance(hEntity workplane, hEntity pt, hEntity ln); + static Expr *PointPlaneDistance(ExprVector p, hEntity plane); + static ExprVector VectorsParallel3d(ExprVector a, ExprVector b, hParam p); + static ExprVector PointInThreeSpace(hEntity workplane, Expr *u, Expr *v); + + void Clear() {} +}; + +class Constraint : public ConstraintBase { +public: + // See Entity::Entity(). + Constraint() : ConstraintBase({}), disp() {} + + // These define how the constraint is drawn on-screen. + struct { + Vector offset; + hStyle style; + } disp; + + bool IsVisible() const; + bool IsStylable() const; + hStyle GetStyle() const; + bool HasLabel() const; + std::string Label() const; + + enum class DrawAs { DEFAULT, HOVERED, SELECTED }; + void Draw(DrawAs how, Canvas *canvas); + Vector GetLabelPos(const Camera &camera); + void GetReferencePoints(const Camera &camera, std::vector *refs); + + void DoLayout(DrawAs how, Canvas *canvas, + Vector *labelPos, std::vector *refs); + void DoLine(Canvas *canvas, Canvas::hStroke hcs, Vector a, Vector b); + void DoStippledLine(Canvas *canvas, Canvas::hStroke hcs, Vector a, Vector b); + bool DoLineExtend(Canvas *canvas, Canvas::hStroke hcs, + Vector p0, Vector p1, Vector pt, double salient); + void DoArcForAngle(Canvas *canvas, Canvas::hStroke hcs, + Vector a0, Vector da, Vector b0, Vector db, + Vector offset, Vector *ref, bool trim); + void DoArrow(Canvas *canvas, Canvas::hStroke hcs, + Vector p, Vector dir, Vector n, double width, double angle, double da); + void DoLineWithArrows(Canvas *canvas, Canvas::hStroke hcs, + Vector ref, Vector a, Vector b, bool onlyOneExt); + int DoLineTrimmedAgainstBox(Canvas *canvas, Canvas::hStroke hcs, + Vector ref, Vector a, Vector b, bool extend, + Vector gr, Vector gu, double swidth, double sheight); + int DoLineTrimmedAgainstBox(Canvas *canvas, Canvas::hStroke hcs, + Vector ref, Vector a, Vector b, bool extend = true); + void DoLabel(Canvas *canvas, Canvas::hStroke hcs, + Vector ref, Vector *labelPos, Vector gr, Vector gu); + void DoProjectedPoint(Canvas *canvas, Canvas::hStroke hcs, Vector *p); + void DoProjectedPoint(Canvas *canvas, Canvas::hStroke hcs, Vector *p, Vector n, Vector o); + + void DoEqualLenTicks(Canvas *canvas, Canvas::hStroke hcs, + Vector a, Vector b, Vector gn, Vector *refp); + void DoEqualRadiusTicks(Canvas *canvas, Canvas::hStroke hcs, + hEntity he, Vector *refp); + + std::string DescriptionString() const; + + static hConstraint AddConstraint(Constraint *c, bool rememberForUndo); + static hConstraint AddConstraint(Constraint *c); + static void MenuConstrain(Command id); + static void DeleteAllConstraintsFor(Constraint::Type type, hEntity entityA, hEntity ptA); + + static hConstraint ConstrainCoincident(hEntity ptA, hEntity ptB); + static hConstraint Constrain(Constraint::Type type, hEntity ptA, hEntity ptB, hEntity entityA); + static hConstraint Constrain(Constraint::Type type, hEntity ptA, hEntity ptB, + hEntity entityA, hEntity entityB, + bool other, bool other2); +}; + +class hEquation { +public: + uint32_t v; + + inline bool isFromConstraint() const; + inline hConstraint constraint() const; +}; + +class Equation { +public: + int tag; + hEquation h; + + Expr *e; + + void Clear() {} +}; + + +class Style { +public: + int tag; + hStyle h; + + enum { + // If an entity has no style, then it will be colored according to + // whether the group that it's in is active or not, whether it's + // construction or not, and so on. + NO_STYLE = 0, + + ACTIVE_GRP = 1, + CONSTRUCTION = 2, + INACTIVE_GRP = 3, + DATUM = 4, + SOLID_EDGE = 5, + CONSTRAINT = 6, + SELECTED = 7, + HOVERED = 8, + CONTOUR_FILL = 9, + NORMALS = 10, + ANALYZE = 11, + DRAW_ERROR = 12, + DIM_SOLID = 13, + HIDDEN_EDGE = 14, + OUTLINE = 15, + + FIRST_CUSTOM = 0x100 + }; + + std::string name; + + enum class UnitsAs : uint32_t { + PIXELS = 0, + MM = 1 + }; + double width; + UnitsAs widthAs; + double textHeight; + UnitsAs textHeightAs; + enum class TextOrigin : uint32_t { + NONE = 0x00, + LEFT = 0x01, + RIGHT = 0x02, + BOT = 0x04, + TOP = 0x08 + }; + TextOrigin textOrigin; + double textAngle; + RgbaColor color; + bool filled; + RgbaColor fillColor; + bool visible; + bool exportable; + StipplePattern stippleType; + double stippleScale; + int zIndex; + + // The default styles, for entities that don't have a style assigned yet, + // and for datums and such. + typedef struct { + hStyle h; + const char *cnfPrefix; + RgbaColor color; + double width; + int zIndex; + } Default; + static const Default Defaults[]; + + static std::string CnfColor(const std::string &prefix); + static std::string CnfWidth(const std::string &prefix); + static std::string CnfTextHeight(const std::string &prefix); + static std::string CnfPrefixToName(const std::string &prefix); + + static void CreateAllDefaultStyles(); + static void CreateDefaultStyle(hStyle h); + static void FillDefaultStyle(Style *s, const Default *d = NULL, bool factory = false); + static void FreezeDefaultStyles(); + static void LoadFactoryDefaults(); + + static void AssignSelectionToStyle(uint32_t v); + static uint32_t CreateCustomStyle(bool rememberForUndo = true); + + static RgbaColor RewriteColor(RgbaColor rgb); + + static Style *Get(hStyle hs); + static RgbaColor Color(hStyle hs, bool forExport=false); + static RgbaColor Color(int hs, bool forExport=false); + static RgbaColor FillColor(hStyle hs, bool forExport=false); + static double Width(hStyle hs); + static double Width(int hs); + static double WidthMm(int hs); + static double TextHeight(hStyle hs); + static double DefaultTextHeight(); + static Canvas::Stroke Stroke(hStyle hs); + static Canvas::Stroke Stroke(int hs); + static bool Exportable(int hs); + static hStyle ForEntity(hEntity he); + static StipplePattern PatternType(hStyle hs); + static double StippleScaleMm(hStyle hs); + + std::string DescriptionString() const; + + void Clear() {} +}; + + +inline hEntity hGroup::entity(int i) const + { hEntity r; r.v = 0x80000000 | (v << 16) | (uint32_t)i; return r; } +inline hParam hGroup::param(int i) const + { hParam r; r.v = 0x80000000 | (v << 16) | (uint32_t)i; return r; } +inline hEquation hGroup::equation(int i) const + { hEquation r; r.v = (v << 16) | 0x80000000 | (uint32_t)i; return r; } + +inline bool hRequest::IsFromReferences() const { + if(v == Request::HREQUEST_REFERENCE_XY.v) return true; + if(v == Request::HREQUEST_REFERENCE_YZ.v) return true; + if(v == Request::HREQUEST_REFERENCE_ZX.v) return true; + return false; +} +inline hEntity hRequest::entity(int i) const + { hEntity r; r.v = (v << 16) | (uint32_t)i; return r; } +inline hParam hRequest::param(int i) const + { hParam r; r.v = (v << 16) | (uint32_t)i; return r; } + +inline bool hEntity::isFromRequest() const + { if(v & 0x80000000) return false; else return true; } +inline hRequest hEntity::request() const + { hRequest r; r.v = (v >> 16); return r; } +inline hGroup hEntity::group() const + { hGroup r; r.v = (v >> 16) & 0x3fff; return r; } +inline hEquation hEntity::equation(int i) const + { hEquation r; r.v = v | 0x40000000 | (uint32_t)i; return r; } + +inline hRequest hParam::request() const + { hRequest r; r.v = (v >> 16); return r; } + + +inline hEquation hConstraint::equation(int i) const + { hEquation r; r.v = (v << 16) | (uint32_t)i; return r; } +inline hParam hConstraint::param(int i) const + { hParam r; r.v = v | 0x40000000 | (uint32_t)i; return r; } + +inline bool hEquation::isFromConstraint() const + { if(v & 0xc0000000) return false; else return true; } +inline hConstraint hEquation::constraint() const + { hConstraint r; r.v = (v >> 16); return r; } + +// The format for entities stored on the clipboard. +class ClipboardRequest { +public: + Request::Type type; + int extraPoints; + hStyle style; + std::string str; + std::string font; + Platform::Path file; + bool construction; + + Vector point[MAX_POINTS_IN_ENTITY]; + double distance; + + hEntity oldEnt; + hEntity oldPointEnt[MAX_POINTS_IN_ENTITY]; + hRequest newReq; +}; + +#endif diff --git a/src/libslic3r/slvs/solvespace.h b/src/libslic3r/slvs/solvespace.h new file mode 100644 index 0000000000..c04e05d063 --- /dev/null +++ b/src/libslic3r/slvs/solvespace.h @@ -0,0 +1,935 @@ +//----------------------------------------------------------------------------- +// All declarations not grouped specially elsewhere. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- + +#ifndef __SOLVESPACE_H +#define __SOLVESPACE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// We declare these in advance instead of simply using FT_Library +// (defined as typedef FT_LibraryRec_* FT_Library) because including +// freetype.h invokes indescribable horrors and we would like to avoid +// doing that every time we include solvespace.h. + +#if FULL_LIB_JJS +struct FT_LibraryRec_; +struct FT_FaceRec_; +#endif + +typedef struct _cairo cairo_t; + +// The few floating-point equality comparisons in SolveSpace have been +// carefully considered, so we disable the -Wfloat-equal warning for them +#ifdef __clang__ +# define EXACT(expr) \ + (_Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wfloat-equal\"") \ + (expr) \ + _Pragma("clang diagnostic pop")) +#else +# define EXACT(expr) (expr) +#endif + +// Debugging functions +#if defined(__GNUC__) +#define ssassert(condition, message) \ + do { \ + if(__builtin_expect((condition), true) == false) { \ + SolveSpace::assert_failure(__FILE__, __LINE__, __func__, #condition, message); \ + __builtin_unreachable(); \ + } \ + } while(0) +#else +#define ssassert(condition, message) \ + do { \ + if((condition) == false) { \ + SolveSpace::assert_failure(__FILE__, __LINE__, __func__, #condition, message); \ + abort(); \ + } \ + } while(0) +#endif + +#ifndef isnan +# define isnan(x) (((x) != (x)) || (x > 1e11) || (x < -1e11)) +#endif + +namespace SolveSpace { + +using std::min; +using std::max; +using std::swap; + +#if defined(__GNUC__) +__attribute__((noreturn)) +#endif +void assert_failure(const char *file, unsigned line, const char *function, + const char *condition, const char *message); + +#if defined(__GNUC__) +__attribute__((__format__ (__printf__, 1, 2))) +#endif +std::string ssprintf(const char *fmt, ...); + +#if FULL_LIB_JJS +inline int WRAP(int v, int n) { + // Clamp it to the range [0, n) + while(v >= n) v -= n; + while(v < 0) v += n; + return v; +} +inline double WRAP_NOT_0(double v, double n) { + // Clamp it to the range (0, n] + while(v > n) v -= n; + while(v <= 0) v += n; + return v; +} +inline double WRAP_SYMMETRIC(double v, double n) { + // Clamp it to the range (-n/2, n/2] + while(v > n/2) v -= n; + while(v <= -n/2) v += n; + return v; +} + +#endif +// Why is this faster than the library function? +inline double ffabs(double v) { return (v > 0) ? v : (-v); } + +#if FULL_LIB_JJS +#define CO(v) (v).x, (v).y, (v).z +#endif + +#define ANGLE_COS_EPS (1e-6) +#define LENGTH_EPS (1e-6) +#define VERY_POSITIVE (1e10) +#define VERY_NEGATIVE (-1e10) + +#if FULL_LIB_JJS +inline double Random(double vmax) { + return (vmax*rand()) / RAND_MAX; +} +#endif + +class Expr; +class ExprVector; +class ExprQuaternion; +class RgbaColor; + +#if FULL_LIB_JJS +enum class Command : uint32_t; +enum class ContextCommand : uint32_t; +#endif + +//================ +// From the platform-specific code. + +#include "platform/platform.h" + +#if FULL_LIB_JJS +const size_t MAX_RECENT = 8; +extern Platform::Path RecentFile[MAX_RECENT]; +void RefreshRecentMenus(); + +enum DialogChoice { DIALOG_YES = 1, DIALOG_NO = -1, DIALOG_CANCEL = 0 }; +DialogChoice SaveFileYesNoCancel(); +DialogChoice LoadAutosaveYesNo(); +DialogChoice LocateImportedFileYesNoCancel(const Platform::Path &filename, + bool canCancel); + +#define AUTOSAVE_EXT "slvs~" + +enum class Unit : uint32_t { + MM = 0, + INCHES +}; + +#endif + +#if FULL_LIB_JJS +struct FileFilter; + +bool GetSaveFile(Platform::Path *filename, const std::string &defExtension, + const FileFilter filters[]); +bool GetOpenFile(Platform::Path *filename, const std::string &defExtension, + const FileFilter filters[]); +std::vector GetFontFiles(); + +void OpenWebsite(const char *url); + +void RefreshLocale(); + +void CheckMenuByCmd(Command id, bool checked); +void RadioMenuByCmd(Command id, bool selected); +void EnableMenuByCmd(Command id, bool enabled); + +void ShowGraphicsEditControl(int x, int y, int fontHeight, int minWidthChars, + const std::string &str); +void HideGraphicsEditControl(); +bool GraphicsEditControlIsVisible(); +void ShowTextEditControl(int x, int y, const std::string &str); +void HideTextEditControl(); +bool TextEditControlIsVisible(); +void MoveTextScrollbarTo(int pos, int maxPos, int page); + +void AddContextMenuItem(const char *legend, ContextCommand id); +void CreateContextSubmenu(); +ContextCommand ShowContextMenu(); + +void ShowTextWindow(bool visible); +void InvalidateText(); +void InvalidateGraphics(); +void PaintGraphics(); +void ToggleFullScreen(); +bool FullScreenIsActive(); +void GetGraphicsWindowSize(int *w, int *h); +void GetTextWindowSize(int *w, int *h); +double GetScreenDpi(); +int64_t GetMilliseconds(); + +#endif + +void dbp(const char *str, ...); +#if FULL_LIB_JJS +#define DBPTRI(tri) \ + dbp("tri: (%.3f %.3f %.3f) (%.3f %.3f %.3f) (%.3f %.3f %.3f)", \ + CO((tri).a), CO((tri).b), CO((tri).c)) + +void SetCurrentFilename(const Platform::Path &filename); +void SetMousePointerToHand(bool yes); + +#endif +void DoMessageBox(const char *str, int rows, int cols, bool error); +#if FULL_LIB_JJS +void SetTimerFor(int milliseconds); +void SetAutosaveTimerFor(int minutes); +void ScheduleLater(); +void ExitNow(); + +#endif + +void CnfFreezeInt(uint32_t val, const std::string &name); +#if FULL_LIB_JJS +void CnfFreezeFloat(float val, const std::string &name); +void CnfFreezeString(const std::string &val, const std::string &name); +std::string CnfThawString(const std::string &val, const std::string &name); + +#endif +uint32_t CnfThawInt(uint32_t val, const std::string &name); +#if FULL_LIB_JJS +float CnfThawFloat(float val, const std::string &name); +#endif + +std::vector InitPlatform(int argc, char **argv); + +void *AllocTemporary(size_t n); +void FreeTemporary(void *p); +void FreeAllTemporary(); +void *MemAlloc(size_t n); +void MemFree(void *p); + +#if FULL_LIB_JJS +void vl(); // debug function to validate heaps +#endif + +#include "resource.h" + +// End of platform-specific functions +//================ + +template +struct CompareHandle { + bool operator()(T lhs, T rhs) const { return lhs.v < rhs.v; } +}; + +template +using handle_map = std::map>; + + +class Group; +class SSurface; +#include "dsc.h" +#include "polygon.h" +#include "srf/surface.h" +#include "render/render.h" + +class Entity; +class hEntity; +class Param; +class hParam; +typedef IdList EntityList; +typedef IdList ParamList; + +enum class SolveResult : uint32_t { + OKAY = 0, + DIDNT_CONVERGE = 10, + REDUNDANT_OKAY = 11, + REDUNDANT_DIDNT_CONVERGE = 12, + TOO_MANY_UNKNOWNS = 20 +}; + + +#include "sketch.h" +#if FULL_LIB_JJS +#include "ui.h" +#endif +#include "expr.h" + +#if FULL_LIB_JJS + +// Utility functions that are provided in the platform-independent code. +class utf8_iterator : std::iterator { + const char *p, *n; +public: + utf8_iterator(const char *p) : p(p), n(NULL) {} + bool operator==(const utf8_iterator &i) const { return p==i.p; } + bool operator!=(const utf8_iterator &i) const { return p!=i.p; } + ptrdiff_t operator- (const utf8_iterator &i) const { return p -i.p; } + utf8_iterator& operator++() { **this; p=n; n=NULL; return *this; } + utf8_iterator operator++(int) { utf8_iterator t(*this); operator++(); return t; } + char32_t operator*(); +}; + + +class ReadUTF8 { + const std::string &str; +public: + ReadUTF8(const std::string &str) : str(str) {} + utf8_iterator begin() const { return utf8_iterator(&str[0]); } + utf8_iterator end() const { return utf8_iterator(&str[str.length()]); } +}; +#endif + +#define arraylen(x) (sizeof((x))/sizeof((x)[0])) +#define PI (3.1415926535897931) +void MakeMatrix(double *mat, double a11, double a12, double a13, double a14, + double a21, double a22, double a23, double a24, + double a31, double a32, double a33, double a34, + double a41, double a42, double a43, double a44); +void MultMatrix(double *mata, double *matb, double *matr); + +std::string MakeAcceleratorLabel(int accel); +void Message(const char *str, ...); +void Error(const char *str, ...); +void CnfFreezeBool(bool v, const std::string &name); +void CnfFreezeColor(RgbaColor v, const std::string &name); +bool CnfThawBool(bool v, const std::string &name); +RgbaColor CnfThawColor(RgbaColor v, const std::string &name); + +class System { +public: + enum { MAX_UNKNOWNS = 1024 }; + + EntityList entity; + ParamList param; + IdList eq; + + // A list of parameters that are being dragged; these are the ones that + // we should put as close as possible to their initial positions. + List dragged; + + enum { + // In general, the tag indicates the subsys that a variable/equation + // has been assigned to; these are exceptions for variables: + VAR_SUBSTITUTED = 10000, + VAR_DOF_TEST = 10001, + // and for equations: + EQ_SUBSTITUTED = 20000 + }; + + // The system Jacobian matrix + struct { + // The corresponding equation for each row + hEquation eq[MAX_UNKNOWNS]; + + // The corresponding parameter for each column + hParam param[MAX_UNKNOWNS]; + + // We're solving AX = B + int m, n; + struct { + Expr *sym[MAX_UNKNOWNS][MAX_UNKNOWNS]; + double num[MAX_UNKNOWNS][MAX_UNKNOWNS]; + } A; + + double scale[MAX_UNKNOWNS]; + + // Some helpers for the least squares solve + double AAt[MAX_UNKNOWNS][MAX_UNKNOWNS]; + double Z[MAX_UNKNOWNS]; + + double X[MAX_UNKNOWNS]; + + struct { + Expr *sym[MAX_UNKNOWNS]; + double num[MAX_UNKNOWNS]; + } B; + } mat; + + static const double RANK_MAG_TOLERANCE, CONVERGE_TOLERANCE; + int CalculateRank(); + bool TestRank(); + static bool SolveLinearSystem(double X[], double A[][MAX_UNKNOWNS], + double B[], int N); + bool SolveLeastSquares(); + + bool WriteJacobian(int tag); + void EvalJacobian(); + + void WriteEquationsExceptFor(hConstraint hc, Group *g); + void FindWhichToRemoveToFixJacobian(Group *g, List *bad, bool forceDofCheck); + void SolveBySubstitution(); + + bool IsDragged(hParam p); + + bool NewtonSolve(int tag); + + void MarkParamsFree(bool findFree); + int CalculateDof(); + + SolveResult Solve(Group *g, int *dof, List *bad, + bool andFindBad, bool andFindFree, bool forceDofCheck = false); + + SolveResult SolveRank(Group *g, int *dof, List *bad, + bool andFindBad, bool andFindFree, bool forceDofCheck = false); + + void Clear(); +}; + +#if FULL_LIB_JJS +#include "ttf.h" + +class StepFileWriter { +public: + void ExportSurfacesTo(const Platform::Path &filename); + void WriteHeader(); + void WriteProductHeader(); + int ExportCurve(SBezier *sb); + int ExportCurveLoop(SBezierLoop *loop, bool inner); + void ExportSurface(SSurface *ss, SBezierList *sbl); + void WriteWireframe(); + void WriteFooter(); + + List curves; + List advancedFaces; + FILE *f; + int id; +}; + +class VectorFileWriter { +protected: + Vector u, v, n, origin; + double cameraTan, scale; + +public: + FILE *f; + Platform::Path filename; + Vector ptMin, ptMax; + + static double MmToPts(double mm); + + static VectorFileWriter *ForFile(const Platform::Path &filename); + + void SetModelviewProjection(const Vector &u, const Vector &v, const Vector &n, + const Vector &origin, double cameraTan, double scale); + Vector Transform(Vector &pos) const; + + void OutputLinesAndMesh(SBezierLoopSetSet *sblss, SMesh *sm); + + void BezierAsPwl(SBezier *sb); + void BezierAsNonrationalCubic(SBezier *sb, int depth=0); + + virtual void StartPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) = 0; + virtual void FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) = 0; + virtual void Bezier(SBezier *sb) = 0; + virtual void Triangle(STriangle *tr) = 0; + virtual bool OutputConstraints(IdList *) { return false; } + virtual void StartFile() = 0; + virtual void FinishAndCloseFile() = 0; + virtual bool HasCanvasSize() const = 0; + virtual bool CanOutputMesh() const = 0; +}; +class DxfFileWriter : public VectorFileWriter { +public: + struct BezierPath { + std::vector beziers; + }; + + std::vector paths; + IdList *constraint; + + static const char *lineTypeName(StipplePattern stippleType); + + bool OutputConstraints(IdList *constraint) override; + + void StartPath( RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void Triangle(STriangle *tr) override; + void Bezier(SBezier *sb) override; + void StartFile() override; + void FinishAndCloseFile() override; + bool HasCanvasSize() const override { return false; } + bool CanOutputMesh() const override { return false; } + bool NeedToOutput(Constraint *c); +}; +class EpsFileWriter : public VectorFileWriter { +public: + Vector prevPt; + void MaybeMoveTo(Vector s, Vector f); + + void StartPath( RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void Triangle(STriangle *tr) override; + void Bezier(SBezier *sb) override; + void StartFile() override; + void FinishAndCloseFile() override; + bool HasCanvasSize() const override { return true; } + bool CanOutputMesh() const override { return true; } +}; +class PdfFileWriter : public VectorFileWriter { +public: + uint32_t xref[10]; + uint32_t bodyStart; + Vector prevPt; + void MaybeMoveTo(Vector s, Vector f); + + void StartPath( RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void Triangle(STriangle *tr) override; + void Bezier(SBezier *sb) override; + void StartFile() override; + void FinishAndCloseFile() override; + bool HasCanvasSize() const override { return true; } + bool CanOutputMesh() const override { return true; } +}; +class SvgFileWriter : public VectorFileWriter { +public: + Vector prevPt; + void MaybeMoveTo(Vector s, Vector f); + + void StartPath( RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void Triangle(STriangle *tr) override; + void Bezier(SBezier *sb) override; + void StartFile() override; + void FinishAndCloseFile() override; + bool HasCanvasSize() const override { return true; } + bool CanOutputMesh() const override { return true; } +}; +class HpglFileWriter : public VectorFileWriter { +public: + static double MmToHpglUnits(double mm); + void StartPath( RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void Triangle(STriangle *tr) override; + void Bezier(SBezier *sb) override; + void StartFile() override; + void FinishAndCloseFile() override; + bool HasCanvasSize() const override { return false; } + bool CanOutputMesh() const override { return false; } +}; +class Step2dFileWriter : public VectorFileWriter { + StepFileWriter sfw; + void StartPath( RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void Triangle(STriangle *tr) override; + void Bezier(SBezier *sb) override; + void StartFile() override; + void FinishAndCloseFile() override; + bool HasCanvasSize() const override { return false; } + bool CanOutputMesh() const override { return false; } +}; +class GCodeFileWriter : public VectorFileWriter { +public: + SEdgeList sel; + void StartPath( RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void Triangle(STriangle *tr) override; + void Bezier(SBezier *sb) override; + void StartFile() override; + void FinishAndCloseFile() override; + bool HasCanvasSize() const override { return false; } + bool CanOutputMesh() const override { return false; } +}; +#endif + +#ifdef LIBRARY +# define ENTITY EntityBase +# define CONSTRAINT ConstraintBase +#else +# define ENTITY Entity +# define CONSTRAINT Constraint +#endif +class Sketch { +public: + // These are user-editable, and define the sketch. + IdList group; + List
groupOrder; + IdList constraint; + IdList request; + IdList style; + + // These are generated from the above. + IdList entity; + IdList param; + + inline CONSTRAINT *GetConstraint(hConstraint h) + { return constraint.FindById(h); } + inline ENTITY *GetEntity (hEntity h) { return entity. FindById(h); } + inline Param *GetParam (hParam h) { return param. FindById(h); } + inline Request *GetRequest(hRequest h) { return request.FindById(h); } + inline Group *GetGroup (hGroup h) { return group. FindById(h); } + // Styles are handled a bit differently. + + void Clear(); + + BBox CalculateEntityBBox(bool includingInvisible); + Group *GetRunningMeshGroupFor(hGroup h); +}; +#undef ENTITY +#undef CONSTRAINT + +#if FULL_LIB_JJS + +class SolveSpaceUI { +public: + TextWindow *pTW; + TextWindow &TW; + GraphicsWindow GW; + + // The state for undo/redo + typedef struct { + IdList group; + List
groupOrder; + IdList request; + IdList constraint; + IdList param; + IdList style; + hGroup activeGroup; + + void Clear() { + group.Clear(); + request.Clear(); + constraint.Clear(); + param.Clear(); + style.Clear(); + } + } UndoState; + enum { MAX_UNDO = 16 }; + typedef struct { + UndoState d[MAX_UNDO]; + int cnt; + int write; + } UndoStack; + UndoStack undo; + UndoStack redo; + + std::map, Platform::PathLess> images; + bool ReloadLinkedImage(const Platform::Path &saveFile, Platform::Path *filename, + bool canCancel); + + void UndoEnableMenus(); + void UndoRemember(); + void UndoUndo(); + void UndoRedo(); + void PushFromCurrentOnto(UndoStack *uk); + void PopOntoCurrentFrom(UndoStack *uk); + void UndoClearState(UndoState *ut); + void UndoClearStack(UndoStack *uk); + + // Little bits of extra configuration state + enum { MODEL_COLORS = 8 }; + RgbaColor modelColor[MODEL_COLORS]; + Vector lightDir[2]; + double lightIntensity[2]; + double ambientIntensity; + double chordTol; + double chordTolCalculated; + int maxSegments; + double exportChordTol; + int exportMaxSegments; + double cameraTangent; + float gridSpacing; + float exportScale; + float exportOffset; + bool fixExportColors; + bool drawBackFaces; + bool showContourAreas; + bool checkClosedContour; + bool showToolbar; + Platform::Path screenshotFile; + RgbaColor backgroundColor; + bool exportShadedTriangles; + bool exportPwlCurves; + bool exportCanvasSizeAuto; + bool exportMode; + struct { + float left; + float right; + float bottom; + float top; + } exportMargin; + struct { + float width; + float height; + float dx; + float dy; + } exportCanvas; + struct { + float depth; + int passes; + float feed; + float plungeFeed; + } gCode; + + Unit viewUnits; + int afterDecimalMm; + int afterDecimalInch; + int autosaveInterval; // in minutes + + std::string MmToString(double v); + double ExprToMm(Expr *e); + double StringToMm(const std::string &s); + const char *UnitName(); + double MmPerUnit(); + int UnitDigitsAfterDecimal(); + void SetUnitDigitsAfterDecimal(int v); + double ChordTolMm(); + double ExportChordTolMm(); + int GetMaxSegments(); + bool usePerspectiveProj; + double CameraTangent(); + + // Some stuff relating to the tangent arcs created non-parametrically + // as special requests. + double tangentArcRadius; + bool tangentArcManual; + bool tangentArcDeleteOld; + + // The platform-dependent code calls this before entering the msg loop + void Init(); + bool Load(const Platform::Path &filename); + void Exit(); + + // File load/save routines, including the additional files that get + // loaded when we have link groups. + FILE *fh; + void AfterNewFile(); + static void RemoveFromRecentList(const Platform::Path &filename); + static void AddToRecentList(const Platform::Path &filename); + Platform::Path saveFile; + bool fileLoadError; + bool unsaved; + typedef struct { + char type; + const char *desc; + char fmt; + void *ptr; + } SaveTable; + static const SaveTable SAVED[]; + void SaveUsingTable(const Platform::Path &filename, int type); + void LoadUsingTable(const Platform::Path &filename, char *key, char *val); + struct { + Group g; + Request r; + Entity e; + Param p; + Constraint c; + Style s; + } sv; + static void MenuFile(Command id); + bool Autosave(); + void RemoveAutosave(); + bool GetFilenameAndSave(bool saveAs); + bool OkayToStartNewFile(); + hGroup CreateDefaultDrawingGroup(); + void UpdateWindowTitle(); + void ClearExisting(); + void NewFile(); + bool SaveToFile(const Platform::Path &filename); + bool LoadAutosaveFor(const Platform::Path &filename); + bool LoadFromFile(const Platform::Path &filename, bool canCancel = false); + void UpgradeLegacyData(); + bool LoadEntitiesFromFile(const Platform::Path &filename, EntityList *le, + SMesh *m, SShell *sh); + bool ReloadAllLinked(const Platform::Path &filename, bool canCancel = false); + // And the various export options + void ExportAsPngTo(const Platform::Path &filename); + void ExportMeshTo(const Platform::Path &filename); + void ExportMeshAsStlTo(FILE *f, SMesh *sm); + void ExportMeshAsObjTo(FILE *fObj, FILE *fMtl, SMesh *sm); + void ExportMeshAsThreeJsTo(FILE *f, const Platform::Path &filename, + SMesh *sm, SOutlineList *sol); + void ExportViewOrWireframeTo(const Platform::Path &filename, bool exportWireframe); + void ExportSectionTo(const Platform::Path &filename); + void ExportWireframeCurves(SEdgeList *sel, SBezierList *sbl, + VectorFileWriter *out); + void ExportLinesAndMesh(SEdgeList *sel, SBezierList *sbl, SMesh *sm, + Vector u, Vector v, + Vector n, Vector origin, + double cameraTan, + VectorFileWriter *out); + + static void MenuAnalyze(Command id); + + // Additional display stuff + struct { + SContour path; + hEntity point; + } traced; + SEdgeList nakedEdges; + struct { + bool draw; + Vector ptA; + Vector ptB; + } extraLine; + struct { + bool draw, showOrigin; + Vector pt, u, v; + } justExportedInfo; + struct { + bool draw; + bool dirty; + Vector position; + } centerOfMass; + + class Clipboard { + public: + List r; + List c; + + void Clear(); + bool ContainsEntity(hEntity old); + hEntity NewEntityFor(hEntity old); + }; + Clipboard clipboard; + + void MarkGroupDirty(hGroup hg, bool onlyThis = false); + void MarkGroupDirtyByEntity(hEntity he); + + // Consistency checking on the sketch: stuff with missing dependencies + // will get deleted automatically. + struct { + int requests; + int groups; + int constraints; + int nonTrivialConstraints; + } deleted; + bool GroupExists(hGroup hg); + bool PruneOrphans(); + bool EntityExists(hEntity he); + bool GroupsInOrder(hGroup before, hGroup after); + bool PruneGroups(hGroup hg); + bool PruneRequests(hGroup hg); + bool PruneConstraints(hGroup hg); + static void ShowNakedEdges(bool reportOnlyWhenNotOkay); + + enum class Generate : uint32_t { + DIRTY, + ALL, + REGEN, + UNTIL_ACTIVE, + }; + + void GenerateAll(Generate type = Generate::DIRTY, bool andFindFree = false, + bool genForBBox = false); + void SolveGroup(hGroup hg, bool andFindFree); + void SolveGroupAndReport(hGroup hg, bool andFindFree); + SolveResult TestRankForGroup(hGroup hg); + void WriteEqSystemForGroup(hGroup hg); + void MarkDraggedParams(); + void ForceReferences(); + void UpdateCenterOfMass(); + + bool ActiveGroupsOkay(); + + // The system to be solved. + System *pSys; + System &sys; + + // All the TrueType fonts in memory + TtfFontList fonts; + + // Everything has been pruned, so we know there's no dangling references + // to entities that don't exist. Before that, we mustn't try to display + // the sketch! + bool allConsistent; + + struct { + bool scheduled; + bool showTW; + bool generateAll; + } later; + void ScheduleShowTW(); + void ScheduleGenerateAll(); + void DoLater(); + + static void MenuHelp(Command id); + + void Clear(); + + // We allocate TW and sys on the heap to work around an MSVC problem + // where it puts zero-initialized global data in the binary (~30M of zeroes) + // in release builds. + SolveSpaceUI() + : pTW(new TextWindow({})), TW(*pTW), + pSys(new System({})), sys(*pSys) {} + + ~SolveSpaceUI() { + delete pTW; + delete pSys; + } +}; +void ImportDxf(const Platform::Path &file); +void ImportDwg(const Platform::Path &file); + +extern SolveSpaceUI SS; +#endif +extern Sketch SK; + +} + +#ifndef __OBJC__ +using namespace SolveSpace; +#endif + +#endif diff --git a/src/libslic3r/slvs/srf/surface.h b/src/libslic3r/slvs/srf/surface.h new file mode 100644 index 0000000000..98ad820873 --- /dev/null +++ b/src/libslic3r/slvs/srf/surface.h @@ -0,0 +1,429 @@ +//----------------------------------------------------------------------------- +// Functions relating to rational polynomial surfaces, which are trimmed by +// curves (either rational polynomial curves, or piecewise linear +// approximations to curves of intersection that can't be represented +// exactly in ratpoly form), and assembled into watertight shells. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- + +#ifndef __SURFACE_H +#define __SURFACE_H + +// Utility functions, Bernstein polynomials of order 1-3 and their derivatives. +double Bernstein(int k, int deg, double t); +double BernsteinDerivative(int k, int deg, double t); + +class SBezierList; +class SSurface; +class SCurvePt; + +// Utility data structure, a two-dimensional BSP to accelerate polygon +// operations. +class SBspUv { +public: + Point2d a, b; + + SBspUv *pos; + SBspUv *neg; + + SBspUv *more; + + enum class Class : uint32_t { + INSIDE = 100, + OUTSIDE = 200, + EDGE_PARALLEL = 300, + EDGE_ANTIPARALLEL = 400, + EDGE_OTHER = 500 + }; + + static SBspUv *Alloc(); + static SBspUv *From(SEdgeList *el, SSurface *srf); + + void ScalePoints(Point2d *pt, Point2d *a, Point2d *b, SSurface *srf) const; + double ScaledSignedDistanceToLine(Point2d pt, Point2d a, Point2d b, + SSurface *srf) const; + double ScaledDistanceToLine(Point2d pt, Point2d a, Point2d b, bool asSegment, + SSurface *srf) const; + + void InsertEdge(Point2d a, Point2d b, SSurface *srf); + static SBspUv *InsertOrCreateEdge(SBspUv *where, Point2d ea, Point2d eb, SSurface *srf); + Class ClassifyPoint(Point2d p, Point2d eb, SSurface *srf) const; + Class ClassifyEdge(Point2d ea, Point2d eb, SSurface *srf) const; + double MinimumDistanceToEdge(Point2d p, SSurface *srf) const; +}; + +// Now the data structures to represent a shell of trimmed rational polynomial +// surfaces. + +class SShell; + +class hSSurface { +public: + uint32_t v; +}; + +class hSCurve { +public: + uint32_t v; +}; + +// Stuff for rational polynomial curves, of degree one to three. These are +// our inputs, and are also calculated for certain exact surface-surface +// intersections. +class SBezier { +public: + int tag; + int auxA, auxB; + + int deg; + Vector ctrl[4]; + double weight[4]; + uint32_t entity; + + Vector PointAt(double t) const; + Vector TangentAt(double t) const; + void ClosestPointTo(Vector p, double *t, bool mustConverge=true) const; + void SplitAt(double t, SBezier *bef, SBezier *aft) const; + bool PointOnThisAndCurve(const SBezier *sbb, Vector *p) const; + + Vector Start() const; + Vector Finish() const; + bool Equals(SBezier *b) const; + void MakePwlInto(SEdgeList *sel, double chordTol=0) const; + void MakePwlInto(List *l, double chordTol=0) const; + void MakePwlInto(SContour *sc, double chordTol=0) const; + void MakePwlInto(List *l, double chordTol=0) const; + void MakePwlWorker(List *l, double ta, double tb, double chordTol) const; + void MakePwlInitialWorker(List *l, double ta, double tb, double chordTol) const; + void MakeNonrationalCubicInto(SBezierList *bl, double tolerance, int depth = 0) const; + + void AllIntersectionsWith(const SBezier *sbb, SPointList *spl) const; + void GetBoundingProjd(Vector u, Vector orig, double *umin, double *umax) const; + void Reverse(); + + bool IsInPlane(Vector n, double d) const; + bool IsCircle(Vector axis, Vector *center, double *r) const; + bool IsRational() const; + + SBezier TransformedBy(Vector t, Quaternion q, double scale) const; + SBezier InPerspective(Vector u, Vector v, Vector n, + Vector origin, double cameraTan) const; + void ScaleSelfBy(double s); + + static SBezier From(Vector p0, Vector p1, Vector p2, Vector p3); + static SBezier From(Vector p0, Vector p1, Vector p2); + static SBezier From(Vector p0, Vector p1); + static SBezier From(Vector4 p0, Vector4 p1, Vector4 p2, Vector4 p3); + static SBezier From(Vector4 p0, Vector4 p1, Vector4 p2); + static SBezier From(Vector4 p0, Vector4 p1); +}; + +class SBezierList { +public: + List l; + + void Clear(); + void ScaleSelfBy(double s); + void CullIdenticalBeziers(); + void AllIntersectionsWith(SBezierList *sblb, SPointList *spl) const; + bool GetPlaneContainingBeziers(Vector *p, Vector *u, Vector *v, + Vector *notCoplanarAt) const; +}; + +class SBezierLoop { +public: + int tag; + List l; + + inline void Clear() { l.Clear(); } + bool IsClosed() const; + void Reverse(); + void MakePwlInto(SContour *sc, double chordTol=0) const; + void GetBoundingProjd(Vector u, Vector orig, double *umin, double *umax) const; + + static SBezierLoop FromCurves(SBezierList *spcl, + bool *allClosed, SEdge *errorAt); +}; + +class SBezierLoopSet { +public: + List l; + Vector normal; + Vector point; + double area; + + static SBezierLoopSet From(SBezierList *spcl, SPolygon *poly, + double chordTol, + bool *allClosed, SEdge *errorAt, + SBezierList *openContours); + + void GetBoundingProjd(Vector u, Vector orig, double *umin, double *umax) const; + double SignedArea(); + void MakePwlInto(SPolygon *sp) const; + void Clear(); +}; + +class SBezierLoopSetSet { +public: + List l; + + void FindOuterFacesFrom(SBezierList *sbl, SPolygon *spxyz, SSurface *srfuv, + double chordTol, + bool *allClosed, SEdge *notClosedAt, + bool *allCoplanar, Vector *notCoplanarAt, + SBezierList *openContours); + void AddOpenPath(SBezier *sb); + void Clear(); +}; + +// Stuff for the surface trim curves: piecewise linear +class SCurvePt { +public: + int tag; + Vector p; + bool vertex; +}; + +class SCurve { +public: + hSCurve h; + + // In a Boolean, C = A op B. The curves in A and B get copied into C, and + // therefore must get new hSCurves assigned. For the curves in A and B, + // we use newH to record their new handle in C. + hSCurve newH; + enum class Source : uint32_t { + A = 100, + B = 200, + INTERSECTION = 300 + }; + Source source; + + bool isExact; + SBezier exact; + + List pts; + + hSSurface surfA; + hSSurface surfB; + + static SCurve FromTransformationOf(SCurve *a, Vector t, + Quaternion q, double scale); + SCurve MakeCopySplitAgainst(SShell *agnstA, SShell *agnstB, + SSurface *srfA, SSurface *srfB) const; + void RemoveShortSegments(SSurface *srfA, SSurface *srfB); + SSurface *GetSurfaceA(SShell *a, SShell *b) const; + SSurface *GetSurfaceB(SShell *a, SShell *b) const; + + void Clear(); +}; + +// A segment of a curve by which a surface is trimmed: indicates which curve, +// by its handle, and the starting and ending points of our segment of it. +// The vector out points out of the surface; it, the surface outer normal, +// and a tangent to the beginning of the curve are all orthogonal. +class STrimBy { +public: + hSCurve curve; + bool backwards; + // If a trim runs backwards, then start and finish still correspond to + // the actual start and finish, but they appear in reverse order in + // the referenced curve. + Vector start; + Vector finish; + + static STrimBy EntireCurve(SShell *shell, hSCurve hsc, bool backwards); +}; + +// An intersection point between a line and a surface +class SInter { +public: + int tag; + Vector p; + SSurface *srf; + Point2d pinter; + Vector surfNormal; // of the intersecting surface, at pinter + bool onEdge; // pinter is on edge of trim poly +}; + +// A rational polynomial surface in Bezier form. +class SSurface { +public: + + enum class CombineAs : uint32_t { + UNION = 10, + DIFFERENCE = 11, + INTERSECT = 12 + }; + + int tag; + hSSurface h; + + // Same as newH for the curves; record what a surface gets renamed to + // when I copy things over. + hSSurface newH; + + RgbaColor color; + uint32_t face; + + int degm, degn; + Vector ctrl[4][4]; + double weight[4][4]; + + List trim; + + // For testing whether a point (u, v) on the surface lies inside the trim + SBspUv *bsp; + SEdgeList edges; + + // For caching our initial (u, v) when doing Newton iterations to project + // a point into our surface. + Point2d cached; + + static SSurface FromExtrusionOf(SBezier *spc, Vector t0, Vector t1); + static SSurface FromRevolutionOf(SBezier *sb, Vector pt, Vector axis, + double thetas, double thetaf); + static SSurface FromPlane(Vector pt, Vector u, Vector v); + static SSurface FromTransformationOf(SSurface *a, Vector t, Quaternion q, + double scale, + bool includingTrims); + void ScaleSelfBy(double s); + + void EdgeNormalsWithinSurface(Point2d auv, Point2d buv, + Vector *pt, Vector *enin, Vector *enout, + Vector *surfn, + uint32_t auxA, + SShell *shell, SShell *sha, SShell *shb); + void FindChainAvoiding(SEdgeList *src, SEdgeList *dest, SPointList *avoid); + SSurface MakeCopyTrimAgainst(SShell *parent, SShell *a, SShell *b, + SShell *into, SSurface::CombineAs type); + void TrimFromEdgeList(SEdgeList *el, bool asUv); + void IntersectAgainst(SSurface *b, SShell *agnstA, SShell *agnstB, + SShell *into); + void AddExactIntersectionCurve(SBezier *sb, SSurface *srfB, + SShell *agnstA, SShell *agnstB, SShell *into); + + typedef struct { + int tag; + Point2d p; + } Inter; + void WeightControlPoints(); + void UnWeightControlPoints(); + void CopyRowOrCol(bool row, int this_ij, SSurface *src, int src_ij); + void BlendRowOrCol(bool row, int this_ij, SSurface *a, int a_ij, + SSurface *b, int b_ij); + double DepartureFromCoplanar() const; + void SplitInHalf(bool byU, SSurface *sa, SSurface *sb); + void AllPointsIntersecting(Vector a, Vector b, + List *l, + bool asSegment, bool trimmed, bool inclTangent); + void AllPointsIntersectingUntrimmed(Vector a, Vector b, + int *cnt, int *level, + List *l, bool asSegment, + SSurface *sorig); + + void ClosestPointTo(Vector p, Point2d *puv, bool mustConverge=true); + void ClosestPointTo(Vector p, double *u, double *v, bool mustConverge=true); + bool ClosestPointNewton(Vector p, double *u, double *v, bool mustConverge=true) const; + + bool PointIntersectingLine(Vector p0, Vector p1, double *u, double *v) const; + Vector ClosestPointOnThisAndSurface(SSurface *srf2, Vector p); + void PointOnSurfaces(SSurface *s1, SSurface *s2, double *u, double *v); + Vector PointAt(double u, double v) const; + Vector PointAt(Point2d puv) const; + void TangentsAt(double u, double v, Vector *tu, Vector *tv) const; + Vector NormalAt(Point2d puv) const; + Vector NormalAt(double u, double v) const; + bool LineEntirelyOutsideBbox(Vector a, Vector b, bool asSegment) const; + void GetAxisAlignedBounding(Vector *ptMax, Vector *ptMin) const; + bool CoincidentWithPlane(Vector n, double d) const; + bool CoincidentWith(SSurface *ss, bool sameNormal) const; + bool IsExtrusion(SBezier *of, Vector *along) const; + bool IsCylinder(Vector *axis, Vector *center, double *r, + Vector *start, Vector *finish) const; + + void TriangulateInto(SShell *shell, SMesh *sm); + + // these are intended as bitmasks, even though there's just one now + enum class MakeAs : uint32_t { + UV = 0x01, + XYZ = 0x00 + }; + void MakeTrimEdgesInto(SEdgeList *sel, MakeAs flags, SCurve *sc, STrimBy *stb); + void MakeEdgesInto(SShell *shell, SEdgeList *sel, MakeAs flags, + SShell *useCurvesFrom=NULL); + + Vector ExactSurfaceTangentAt(Vector p, SSurface *srfA, SSurface *srfB, + Vector dir); + void MakeSectionEdgesInto(SShell *shell, SEdgeList *sel, SBezierList *sbl); + void MakeClassifyingBsp(SShell *shell, SShell *useCurvesFrom); + double ChordToleranceForEdge(Vector a, Vector b) const; + void MakeTriangulationGridInto(List *l, double vs, double vf, + bool swapped) const; + Vector PointAtMaybeSwapped(double u, double v, bool swapped) const; + + void Reverse(); + void Clear(); +}; + +class SShell { +public: + IdList curve; + IdList surface; + + bool booleanFailed; + + void MakeFromExtrusionOf(SBezierLoopSet *sbls, Vector t0, Vector t1, + RgbaColor color); + void MakeFromRevolutionOf(SBezierLoopSet *sbls, Vector pt, Vector axis, + RgbaColor color, Group *group); + + void MakeFromUnionOf(SShell *a, SShell *b); + void MakeFromDifferenceOf(SShell *a, SShell *b); + void MakeFromBoolean(SShell *a, SShell *b, SSurface::CombineAs type); + void CopyCurvesSplitAgainst(bool opA, SShell *agnst, SShell *into); + void CopySurfacesTrimAgainst(SShell *sha, SShell *shb, SShell *into, SSurface::CombineAs type); + void MakeIntersectionCurvesAgainst(SShell *against, SShell *into); + void MakeClassifyingBsps(SShell *useCurvesFrom); + void AllPointsIntersecting(Vector a, Vector b, List *il, + bool asSegment, bool trimmed, bool inclTangent); + void MakeCoincidentEdgesInto(SSurface *proto, bool sameNormal, + SEdgeList *el, SShell *useCurvesFrom); + void RewriteSurfaceHandlesForCurves(SShell *a, SShell *b); + void CleanupAfterBoolean(); + + // Definitions when classifying regions of a surface; it is either inside, + // outside, or coincident (with parallel or antiparallel normal) with a + // shell. + enum class Class : uint32_t { + INSIDE = 100, + OUTSIDE = 200, + COINC_SAME = 300, + COINC_OPP = 400 + }; + static const double DOTP_TOL; + Class ClassifyRegion(Vector edge_n, Vector inter_surf_n, + Vector edge_surf_n) const; + + bool ClassifyEdge(Class *indir, Class *outdir, + Vector ea, Vector eb, + Vector p, Vector edge_n_in, + Vector edge_n_out, Vector surf_n); + + void MakeFromCopyOf(SShell *a); + void MakeFromTransformationOf(SShell *a, + Vector trans, Quaternion q, double scale); + void MakeFromAssemblyOf(SShell *a, SShell *b); + void MergeCoincidentSurfaces(); + + void TriangulateInto(SMesh *sm); + void MakeEdgesInto(SEdgeList *sel); + void MakeSectionEdgesInto(Vector n, double d, SEdgeList *sel, SBezierList *sbl); + bool IsEmpty() const; + void RemapFaces(Group *g, int remap); + void Clear(); +}; + +#endif + diff --git a/src/libslic3r/slvs/system.cpp b/src/libslic3r/slvs/system.cpp new file mode 100644 index 0000000000..e90611a9f2 --- /dev/null +++ b/src/libslic3r/slvs/system.cpp @@ -0,0 +1,587 @@ +//----------------------------------------------------------------------------- +// Once we've written our constraint equations in the symbolic algebra system, +// these routines linearize them, and solve by a modified Newton's method. +// This also contains the routines to detect non-convergence or inconsistency, +// and report diagnostics to the user. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +// This tolerance is used to determine whether two (linearized) constraints +// are linearly dependent. If this is too small, then we will attempt to +// solve truly inconsistent systems and fail. But if it's too large, then +// we will give up on legitimate systems like a skinny right angle triangle by +// its hypotenuse and long side. +const double System::RANK_MAG_TOLERANCE = 1e-4; + +// The solver will converge all unknowns to within this tolerance. This must +// always be much less than LENGTH_EPS, and in practice should be much less. +const double System::CONVERGE_TOLERANCE = (LENGTH_EPS/(1e2)); + +bool System::WriteJacobian(int tag) { + int a, i, j; + + j = 0; + for(a = 0; a < param.n; a++) { + if(j >= MAX_UNKNOWNS) return false; + + Param *p = &(param.elem[a]); + if(p->tag != tag) continue; + mat.param[j] = p->h; + j++; + } + mat.n = j; + + i = 0; + for(a = 0; a < eq.n; a++) { + if(i >= MAX_UNKNOWNS) return false; + + Equation *e = &(eq.elem[a]); + if(e->tag != tag) continue; + + mat.eq[i] = e->h; + Expr *f = e->e->DeepCopyWithParamsAsPointers(¶m, &(SK.param)); + f = f->FoldConstants(); + + // Hash table (61 bits) to accelerate generation of zero partials. + uint64_t scoreboard = f->ParamsUsed(); + for(j = 0; j < mat.n; j++) { + Expr *pd; + if(scoreboard & ((uint64_t)1 << (mat.param[j].v % 61)) && + f->DependsOn(mat.param[j])) + { + pd = f->PartialWrt(mat.param[j]); + pd = pd->FoldConstants(); + pd = pd->DeepCopyWithParamsAsPointers(¶m, &(SK.param)); + } else { + pd = Expr::From(0.0); + } + mat.A.sym[i][j] = pd; + } + mat.B.sym[i] = f; + i++; + } + mat.m = i; + + return true; +} + +void System::EvalJacobian() { + int i, j; + for(i = 0; i < mat.m; i++) { + for(j = 0; j < mat.n; j++) { + mat.A.num[i][j] = (mat.A.sym[i][j])->Eval(); + } + } +} + +bool System::IsDragged(hParam p) { + hParam *pp; + for(pp = dragged.First(); pp; pp = dragged.NextAfter(pp)) { + if(p.v == pp->v) return true; + } + return false; +} + +void System::SolveBySubstitution() { + int i; + for(i = 0; i < eq.n; i++) { + Equation *teq = &(eq.elem[i]); + Expr *tex = teq->e; + + if(tex->op == Expr::Op::MINUS && + tex->a->op == Expr::Op::PARAM && + tex->b->op == Expr::Op::PARAM) + { + hParam a = tex->a->parh; + hParam b = tex->b->parh; + if(!(param.FindByIdNoOops(a) && param.FindByIdNoOops(b))) { + // Don't substitute unless they're both solver params; + // otherwise it's an equation that can be solved immediately, + // or an error to flag later. + continue; + } + + if(IsDragged(a)) { + // A is being dragged, so A should stay, and B should go + hParam t = a; + a = b; + b = t; + } + + int j; + for(j = 0; j < eq.n; j++) { + Equation *req = &(eq.elem[j]); + (req->e)->Substitute(a, b); // A becomes B, B unchanged + } + for(j = 0; j < param.n; j++) { + Param *rp = &(param.elem[j]); + if(rp->substd.v == a.v) { + rp->substd = b; + } + } + Param *ptr = param.FindById(a); + ptr->tag = VAR_SUBSTITUTED; + ptr->substd = b; + + teq->tag = EQ_SUBSTITUTED; + } + } +} + +//----------------------------------------------------------------------------- +// Calculate the rank of the Jacobian matrix, by Gram-Schimdt orthogonalization +// in place. A row (~equation) is considered to be all zeros if its magnitude +// is less than the tolerance RANK_MAG_TOLERANCE. +//----------------------------------------------------------------------------- +int System::CalculateRank() { + // Actually work with magnitudes squared, not the magnitudes + double rowMag[MAX_UNKNOWNS] = {}; + double tol = RANK_MAG_TOLERANCE*RANK_MAG_TOLERANCE; + + int i, iprev, j; + int rank = 0; + + for(i = 0; i < mat.m; i++) { + // Subtract off this row's component in the direction of any + // previous rows + for(iprev = 0; iprev < i; iprev++) { + if(rowMag[iprev] <= tol) continue; // ignore zero rows + + double dot = 0; + for(j = 0; j < mat.n; j++) { + dot += (mat.A.num[iprev][j]) * (mat.A.num[i][j]); + } + for(j = 0; j < mat.n; j++) { + mat.A.num[i][j] -= (dot/rowMag[iprev])*mat.A.num[iprev][j]; + } + } + // Our row is now normal to all previous rows; calculate the + // magnitude of what's left + double mag = 0; + for(j = 0; j < mat.n; j++) { + mag += (mat.A.num[i][j]) * (mat.A.num[i][j]); + } + if(mag > tol) { + rank++; + } + rowMag[i] = mag; + } + + return rank; +} + +bool System::TestRank() { + EvalJacobian(); + return CalculateRank() == mat.m; +} + +bool System::SolveLinearSystem(double X[], double A[][MAX_UNKNOWNS], + double B[], int n) +{ + // Gaussian elimination, with partial pivoting. It's an error if the + // matrix is singular, because that means two constraints are + // equivalent. + int i, j, ip, jp, imax = 0; + double max, temp; + + for(i = 0; i < n; i++) { + // We are trying eliminate the term in column i, for rows i+1 and + // greater. First, find a pivot (between rows i and N-1). + max = 0; + for(ip = i; ip < n; ip++) { + if(ffabs(A[ip][i]) > max) { + imax = ip; + max = ffabs(A[ip][i]); + } + } + // Don't give up on a singular matrix unless it's really bad; the + // assumption code is responsible for identifying that condition, + // so we're not responsible for reporting that error. + if(ffabs(max) < 1e-20) continue; + + // Swap row imax with row i + for(jp = 0; jp < n; jp++) { + swap(A[i][jp], A[imax][jp]); + } + swap(B[i], B[imax]); + + // For rows i+1 and greater, eliminate the term in column i. + for(ip = i+1; ip < n; ip++) { + temp = A[ip][i]/A[i][i]; + + for(jp = i; jp < n; jp++) { + A[ip][jp] -= temp*(A[i][jp]); + } + B[ip] -= temp*B[i]; + } + } + + // We've put the matrix in upper triangular form, so at this point we + // can solve by back-substitution. + for(i = n - 1; i >= 0; i--) { + if(ffabs(A[i][i]) < 1e-20) continue; + + temp = B[i]; + for(j = n - 1; j > i; j--) { + temp -= X[j]*A[i][j]; + } + X[i] = temp / A[i][i]; + } + + return true; +} + +bool System::SolveLeastSquares() { + int r, c, i; + + // Scale the columns; this scale weights the parameters for the least + // squares solve, so that we can encourage the solver to make bigger + // changes in some parameters, and smaller in others. + for(c = 0; c < mat.n; c++) { + if(IsDragged(mat.param[c])) { + // It's least squares, so this parameter doesn't need to be all + // that big to get a large effect. + mat.scale[c] = 1/20.0; + } else { + mat.scale[c] = 1; + } + for(r = 0; r < mat.m; r++) { + mat.A.num[r][c] *= mat.scale[c]; + } + } + + // Write A*A' + for(r = 0; r < mat.m; r++) { + for(c = 0; c < mat.m; c++) { // yes, AAt is square + double sum = 0; + for(i = 0; i < mat.n; i++) { + sum += mat.A.num[r][i]*mat.A.num[c][i]; + } + mat.AAt[r][c] = sum; + } + } + + if(!SolveLinearSystem(mat.Z, mat.AAt, mat.B.num, mat.m)) return false; + + // And multiply that by A' to get our solution. + for(c = 0; c < mat.n; c++) { + double sum = 0; + for(i = 0; i < mat.m; i++) { + sum += mat.A.num[i][c]*mat.Z[i]; + } + mat.X[c] = sum * mat.scale[c]; + } + return true; +} + +bool System::NewtonSolve(int tag) { + + int iter = 0; + bool converged = false; + int i; + + // Evaluate the functions at our operating point. + for(i = 0; i < mat.m; i++) { + mat.B.num[i] = (mat.B.sym[i])->Eval(); + } + do { + // And evaluate the Jacobian at our initial operating point. + EvalJacobian(); + + if(!SolveLeastSquares()) break; + + // Take the Newton step; + // J(x_n) (x_{n+1} - x_n) = 0 - F(x_n) + for(i = 0; i < mat.n; i++) { + Param *p = param.FindById(mat.param[i]); + p->val -= mat.X[i]; + if(isnan(p->val)) { + // Very bad, and clearly not convergent + return false; + } + } + + // Re-evalute the functions, since the params have just changed. + for(i = 0; i < mat.m; i++) { + mat.B.num[i] = (mat.B.sym[i])->Eval(); + } + // Check for convergence + converged = true; + for(i = 0; i < mat.m; i++) { + if(isnan(mat.B.num[i])) { + return false; + } + if(ffabs(mat.B.num[i]) > CONVERGE_TOLERANCE) { + converged = false; + break; + } + } + } while(iter++ < 50 && !converged); + + return converged; +} + +void System::WriteEquationsExceptFor(hConstraint hc, Group *g) { + int i; + // Generate all the equations from constraints in this group + for(i = 0; i < SK.constraint.n; i++) { + ConstraintBase *c = &(SK.constraint.elem[i]); + if(c->group.v != g->h.v) continue; + if(c->h.v == hc.v) continue; + + if(c->HasLabel() && c->type != Constraint::Type::COMMENT && + g->allDimsReference) + { + // When all dimensions are reference, we adjust them to display + // the correct value, and then don't generate any equations. + c->ModifyToSatisfy(); + continue; + } + if(g->relaxConstraints && c->type != Constraint::Type::POINTS_COINCIDENT) { + // When the constraints are relaxed, we keep only the point- + // coincident constraints, and the constraints generated by + // the entities and groups. + continue; + } + + c->GenerateEquations(&eq); + } + // And the equations from entities + for(i = 0; i < SK.entity.n; i++) { + EntityBase *e = &(SK.entity.elem[i]); + if(e->group.v != g->h.v) continue; + + e->GenerateEquations(&eq); + } + // And from the groups themselves + g->GenerateEquations(&eq); +} + +void System::FindWhichToRemoveToFixJacobian(Group *g, List *bad, bool forceDofCheck) { + int a, i; + + for(a = 0; a < 2; a++) { + for(i = 0; i < SK.constraint.n; i++) { + ConstraintBase *c = &(SK.constraint.elem[i]); + if(c->group.v != g->h.v) continue; + if((c->type == Constraint::Type::POINTS_COINCIDENT && a == 0) || + (c->type != Constraint::Type::POINTS_COINCIDENT && a == 1)) + { + // Do the constraints in two passes: first everything but + // the point-coincident constraints, then only those + // constraints (so they appear last in the list). + continue; + } + + param.ClearTags(); + eq.Clear(); + WriteEquationsExceptFor(c->h, g); + eq.ClearTags(); + + // It's a major speedup to solve the easy ones by substitution here, + // and that doesn't break anything. + if(!forceDofCheck) { + SolveBySubstitution(); + } + + WriteJacobian(0); + EvalJacobian(); + + int rank = CalculateRank(); + if(rank == mat.m) { + // We fixed it by removing this constraint + bad->Add(&(c->h)); + } + } + } +} + +SolveResult System::Solve(Group *g, int *dof, List *bad, + bool andFindBad, bool andFindFree, bool forceDofCheck) +{ + WriteEquationsExceptFor(Constraint::NO_CONSTRAINT, g); + + int i; + bool rankOk; + +/* + dbp("%d equations", eq.n); + for(i = 0; i < eq.n; i++) { + dbp(" %.3f = %s = 0", eq.elem[i].e->Eval(), eq.elem[i].e->Print()); + } + dbp("%d parameters", param.n); + for(i = 0; i < param.n; i++) { + dbp(" param %08x at %.3f", param.elem[i].h.v, param.elem[i].val); + } */ + + // All params and equations are assigned to group zero. + param.ClearTags(); + eq.ClearTags(); + + if(!forceDofCheck) { + SolveBySubstitution(); + } + + // Before solving the big system, see if we can find any equations that + // are soluble alone. This can be a huge speedup. We don't know whether + // the system is consistent yet, but if it isn't then we'll catch that + // later. + int alone = 1; + for(i = 0; i < eq.n; i++) { + Equation *e = &(eq.elem[i]); + if(e->tag != 0) continue; + + hParam hp = e->e->ReferencedParams(¶m); + if(hp.v == Expr::NO_PARAMS.v) continue; + if(hp.v == Expr::MULTIPLE_PARAMS.v) continue; + + Param *p = param.FindById(hp); + if(p->tag != 0) continue; // let rank test catch inconsistency + + e->tag = alone; + p->tag = alone; + WriteJacobian(alone); + if(!NewtonSolve(alone)) { + // We don't do the rank test, so let's arbitrarily return + // the DIDNT_CONVERGE result here. + rankOk = true; + // Failed to converge, bail out early + goto didnt_converge; + } + alone++; + } + + // Now write the Jacobian for what's left, and do a rank test; that + // tells us if the system is inconsistently constrained. + if(!WriteJacobian(0)) { + return SolveResult::TOO_MANY_UNKNOWNS; + } + + rankOk = TestRank(); + + // And do the leftovers as one big system + if(!NewtonSolve(0)) { + goto didnt_converge; + } + + rankOk = TestRank(); + if(!rankOk) { + if(!g->allowRedundant) { + if(andFindBad) FindWhichToRemoveToFixJacobian(g, bad, forceDofCheck); + } + } else { + // This is not the full Jacobian, but any substitutions or single-eq + // solves removed one equation and one unknown, therefore no effect + // on the number of DOF. + if(dof) *dof = CalculateDof(); + MarkParamsFree(andFindFree); + } + // System solved correctly, so write the new values back in to the + // main parameter table. + for(i = 0; i < param.n; i++) { + Param *p = &(param.elem[i]); + double val; + if(p->tag == VAR_SUBSTITUTED) { + val = param.FindById(p->substd)->val; + } else { + val = p->val; + } + Param *pp = SK.GetParam(p->h); + pp->val = val; + pp->known = true; + pp->free = p->free; + } + return rankOk ? SolveResult::OKAY : SolveResult::REDUNDANT_OKAY; + +didnt_converge: + SK.constraint.ClearTags(); + for(i = 0; i < eq.n; i++) { + if(ffabs(mat.B.num[i]) > CONVERGE_TOLERANCE || isnan(mat.B.num[i])) { + // This constraint is unsatisfied. + if(!mat.eq[i].isFromConstraint()) continue; + + hConstraint hc = mat.eq[i].constraint(); + ConstraintBase *c = SK.constraint.FindByIdNoOops(hc); + if(!c) continue; + // Don't double-show constraints that generated multiple + // unsatisfied equations + if(!c->tag) { + bad->Add(&(c->h)); + c->tag = 1; + } + } + } + + return rankOk ? SolveResult::DIDNT_CONVERGE : SolveResult::REDUNDANT_DIDNT_CONVERGE; +} + +SolveResult System::SolveRank(Group *g, int *dof, List *bad, + bool andFindBad, bool andFindFree, bool forceDofCheck) +{ + WriteEquationsExceptFor(Constraint::NO_CONSTRAINT, g); + + // All params and equations are assigned to group zero. + param.ClearTags(); + eq.ClearTags(); + + if(!forceDofCheck) { + SolveBySubstitution(); + } + + // Now write the Jacobian, and do a rank test; that + // tells us if the system is inconsistently constrained. + if(!WriteJacobian(0)) { + return SolveResult::TOO_MANY_UNKNOWNS; + } + + bool rankOk = TestRank(); + if(!rankOk) { + if(!g->allowRedundant) { + if(andFindBad) FindWhichToRemoveToFixJacobian(g, bad, forceDofCheck); + } + } else { + // This is not the full Jacobian, but any substitutions or single-eq + // solves removed one equation and one unknown, therefore no effect + // on the number of DOF. + if(dof) *dof = CalculateDof(); + MarkParamsFree(andFindFree); + } + return rankOk ? SolveResult::OKAY : SolveResult::REDUNDANT_OKAY; +} + +void System::Clear() { + entity.Clear(); + param.Clear(); + eq.Clear(); + dragged.Clear(); +} + +void System::MarkParamsFree(bool find) { + // If requested, find all the free (unbound) variables. This might be + // more than the number of degrees of freedom. Don't always do this, + // because the display would get annoying and it's slow. + for(int i = 0; i < param.n; i++) { + Param *p = &(param.elem[i]); + p->free = false; + + if(find) { + if(p->tag == 0) { + p->tag = VAR_DOF_TEST; + WriteJacobian(0); + EvalJacobian(); + int rank = CalculateRank(); + if(rank == mat.m) { + p->free = true; + } + p->tag = 0; + } + } + } +} + +int System::CalculateDof() { + return mat.n - mat.m; +} + diff --git a/src/libslic3r/slvs/util.cpp b/src/libslic3r/slvs/util.cpp new file mode 100644 index 0000000000..e6ed51b32e --- /dev/null +++ b/src/libslic3r/slvs/util.cpp @@ -0,0 +1,1161 @@ +//----------------------------------------------------------------------------- +// Utility functions, mostly various kinds of vector math (working on real +// numbers, not working on quantities in the symbolic algebra system). +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#define _USE_MATH_DEFINES +#include "solvespace.h" + +#include + +std::string SolveSpace::ssprintf(const char *fmt, ...) +{ + va_list va; + + va_start(va, fmt); + int size = vsnprintf(NULL, 0, fmt, va); + ssassert(size >= 0, "vsnprintf could not encode string"); + va_end(va); + + std::string result; + result.resize(size + 1); + + va_start(va, fmt); + vsnprintf(&result[0], size + 1, fmt, va); + va_end(va); + + result.resize(size); + return result; +} +#if FULL_LIB_JJS + +char32_t utf8_iterator::operator*() +{ + const uint8_t *it = (const uint8_t*) this->p; + char32_t result = *it; + + if((result & 0x80) != 0) { + unsigned int mask = 0x40; + + do { + result <<= 6; + unsigned int c = (*++it); + mask <<= 5; + result += c - 0x80; + } while((result & mask) != 0); + + result &= mask - 1; + } + + this->n = (const char*) (it + 1); + return result; +} +int64_t SolveSpace::GetMilliseconds() +{ + auto timestamp = std::chrono::steady_clock::now().time_since_epoch(); + return std::chrono::duration_cast(timestamp).count(); +} +#endif + +void SolveSpace::MakeMatrix(double *mat, + double a11, double a12, double a13, double a14, + double a21, double a22, double a23, double a24, + double a31, double a32, double a33, double a34, + double a41, double a42, double a43, double a44) +{ + mat[ 0] = a11; + mat[ 1] = a21; + mat[ 2] = a31; + mat[ 3] = a41; + mat[ 4] = a12; + mat[ 5] = a22; + mat[ 6] = a32; + mat[ 7] = a42; + mat[ 8] = a13; + mat[ 9] = a23; + mat[10] = a33; + mat[11] = a43; + mat[12] = a14; + mat[13] = a24; + mat[14] = a34; + mat[15] = a44; +} + +void SolveSpace::MultMatrix(double *mata, double *matb, double *matr) { + for(int i = 0; i < 4; i++) { + for(int j = 0; j < 4; j++) { + double s = 0.0; + for(int k = 0; k < 4; k++) { + s += mata[k * 4 + j] * matb[i * 4 + k]; + } + matr[i * 4 + j] = s; + } + } +} + +//----------------------------------------------------------------------------- +// Word-wrap the string for our message box appropriately, and then display +// that string. +//----------------------------------------------------------------------------- +static void DoStringForMessageBox(const char *str, va_list f, bool error) +{ + char inBuf[1024*50]; + vsprintf(inBuf, str, f); + + char outBuf[1024*50]; + int i = 0, j = 0, len = 0, longestLen = 47; + int rows = 0, cols = 0; + + // Count the width of the longest line that starts with spaces; those + // are list items, that should not be split in the middle. + bool listLine = false; + while(inBuf[i]) { + if(inBuf[i] == '\r') { + // ignore these + } else if(inBuf[i] == ' ' && len == 0) { + listLine = true; + } else if(inBuf[i] == '\n') { + if(listLine) longestLen = max(longestLen, len); + len = 0; + } else { + len++; + } + i++; + } + if(listLine) longestLen = max(longestLen, len); + + // Word wrap according to our target line length longestLen. + len = 0; + i = 0; + while(inBuf[i]) { + if(inBuf[i] == '\r') { + // ignore these + } else if(inBuf[i] == '\n') { + outBuf[j++] = '\n'; + if(len == 0) rows++; + len = 0; + } else if(inBuf[i] == ' ' && len > longestLen) { + outBuf[j++] = '\n'; + len = 0; + } else { + outBuf[j++] = inBuf[i]; + // Count rows when we draw the first character; so an empty + // row doesn't end up counting. + if(len == 0) rows++; + len++; + } + cols = max(cols, len); + i++; + } + outBuf[j++] = '\0'; + + // And then display the text with our actual longest line length. + DoMessageBox(outBuf, rows, cols, error); +} +void SolveSpace::Error(const char *str, ...) +{ + va_list f; + va_start(f, str); + DoStringForMessageBox(str, f, /*error=*/true); + va_end(f); +} +void SolveSpace::Message(const char *str, ...) +{ + va_list f; + va_start(f, str); + DoStringForMessageBox(str, f, /*error=*/false); + va_end(f); +} + +void SolveSpace::CnfFreezeBool(bool v, const std::string &name) + { CnfFreezeInt(v ? 1 : 0, name); } + +void SolveSpace::CnfFreezeColor(RgbaColor v, const std::string &name) + { CnfFreezeInt(v.ToPackedInt(), name); } + +bool SolveSpace::CnfThawBool(bool v, const std::string &name) + { return CnfThawInt(v ? 1 : 0, name) != 0; } + +RgbaColor SolveSpace::CnfThawColor(RgbaColor v, const std::string &name) + { return RgbaColor::FromPackedInt(CnfThawInt(v.ToPackedInt(), name)); } + +//----------------------------------------------------------------------------- +// Solve a mostly banded matrix. In a given row, there are LEFT_OF_DIAG +// elements to the left of the diagonal element, and RIGHT_OF_DIAG elements to +// the right (so that the total band width is LEFT_OF_DIAG + RIGHT_OF_DIAG + 1). +// There also may be elements in the last two columns of any row. We solve +// without pivoting. +//----------------------------------------------------------------------------- +void BandedMatrix::Solve() { + int i, ip, j, jp; + double temp; + + // Reduce the matrix to upper triangular form. + for(i = 0; i < n; i++) { + for(ip = i+1; ip < n && ip <= (i + LEFT_OF_DIAG); ip++) { + temp = A[ip][i]/A[i][i]; + + for(jp = i; jp < (n - 2) && jp <= (i + RIGHT_OF_DIAG); jp++) { + A[ip][jp] -= temp*(A[i][jp]); + } + A[ip][n-2] -= temp*(A[i][n-2]); + A[ip][n-1] -= temp*(A[i][n-1]); + + B[ip] -= temp*B[i]; + } + } + + // And back-substitute. + for(i = n - 1; i >= 0; i--) { + temp = B[i]; + + if(i < n-1) temp -= X[n-1]*A[i][n-1]; + if(i < n-2) temp -= X[n-2]*A[i][n-2]; + + for(j = min(n - 3, i + RIGHT_OF_DIAG); j > i; j--) { + temp -= X[j]*A[i][j]; + } + X[i] = temp / A[i][i]; + } +} + +const Quaternion Quaternion::IDENTITY = { 1, 0, 0, 0 }; + +Quaternion Quaternion::From(double w, double vx, double vy, double vz) { + Quaternion q; + q.w = w; + q.vx = vx; + q.vy = vy; + q.vz = vz; + return q; +} + +Quaternion Quaternion::From(hParam w, hParam vx, hParam vy, hParam vz) { + Quaternion q; + q.w = SK.GetParam(w )->val; + q.vx = SK.GetParam(vx)->val; + q.vy = SK.GetParam(vy)->val; + q.vz = SK.GetParam(vz)->val; + return q; +} + +Quaternion Quaternion::From(Vector axis, double dtheta) { + Quaternion q; + double c = cos(dtheta / 2), s = sin(dtheta / 2); + axis = axis.WithMagnitude(s); + q.w = c; + q.vx = axis.x; + q.vy = axis.y; + q.vz = axis.z; + return q; +} + +Quaternion Quaternion::From(Vector u, Vector v) +{ + Vector n = u.Cross(v); + + Quaternion q; + double s, tr = 1 + u.x + v.y + n.z; + if(tr > 1e-4) { + s = 2*sqrt(tr); + q.w = s/4; + q.vx = (v.z - n.y)/s; + q.vy = (n.x - u.z)/s; + q.vz = (u.y - v.x)/s; + } else { + if(u.x > v.y && u.x > n.z) { + s = 2*sqrt(1 + u.x - v.y - n.z); + q.w = (v.z - n.y)/s; + q.vx = s/4; + q.vy = (u.y + v.x)/s; + q.vz = (n.x + u.z)/s; + } else if(v.y > n.z) { + s = 2*sqrt(1 - u.x + v.y - n.z); + q.w = (n.x - u.z)/s; + q.vx = (u.y + v.x)/s; + q.vy = s/4; + q.vz = (v.z + n.y)/s; + } else { + s = 2*sqrt(1 - u.x - v.y + n.z); + q.w = (u.y - v.x)/s; + q.vx = (n.x + u.z)/s; + q.vy = (v.z + n.y)/s; + q.vz = s/4; + } + } + + return q.WithMagnitude(1); +} + +Quaternion Quaternion::Plus(Quaternion b) const { + Quaternion q; + q.w = w + b.w; + q.vx = vx + b.vx; + q.vy = vy + b.vy; + q.vz = vz + b.vz; + return q; +} + +Quaternion Quaternion::Minus(Quaternion b) const { + Quaternion q; + q.w = w - b.w; + q.vx = vx - b.vx; + q.vy = vy - b.vy; + q.vz = vz - b.vz; + return q; +} + +Quaternion Quaternion::ScaledBy(double s) const { + Quaternion q; + q.w = w*s; + q.vx = vx*s; + q.vy = vy*s; + q.vz = vz*s; + return q; +} + +double Quaternion::Magnitude() const { + return sqrt(w*w + vx*vx + vy*vy + vz*vz); +} + +Quaternion Quaternion::WithMagnitude(double s) const { + return ScaledBy(s/Magnitude()); +} + +Vector Quaternion::RotationU() const { + Vector v; + v.x = w*w + vx*vx - vy*vy - vz*vz; + v.y = 2*w *vz + 2*vx*vy; + v.z = 2*vx*vz - 2*w *vy; + return v; +} + +Vector Quaternion::RotationV() const { + Vector v; + v.x = 2*vx*vy - 2*w*vz; + v.y = w*w - vx*vx + vy*vy - vz*vz; + v.z = 2*w*vx + 2*vy*vz; + return v; +} + +Vector Quaternion::RotationN() const { + Vector v; + v.x = 2*w*vy + 2*vx*vz; + v.y = 2*vy*vz - 2*w*vx; + v.z = w*w - vx*vx - vy*vy + vz*vz; + return v; +} + +Vector Quaternion::Rotate(Vector p) const { + // Express the point in the new basis + return (RotationU().ScaledBy(p.x)).Plus( + RotationV().ScaledBy(p.y)).Plus( + RotationN().ScaledBy(p.z)); +} + +Quaternion Quaternion::Inverse() const { + Quaternion r; + r.w = w; + r.vx = -vx; + r.vy = -vy; + r.vz = -vz; + return r.WithMagnitude(1); // not that the normalize should be reqd +} + +Quaternion Quaternion::ToThe(double p) const { + // Avoid division by zero, or arccos of something not in its domain + if(w >= (1 - 1e-6)) { + return From(1, 0, 0, 0); + } else if(w <= (-1 + 1e-6)) { + return From(-1, 0, 0, 0); + } + + Quaternion r; + Vector axis = Vector::From(vx, vy, vz); + double theta = acos(w); // okay, since magnitude is 1, so -1 <= w <= 1 + theta *= p; + r.w = cos(theta); + axis = axis.WithMagnitude(sin(theta)); + r.vx = axis.x; + r.vy = axis.y; + r.vz = axis.z; + return r; +} + +Quaternion Quaternion::Times(Quaternion b) const { + double sa = w, sb = b.w; + Vector va = { vx, vy, vz }; + Vector vb = { b.vx, b.vy, b.vz }; + + Quaternion r; + r.w = sa*sb - va.Dot(vb); + Vector vr = vb.ScaledBy(sa).Plus( + va.ScaledBy(sb).Plus( + va.Cross(vb))); + r.vx = vr.x; + r.vy = vr.y; + r.vz = vr.z; + return r; +} + +Quaternion Quaternion::Mirror() const { + Vector u = RotationU(), + v = RotationV(); + u = u.ScaledBy(-1); + v = v.ScaledBy(-1); + return Quaternion::From(u, v); +} + + +Vector Vector::From(double x, double y, double z) { + Vector v; + v.x = x; v.y = y; v.z = z; + return v; +} + +Vector Vector::From(hParam x, hParam y, hParam z) { + Vector v; + v.x = SK.GetParam(x)->val; + v.y = SK.GetParam(y)->val; + v.z = SK.GetParam(z)->val; + return v; +} + +double Vector::Element(int i) const { + switch(i) { + case 0: return x; + case 1: return y; + case 2: return z; + default: ssassert(false, "Unexpected vector element index"); + } +} + +bool Vector::Equals(Vector v, double tol) const { + // Quick axis-aligned tests before going further + double dx = v.x - x; if(dx < -tol || dx > tol) return false; + double dy = v.y - y; if(dy < -tol || dy > tol) return false; + double dz = v.z - z; if(dz < -tol || dz > tol) return false; + + return (this->Minus(v)).MagSquared() < tol*tol; +} + +bool Vector::EqualsExactly(Vector v) const { + return EXACT(x == v.x && + y == v.y && + z == v.z); +} + +Vector Vector::Plus(Vector b) const { + Vector r; + + r.x = x + b.x; + r.y = y + b.y; + r.z = z + b.z; + + return r; +} + +Vector Vector::Minus(Vector b) const { + Vector r; + + r.x = x - b.x; + r.y = y - b.y; + r.z = z - b.z; + + return r; +} + +Vector Vector::Negated() const { + Vector r; + + r.x = -x; + r.y = -y; + r.z = -z; + + return r; +} + +Vector Vector::Cross(Vector b) const { + Vector r; + + r.x = -(z*b.y) + (y*b.z); + r.y = (z*b.x) - (x*b.z); + r.z = -(y*b.x) + (x*b.y); + + return r; +} + +double Vector::Dot(Vector b) const { + return (x*b.x + y*b.y + z*b.z); +} + +double Vector::DirectionCosineWith(Vector b) const { + Vector a = this->WithMagnitude(1); + b = b.WithMagnitude(1); + return a.Dot(b); +} + +Vector Vector::Normal(int which) const { + Vector n; + + // Arbitrarily choose one vector that's normal to us, pivoting + // appropriately. + double xa = fabs(x), ya = fabs(y), za = fabs(z); + if(this->Equals(Vector::From(0, 0, 1))) { + // Make DXFs exported in the XY plane work nicely... + n = Vector::From(1, 0, 0); + } else if(xa < ya && xa < za) { + n.x = 0; + n.y = z; + n.z = -y; + } else if(ya < za) { + n.x = -z; + n.y = 0; + n.z = x; + } else { + n.x = y; + n.y = -x; + n.z = 0; + } + + if(which == 0) { + // That's the vector we return. + } else if(which == 1) { + n = this->Cross(n); + } else ssassert(false, "Unexpected vector normal index"); + + n = n.WithMagnitude(1); + + return n; +} + +Vector Vector::RotatedAbout(Vector orig, Vector axis, double theta) const { + Vector r = this->Minus(orig); + r = r.RotatedAbout(axis, theta); + return r.Plus(orig); +} + +Vector Vector::RotatedAbout(Vector axis, double theta) const { + double c = cos(theta); + double s = sin(theta); + + axis = axis.WithMagnitude(1); + + Vector r; + + r.x = (x)*(c + (1 - c)*(axis.x)*(axis.x)) + + (y)*((1 - c)*(axis.x)*(axis.y) - s*(axis.z)) + + (z)*((1 - c)*(axis.x)*(axis.z) + s*(axis.y)); + + r.y = (x)*((1 - c)*(axis.y)*(axis.x) + s*(axis.z)) + + (y)*(c + (1 - c)*(axis.y)*(axis.y)) + + (z)*((1 - c)*(axis.y)*(axis.z) - s*(axis.x)); + + r.z = (x)*((1 - c)*(axis.z)*(axis.x) - s*(axis.y)) + + (y)*((1 - c)*(axis.z)*(axis.y) + s*(axis.x)) + + (z)*(c + (1 - c)*(axis.z)*(axis.z)); + + return r; +} + +Vector Vector::DotInToCsys(Vector u, Vector v, Vector n) const { + Vector r = { + this->Dot(u), + this->Dot(v), + this->Dot(n) + }; + return r; +} + +Vector Vector::ScaleOutOfCsys(Vector u, Vector v, Vector n) const { + Vector r = u.ScaledBy(x).Plus( + v.ScaledBy(y).Plus( + n.ScaledBy(z))); + return r; +} + +Vector Vector::InPerspective(Vector u, Vector v, Vector n, + Vector origin, double cameraTan) const +{ + Vector r = this->Minus(origin); + r = r.DotInToCsys(u, v, n); + // yes, minus; we are assuming a csys where u cross v equals n, backwards + // from the display stuff + double w = (1 - r.z*cameraTan); + r = r.ScaledBy(1/w); + + return r; +} + +double Vector::DistanceToLine(Vector p0, Vector dp) const { + double m = dp.Magnitude(); + return ((this->Minus(p0)).Cross(dp)).Magnitude() / m; +} + +double Vector::DistanceToPlane(Vector normal, Vector origin) const { + return this->Dot(normal) - origin.Dot(normal); +} + +bool Vector::OnLineSegment(Vector a, Vector b, double tol) const { + if(this->Equals(a, tol) || this->Equals(b, tol)) return true; + + Vector d = b.Minus(a); + + double m = d.MagSquared(); + double distsq = ((this->Minus(a)).Cross(d)).MagSquared() / m; + + if(distsq >= tol*tol) return false; + + double t = (this->Minus(a)).DivPivoting(d); + // On-endpoint already tested + if(t < 0 || t > 1) return false; + return true; +} + +Vector Vector::ClosestPointOnLine(Vector p0, Vector dp) const { + dp = dp.WithMagnitude(1); + // this, p0, and (p0+dp) define a plane; the min distance is in + // that plane, so calculate its normal + Vector pn = (this->Minus(p0)).Cross(dp); + // The minimum distance line is in that plane, perpendicular + // to the line + Vector n = pn.Cross(dp); + + // Calculate the actual distance + double d = (dp.Cross(p0.Minus(*this))).Magnitude(); + return this->Plus(n.WithMagnitude(d)); +} + +double Vector::MagSquared() const { + return x*x + y*y + z*z; +} + +double Vector::Magnitude() const { + return sqrt(x*x + y*y + z*z); +} + +Vector Vector::ScaledBy(double v) const { + Vector r; + + r.x = x * v; + r.y = y * v; + r.z = z * v; + + return r; +} + +Vector Vector::WithMagnitude(double v) const { + double m = Magnitude(); + if(EXACT(m == 0)) { + // We can do a zero vector with zero magnitude, but not any other cases. + if(fabs(v) > 1e-100) { + dbp("Vector::WithMagnitude(%g) of zero vector!", v); + } + return From(0, 0, 0); + } else { + return ScaledBy(v/m); + } +} + +Vector Vector::ProjectVectorInto(hEntity wrkpl) const { + EntityBase *w = SK.GetEntity(wrkpl); + Vector u = w->Normal()->NormalU(); + Vector v = w->Normal()->NormalV(); + + double up = this->Dot(u); + double vp = this->Dot(v); + + return (u.ScaledBy(up)).Plus(v.ScaledBy(vp)); +} + +Vector Vector::ProjectInto(hEntity wrkpl) const { + EntityBase *w = SK.GetEntity(wrkpl); + Vector p0 = w->WorkplaneGetOffset(); + + Vector f = this->Minus(p0); + + return p0.Plus(f.ProjectVectorInto(wrkpl)); +} + +Point2d Vector::Project2d(Vector u, Vector v) const { + Point2d p; + p.x = this->Dot(u); + p.y = this->Dot(v); + return p; +} + +Point2d Vector::ProjectXy() const { + Point2d p; + p.x = x; + p.y = y; + return p; +} + +Vector4 Vector::Project4d() const { + return Vector4::From(1, x, y, z); +} + +double Vector::DivPivoting(Vector delta) const { + double mx = fabs(delta.x), my = fabs(delta.y), mz = fabs(delta.z); + + if(mx > my && mx > mz) { + return x/delta.x; + } else if(my > mz) { + return y/delta.y; + } else { + return z/delta.z; + } +} + +Vector Vector::ClosestOrtho() const { + double mx = fabs(x), my = fabs(y), mz = fabs(z); + + if(mx > my && mx > mz) { + return From((x > 0) ? 1 : -1, 0, 0); + } else if(my > mz) { + return From(0, (y > 0) ? 1 : -1, 0); + } else { + return From(0, 0, (z > 0) ? 1 : -1); + } +} + +Vector Vector::ClampWithin(double minv, double maxv) const { + Vector ret = *this; + + if(ret.x < minv) ret.x = minv; + if(ret.y < minv) ret.y = minv; + if(ret.z < minv) ret.z = minv; + + if(ret.x > maxv) ret.x = maxv; + if(ret.y > maxv) ret.y = maxv; + if(ret.z > maxv) ret.z = maxv; + + return ret; +} + +void Vector::MakeMaxMin(Vector *maxv, Vector *minv) const { + maxv->x = max(maxv->x, x); + maxv->y = max(maxv->y, y); + maxv->z = max(maxv->z, z); + + minv->x = min(minv->x, x); + minv->y = min(minv->y, y); + minv->z = min(minv->z, z); +} + +bool Vector::OutsideAndNotOn(Vector maxv, Vector minv) const { + return (x > maxv.x + LENGTH_EPS) || (x < minv.x - LENGTH_EPS) || + (y > maxv.y + LENGTH_EPS) || (y < minv.y - LENGTH_EPS) || + (z > maxv.z + LENGTH_EPS) || (z < minv.z - LENGTH_EPS); +} + +bool Vector::BoundingBoxesDisjoint(Vector amax, Vector amin, + Vector bmax, Vector bmin) +{ + int i; + for(i = 0; i < 3; i++) { + if(amax.Element(i) < bmin.Element(i) - LENGTH_EPS) return true; + if(amin.Element(i) > bmax.Element(i) + LENGTH_EPS) return true; + } + return false; +} + +#if FULL_LIB_JJS +bool Vector::BoundingBoxIntersectsLine(Vector amax, Vector amin, + Vector p0, Vector p1, bool asSegment) +{ + Vector dp = p1.Minus(p0); + double lp = dp.Magnitude(); + dp = dp.ScaledBy(1.0/lp); + + int i, a; + for(i = 0; i < 3; i++) { + int j = WRAP(i+1, 3), k = WRAP(i+2, 3); + if(lp*fabs(dp.Element(i)) < LENGTH_EPS) continue; // parallel to plane + + for(a = 0; a < 2; a++) { + double d = (a == 0) ? amax.Element(i) : amin.Element(i); + // n dot (p0 + t*dp) = d + // (n dot p0) + t * (n dot dp) = d + double t = (d - p0.Element(i)) / dp.Element(i); + Vector p = p0.Plus(dp.ScaledBy(t)); + + if(asSegment && (t < -LENGTH_EPS || t > (lp+LENGTH_EPS))) continue; + + if(p.Element(j) > amax.Element(j) + LENGTH_EPS) continue; + if(p.Element(k) > amax.Element(k) + LENGTH_EPS) continue; + + if(p.Element(j) < amin.Element(j) - LENGTH_EPS) continue; + if(p.Element(k) < amin.Element(k) - LENGTH_EPS) continue; + + return true; + } + } + + return false; +} +#endif + +Vector Vector::AtIntersectionOfPlanes(Vector n1, double d1, + Vector n2, double d2) +{ + double det = (n1.Dot(n1))*(n2.Dot(n2)) - + (n1.Dot(n2))*(n1.Dot(n2)); + double c1 = (d1*n2.Dot(n2) - d2*n1.Dot(n2))/det; + double c2 = (d2*n1.Dot(n1) - d1*n1.Dot(n2))/det; + + return (n1.ScaledBy(c1)).Plus(n2.ScaledBy(c2)); +} + +void Vector::ClosestPointBetweenLines(Vector a0, Vector da, + Vector b0, Vector db, + double *ta, double *tb) +{ + // Make a semi-orthogonal coordinate system from those directions; + // note that dna and dnb need not be perpendicular. + Vector dn = da.Cross(db); // normal to both + Vector dna = dn.Cross(da); // normal to da + Vector dnb = dn.Cross(db); // normal to db + + // At the intersection of the lines + // a0 + pa*da = b0 + pb*db (where pa, pb are scalar params) + // So dot this equation against dna and dnb to get two equations + // to solve for da and db + *tb = ((a0.Minus(b0)).Dot(dna))/(db.Dot(dna)); + *ta = -((a0.Minus(b0)).Dot(dnb))/(da.Dot(dnb)); +} + +Vector Vector::AtIntersectionOfLines(Vector a0, Vector a1, + Vector b0, Vector b1, + bool *skew, + double *parama, double *paramb) +{ + Vector da = a1.Minus(a0), db = b1.Minus(b0); + + double pa, pb; + Vector::ClosestPointBetweenLines(a0, da, b0, db, &pa, &pb); + + if(parama) *parama = pa; + if(paramb) *paramb = pb; + + // And from either of those, we get the intersection point. + Vector pi = a0.Plus(da.ScaledBy(pa)); + + if(skew) { + // Check if the intersection points on each line are actually + // coincident... + if(pi.Equals(b0.Plus(db.ScaledBy(pb)))) { + *skew = false; + } else { + *skew = true; + } + } + return pi; +} + +Vector Vector::AtIntersectionOfPlaneAndLine(Vector n, double d, + Vector p0, Vector p1, + bool *parallel) +{ + Vector dp = p1.Minus(p0); + + if(fabs(n.Dot(dp)) < LENGTH_EPS) { + if(parallel) *parallel = true; + return Vector::From(0, 0, 0); + } + + if(parallel) *parallel = false; + + // n dot (p0 + t*dp) = d + // (n dot p0) + t * (n dot dp) = d + double t = (d - n.Dot(p0)) / (n.Dot(dp)); + + return p0.Plus(dp.ScaledBy(t)); +} + +static double det2(double a1, double b1, + double a2, double b2) +{ + return (a1*b2) - (b1*a2); +} +static double det3(double a1, double b1, double c1, + double a2, double b2, double c2, + double a3, double b3, double c3) +{ + return a1*det2(b2, c2, b3, c3) - + b1*det2(a2, c2, a3, c3) + + c1*det2(a2, b2, a3, b3); +} +Vector Vector::AtIntersectionOfPlanes(Vector na, double da, + Vector nb, double db, + Vector nc, double dc, + bool *parallel) +{ + double det = det3(na.x, na.y, na.z, + nb.x, nb.y, nb.z, + nc.x, nc.y, nc.z); + if(fabs(det) < 1e-10) { // arbitrary tolerance, not so good + *parallel = true; + return Vector::From(0, 0, 0); + } + *parallel = false; + + double detx = det3(da, na.y, na.z, + db, nb.y, nb.z, + dc, nc.y, nc.z); + + double dety = det3(na.x, da, na.z, + nb.x, db, nb.z, + nc.x, dc, nc.z); + + double detz = det3(na.x, na.y, da, + nb.x, nb.y, db, + nc.x, nc.y, dc ); + + return Vector::From(detx/det, dety/det, detz/det); +} + +size_t VectorHash::operator()(const Vector &v) const { + const size_t size = (size_t)pow(std::numeric_limits::max(), 1.0 / 3.0) - 1; + const double eps = 4.0 * LENGTH_EPS; + + double x = fabs(v.x) / eps; + double y = fabs(v.y) / eps; + double z = fabs(v.y) / eps; + + size_t xs = size_t(fmod(x, (double)size)); + size_t ys = size_t(fmod(y, (double)size)); + size_t zs = size_t(fmod(z, (double)size)); + + return (zs * size + ys) * size + xs; +} + +bool VectorPred::operator()(Vector a, Vector b) const { + return a.Equals(b, LENGTH_EPS); +} + +Vector4 Vector4::From(double w, double x, double y, double z) { + Vector4 ret; + ret.w = w; + ret.x = x; + ret.y = y; + ret.z = z; + return ret; +} + +Vector4 Vector4::From(double w, Vector v) { + return Vector4::From(w, w*v.x, w*v.y, w*v.z); +} + +Vector4 Vector4::Blend(Vector4 a, Vector4 b, double t) { + return (a.ScaledBy(1 - t)).Plus(b.ScaledBy(t)); +} + +Vector4 Vector4::Plus(Vector4 b) const { + return Vector4::From(w + b.w, x + b.x, y + b.y, z + b.z); +} + +Vector4 Vector4::Minus(Vector4 b) const { + return Vector4::From(w - b.w, x - b.x, y - b.y, z - b.z); +} + +Vector4 Vector4::ScaledBy(double s) const { + return Vector4::From(w*s, x*s, y*s, z*s); +} + +Vector Vector4::PerspectiveProject() const { + return Vector::From(x / w, y / w, z / w); +} + +Point2d Point2d::From(double x, double y) { + return { x, y }; +} + +Point2d Point2d::FromPolar(double r, double a) { + return { r * cos(a), r * sin(a) }; +} + +double Point2d::Angle() const { + double a = atan2(y, x); + return M_PI + remainder(a - M_PI, 2 * M_PI); +} + +double Point2d::AngleTo(const Point2d &p) const { + return p.Minus(*this).Angle(); +} + +Point2d Point2d::Plus(const Point2d &b) const { + return { x + b.x, y + b.y }; +} + +Point2d Point2d::Minus(const Point2d &b) const { + return { x - b.x, y - b.y }; +} + +Point2d Point2d::ScaledBy(double s) const { + return { x * s, y * s }; +} + +double Point2d::DivPivoting(Point2d delta) const { + if(fabs(delta.x) > fabs(delta.y)) { + return x/delta.x; + } else { + return y/delta.y; + } +} + +double Point2d::MagSquared() const { + return x*x + y*y; +} + +double Point2d::Magnitude() const { + return sqrt(x*x + y*y); +} + +Point2d Point2d::WithMagnitude(double v) const { + double m = Magnitude(); + if(m < 1e-20) { + dbp("!!! WithMagnitude() of zero vector"); + return { v, 0 }; + } + return { x * v / m, y * v / m }; +} + +double Point2d::DistanceTo(const Point2d &p) const { + double dx = x - p.x; + double dy = y - p.y; + return sqrt(dx*dx + dy*dy); +} + +double Point2d::Dot(Point2d p) const { + return x*p.x + y*p.y; +} + +double Point2d::DistanceToLine(const Point2d &p0, const Point2d &dp, bool asSegment) const { + double m = dp.x*dp.x + dp.y*dp.y; + if(m < LENGTH_EPS*LENGTH_EPS) return VERY_POSITIVE; + + // Let our line be p = p0 + t*dp, for a scalar t from 0 to 1 + double t = (dp.x*(x - p0.x) + dp.y*(y - p0.y))/m; + + if(asSegment) { + if(t < 0.0) return DistanceTo(p0); + if(t > 1.0) return DistanceTo(p0.Plus(dp)); + } + Point2d closest = p0.Plus(dp.ScaledBy(t)); + return DistanceTo(closest); +} + +double Point2d::DistanceToLineSigned(const Point2d &p0, const Point2d &dp, bool asSegment) const { + double m = dp.x*dp.x + dp.y*dp.y; + if(m < LENGTH_EPS*LENGTH_EPS) return VERY_POSITIVE; + + Point2d n = dp.Normal().WithMagnitude(1.0); + double dist = n.Dot(*this) - n.Dot(p0); + if(asSegment) { + // Let our line be p = p0 + t*dp, for a scalar t from 0 to 1 + double t = (dp.x*(x - p0.x) + dp.y*(y - p0.y))/m; + double sign = (dist > 0.0) ? 1.0 : -1.0; + if(t < 0.0) return DistanceTo(p0) * sign; + if(t > 1.0) return DistanceTo(p0.Plus(dp)) * sign; + } + + return dist; +} + +Point2d Point2d::Normal() const { + return { y, -x }; +} + +bool Point2d::Equals(Point2d v, double tol) const { + double dx = v.x - x; if(dx < -tol || dx > tol) return false; + double dy = v.y - y; if(dy < -tol || dy > tol) return false; + + return (this->Minus(v)).MagSquared() < tol*tol; +} + +BBox BBox::From(const Vector &p0, const Vector &p1) { + BBox bbox; + bbox.minp.x = min(p0.x, p1.x); + bbox.minp.y = min(p0.y, p1.y); + bbox.minp.z = min(p0.z, p1.z); + + bbox.maxp.x = max(p0.x, p1.x); + bbox.maxp.y = max(p0.y, p1.y); + bbox.maxp.z = max(p0.z, p1.z); + return bbox; +} + +Vector BBox::GetOrigin() const { return minp.Plus(maxp.Minus(minp).ScaledBy(0.5)); } +Vector BBox::GetExtents() const { return maxp.Minus(minp).ScaledBy(0.5); } + +void BBox::Include(const Vector &v, double r) { + minp.x = min(minp.x, v.x - r); + minp.y = min(minp.y, v.y - r); + minp.z = min(minp.z, v.z - r); + + maxp.x = max(maxp.x, v.x + r); + maxp.y = max(maxp.y, v.y + r); + maxp.z = max(maxp.z, v.z + r); +} + +bool BBox::Overlaps(const BBox &b1) const { + Vector t = b1.GetOrigin().Minus(GetOrigin()); + Vector e = b1.GetExtents().Plus(GetExtents()); + + return fabs(t.x) < e.x && fabs(t.y) < e.y && fabs(t.z) < e.z; +} + +bool BBox::Contains(const Point2d &p, double r) const { + return p.x >= (minp.x - r) && + p.y >= (minp.y - r) && + p.x <= (maxp.x + r) && + p.y <= (maxp.y + r); +} + +const std::vector& SolveSpace::StipplePatternDashes(StipplePattern pattern) { + static bool initialized; + static std::vector dashes[(size_t)StipplePattern::LAST + 1]; + if(!initialized) { + // Inkscape ignores all elements that are exactly zero instead of drawing + // them as dots, so set those to 1e-6. + dashes[(size_t)StipplePattern::CONTINUOUS] = + {}; + dashes[(size_t)StipplePattern::SHORT_DASH] = + { 1.0, 2.0 }; + dashes[(size_t)StipplePattern::DASH] = + { 1.0, 1.0 }; + dashes[(size_t)StipplePattern::DASH_DOT] = + { 1.0, 0.5, 1e-6, 0.5 }; + dashes[(size_t)StipplePattern::DASH_DOT_DOT] = + { 1.0, 0.5, 1e-6, 0.5, 1e-6, 0.5 }; + dashes[(size_t)StipplePattern::DOT] = + { 1e-6, 0.5 }; + dashes[(size_t)StipplePattern::LONG_DASH] = + { 2.0, 0.5 }; + dashes[(size_t)StipplePattern::FREEHAND] = + { 1.0, 2.0 }; + dashes[(size_t)StipplePattern::ZIGZAG] = + { 1.0, 2.0 }; + } + + return dashes[(size_t)pattern]; +} + +double SolveSpace::StipplePatternLength(StipplePattern pattern) { + static bool initialized; + static double lengths[(size_t)StipplePattern::LAST + 1]; + if(!initialized) { + for(size_t i = 0; i < (size_t)StipplePattern::LAST; i++) { + const std::vector &dashes = StipplePatternDashes((StipplePattern)i); + double length = 0.0; + for(double dash : dashes) { + length += dash; + } + lengths[i] = length; + } + } + + return lengths[(size_t)pattern]; +} diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 969dcb693a..aaf37d39b3 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -107,6 +107,13 @@ set(SLIC3R_GUI_SOURCES GUI/Downloader.hpp GUI/DownloadProgressDialog.cpp GUI/DownloadProgressDialog.hpp + GUI/DesignPanel.cpp + GUI/DesignPanel.hpp + GUI/DesignCanvas.cpp + GUI/DesignCanvas.hpp + GUI/DesignSketchTool.cpp + GUI/SketchInlineEditor.cpp + GUI/SketchInlineEditor.hpp GUI/DragCanvas.cpp GUI/DragCanvas.hpp GUI/EditGCodeDialog.cpp @@ -156,6 +163,8 @@ set(SLIC3R_GUI_SOURCES GUI/Gizmos/GLGizmoMove.hpp GUI/Gizmos/GLGizmoPainterBase.cpp GUI/Gizmos/GLGizmoPainterBase.hpp + GUI/Gizmos/GLGizmoPrimitive.cpp + GUI/Gizmos/GLGizmoPrimitive.hpp GUI/Gizmos/GLGizmoRotate.cpp GUI/Gizmos/GLGizmoRotate.hpp GUI/Gizmos/GLGizmoScale.cpp @@ -166,6 +175,8 @@ set(SLIC3R_GUI_SOURCES GUI/Gizmos/GLGizmoSeam.hpp GUI/Gizmos/GLGizmoSimplify.cpp GUI/Gizmos/GLGizmoSimplify.hpp + GUI/Gizmos/GLGizmoSketch.cpp + GUI/Gizmos/GLGizmoSketch.hpp #GUI/Gizmos/GLGizmoSlaSupports.cpp #GUI/Gizmos/GLGizmoSlaSupports.hpp GUI/Gizmos/GLGizmosManager.cpp @@ -746,7 +757,7 @@ if (WIN32) else () add_library(libslic3r_gui STATIC ${SLIC3R_GUI_SOURCES}) endif () -target_include_directories(libslic3r_gui PRIVATE Utils ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(libslic3r_gui PRIVATE Utils ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/GUI) if (WIN32) target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../deps/WebView2/include) diff --git a/src/slic3r/GUI/DesignCanvas.cpp b/src/slic3r/GUI/DesignCanvas.cpp new file mode 100644 index 0000000000..c268865120 --- /dev/null +++ b/src/slic3r/GUI/DesignCanvas.cpp @@ -0,0 +1,885 @@ +#include "DesignCanvas.hpp" + +#include "SketchInlineEditor.hpp" +#include "GLCanvas3D.hpp" +#include "OpenGLManager.hpp" +#include "3DBed.hpp" +#include "GUI_App.hpp" +#include "Plater.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/TriangleMesh.hpp" +#include "3DScene.hpp" +#include "libslic3r/Config.hpp" + +#include +#include +#include +#include +#include + +namespace Slic3r { +namespace GUI { + +DesignCanvas::DesignCanvas(wxWindow* parent) + : wxPanel() +{ + if (!Create(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0)) + return; + + m_canvas_widget = OpenGLManager::create_wxglcanvas(*this); + if (m_canvas_widget == nullptr) + return; + + m_canvas = new GLCanvas3D(m_canvas_widget, m_bed); + m_canvas->set_context(wxGetApp().init_glcontext(*m_canvas_widget)); + m_canvas->allow_multisample(OpenGLManager::can_multisample()); + m_canvas->set_config(wxGetApp().plater()->config()); + m_canvas->set_model(&m_model); + // Reuse the editor's shared slicing process: GLCanvas3D::render() (via + // _max_bounding_box) dereferences the process when canvas type == View3D. + // Passing nullptr segfaults; this mirrors View3D/Preview/AssembleView. + m_canvas->set_process(wxGetApp().plater()->get_background_process()); + m_canvas->set_type(GLCanvas3D::ECanvasType::CanvasView3D); + + m_canvas->enable_picking(false); // viewport face/edge picking is custom (TODO) + m_canvas->enable_moving(false); + m_canvas->enable_gizmos(false); + m_canvas->enable_selection(false); // stock volume selection unused; solid highlight is tree-driven + m_canvas->enable_main_toolbar(false); + m_canvas->enable_select_plate_toolbar(false); + m_canvas->enable_assemble_view_toolbar(false); + m_canvas->enable_separator_toolbar(false); + m_canvas->enable_collapse_toolbar(false); + m_canvas->enable_plate_chrome(false); + m_canvas->enable_labels(false); + + m_canvas->set_design_sketch_tool(&m_sketch_tool); + m_sketch_tool.on_commit = [this](const SketchProfile& prof, const SketchPlane& pl) { + if (m_on_sketch_commit) m_on_sketch_commit(prof, pl); + if (m_canvas) m_canvas->set_as_dirty(); + if (m_canvas_widget) m_canvas_widget->Refresh(); + }; + m_sketch_tool.on_commit_entities = [this](const std::vector& ents, + const std::vector& cons, + const SketchPlane& pl) { + if (m_on_sketch_entities_commit) m_on_sketch_entities_commit(ents, cons, pl); + if (m_canvas) m_canvas->set_as_dirty(); + if (m_canvas_widget) m_canvas_widget->Refresh(); + }; + + // Onshape-style in-canvas value editor, floating over the GL canvas. The tool hands + // us a screen pixel (device px) + a commit/cancel pair; we convert to logical client + // px and wrap the callbacks so each one re-solves and re-renders the viewport. + m_inline_editor = std::make_unique(m_canvas_widget); + m_sketch_tool.on_inline_edit = [this](wxPoint screen_px, double current, + std::function commit, + std::function cancel) { + if (!m_inline_editor) { if (cancel) cancel(); return; } + // The tool hands us canvas device px; convert to logical client px, then to + // absolute screen coords for the floating editor frame. + const double s = m_canvas_widget ? m_canvas_widget->GetContentScaleFactor() : 1.0; + const wxPoint client_pt(int(screen_px.x / s), int(screen_px.y / s)); + const wxPoint scr = m_canvas_widget ? m_canvas_widget->ClientToScreen(client_pt) : client_pt; + // Freeze the sketch tool while the field is open so a stray click/move on the GL + // canvas can't draw under the floating editor; released on commit or cancel. + m_sketch_tool.set_inline_busy(true); + m_inline_editor->open(scr, current, + [this, commit](double v) { + m_sketch_tool.set_inline_busy(false); + if (commit) commit(v); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } + }, + [this, cancel]() { + m_sketch_tool.set_inline_busy(false); + if (cancel) cancel(); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } + }); + }; + // Let the tool force-close the field (keep-as-drawn) — polyline right-click/double-click + // ends the chain even while a per-segment value field is open. + m_sketch_tool.on_inline_dismiss = [this]() { + if (m_inline_editor) m_inline_editor->cancel(); + }; + + // Bottom-right viewport HUD: a borderless, non-focusable float label showing the active + // tool's current values. Top-level (a child widget is hidden by the GL surface, same as + // the inline editor). Fed every frame by the tool's on_readout; empty text hides it. + { + wxWindow* top = wxGetTopLevelParent(m_canvas_widget); + m_hud = new wxFrame(top, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, + wxFRAME_NO_TASKBAR | wxBORDER_NONE | wxFRAME_FLOAT_ON_PARENT | + wxSTAY_ON_TOP | wxTRANSPARENT_WINDOW); + m_hud->SetBackgroundColour(wxColour(28, 30, 34)); + m_hud_label = new wxStaticText(m_hud, wxID_ANY, wxEmptyString); + m_hud_label->SetForegroundColour(wxColour(0x46, 0xE0, 0xC8)); // teal, reads on dark bed + wxFont f = m_hud_label->GetFont(); f.MakeBold(); m_hud_label->SetFont(f); + auto* hs = new wxBoxSizer(wxHORIZONTAL); + hs->Add(m_hud_label, 0, wxALL, 6); + m_hud->SetSizerAndFit(hs); + m_hud->Hide(); + } + m_sketch_tool.on_readout = [this](const std::string& s) { set_readout(s); }; + + refresh_bed(); + + m_canvas->bind_event_handlers(); + + // The Design GL canvas only receives key events (Esc to exit/enter Select, Ctrl+Z undo) + // while it holds keyboard focus. Clicking a side-panel button steals focus, after which + // Esc/Ctrl+Z silently do nothing until the viewport is clicked again. Restore focus + // whenever the pointer enters the viewport (focus-follows-mouse, standard CAD behaviour). + m_canvas_widget->Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& e) { + // …but NOT while an inline value field is open: the field floats over the canvas, so + // the smallest pointer jiggle re-enters the viewport and would yank focus off the + // field (the "no cursor focus on the number, click to focus" bug). + if (m_canvas_widget && !m_sketch_tool.inline_busy()) m_canvas_widget->SetFocus(); + e.Skip(); + }); + + auto* sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(m_canvas_widget, 1, wxEXPAND); + SetSizer(sizer); + SetMinSize(wxSize(300, 300)); +} + +DesignCanvas::~DesignCanvas() +{ + if (m_hud) m_hud->Destroy(); + delete m_canvas; + delete m_canvas_widget; +} + +// Distinct per-body colours (Onshape-style). Body 0 keeps the familiar gold; the rest +// cycle through a small saturated palette so coexisting solids read as separate parts. +static ColorRGBA body_palette(int body_idx) +{ + static const ColorRGBA kPalette[] = { + ColorRGBA(0.86f, 0.66f, 0.20f, 1.0f), // gold + ColorRGBA(0.30f, 0.62f, 0.90f, 1.0f), // blue + ColorRGBA(0.45f, 0.78f, 0.42f, 1.0f), // green + ColorRGBA(0.86f, 0.45f, 0.40f, 1.0f), // coral + ColorRGBA(0.70f, 0.52f, 0.86f, 1.0f), // violet + ColorRGBA(0.90f, 0.70f, 0.35f, 1.0f), // amber + }; + const int n = int(sizeof(kPalette) / sizeof(kPalette[0])); + return kPalette[((body_idx % n) + n) % n]; +} + +void DesignCanvas::reload(bool keep_view) +{ + m_canvas->reset_volumes(); + + for (int i = 0; i < (int)m_model.objects.size(); ++i) + m_canvas->load_object(m_model, i); + + const ColorRGBA sel_gold(0.40f, 0.82f, 1.0f, 1.0f); // cyan tint = solid selected + const ColorRGBA ghost(0.26f, 0.66f, 1.0f, 0.45f); + + const auto& volumes = m_canvas->get_volumes().volumes; + for (auto* v : volumes) { + int obj_idx = v->object_idx(); + if (obj_idx == 0) { + // Object 0 holds one volume per body — colour each by its body index so + // multiple coexisting solids are visually distinct (Onshape per-part colour). + const int b = v->volume_idx(); + bool hidden = (b >= 0 && b < int(m_body_visible.size())) && !m_body_visible[b]; + // Preview-only mode (fillet/chamfer/draft, once a valid target is picked): hide + // every base body so only the result ghost is on screen until Confirm. + if (m_body_hidden) hidden = true; + v->is_active = !hidden; // per-body visibility toggle + if (!hidden) { + // Selection tint wins; otherwise the per-body override (Color tool) or the + // auto palette via body_color(). + ColorRGBA c = m_body_selected ? sel_gold : body_color(b); + if (m_body_translucent) c.a(0.30f); + v->set_color(c); + } + } else if (obj_idx == 1) { + // The ghost is normally a faint blue overlay on the visible body. In preview-only + // mode it IS the result (base bodies hidden), so render it opaque so it reads as a + // finished solid rather than a see-through hint. + v->set_color(m_body_hidden ? ColorRGBA(0.40f, 0.82f, 1.0f, 1.0f) : ghost); + } + } + + if (!keep_view) { + if (m_first_frame && !m_model.objects.empty()) { + m_canvas->select_view("iso"); + m_canvas->zoom_to_volumes(); + m_first_frame = false; + } + } + + m_canvas->set_as_dirty(); + if (m_canvas_widget) + m_canvas_widget->Refresh(); +} + +void DesignCanvas::set_mesh(const TriangleMesh& mesh) +{ + if (m_model.objects.empty()) { + auto* obj = m_model.add_object(); + obj->add_volume(mesh); + obj->add_instance(); + } else { + ModelObject* obj = m_model.objects.front(); + obj->clear_volumes(); + obj->add_volume(mesh); + if (obj->instances.empty()) + obj->add_instance(); + } + + reload(!m_first_frame); +} + +void DesignCanvas::set_bodies(const std::vector& body_meshes, + const std::vector& visible) +{ + // Object 0 carries one GLVolume per body so reload() can colour each distinctly. + // Falls back to a single-volume object when there's only one body (identical look + // to the old set_mesh path). Picking still uses the combined mesh via set_solid_pick. + m_body_visible = visible; // empty => all visible; reload() reads this per volume + if (body_meshes.empty()) { clear_mesh(); return; } + + ModelObject* obj = m_model.objects.empty() ? m_model.add_object() + : m_model.objects.front(); + obj->clear_volumes(); + for (const TriangleMesh& m : body_meshes) + obj->add_volume(m); + if (obj->instances.empty()) + obj->add_instance(); + + reload(!m_first_frame); +} + +void DesignCanvas::clear_mesh() +{ + if (!m_model.objects.empty()) { + m_model.delete_object((size_t)0); + reload(true); + } +} + +void DesignCanvas::set_preview_mesh(const TriangleMesh& mesh) +{ + // Remove existing ghost (object 1) if present + if (m_model.objects.size() > 1) + m_model.delete_object((size_t)1); + + auto* obj = m_model.add_object(); + obj->add_volume(mesh); + obj->add_instance(); + + reload(true); +} + +void DesignCanvas::clear_preview() +{ + if (m_model.objects.size() > 1) { + m_model.delete_object((size_t)1); + reload(true); + } +} + +void DesignCanvas::fit_view() +{ + if (m_canvas && !m_model.objects.empty()) { + m_canvas->zoom_to_volumes(); + m_canvas->set_as_dirty(); + if (m_canvas_widget) + m_canvas_widget->Refresh(); + } +} + +void DesignCanvas::set_view(const std::string& view_name) +{ + if (m_canvas) { + m_canvas->select_view(view_name); + m_canvas->zoom_to_volumes(); + m_canvas->set_as_dirty(); + if (m_canvas_widget) + m_canvas_widget->Refresh(); + } +} + +void DesignCanvas::begin_sketch(const SketchPlane& plane, DesignSketchTool::Mode mode) +{ + m_sketch_tool.begin(plane, mode); + if (m_canvas) m_canvas->set_as_dirty(); + if (m_canvas_widget) m_canvas_widget->Refresh(); +} + +void DesignCanvas::edit_sketch(const std::vector& entities, + const std::vector& constraints, + const SketchPlane& plane) +{ + m_sketch_tool.begin_edit(entities, constraints, plane); + if (m_canvas) m_canvas->set_as_dirty(); + if (m_canvas_widget) m_canvas_widget->Refresh(); +} + +void DesignCanvas::set_sketch_tool(DesignSketchTool::Mode mode) +{ + m_sketch_tool.set_tool(mode); + if (m_canvas) m_canvas->set_as_dirty(); + if (m_canvas_widget) m_canvas_widget->Refresh(); +} + +void DesignCanvas::set_sketch_construction(bool c) +{ + m_sketch_tool.set_construction(c); +} + +void DesignCanvas::set_sketch_polygon_sides(int n) +{ + m_sketch_tool.set_polygon_sides(n); +} + +void DesignCanvas::set_sketch_polygon_circumscribed(bool c) +{ + m_sketch_tool.set_polygon_circumscribed(c); +} + +void DesignCanvas::finish_sketch() +{ + m_sketch_tool.finish(); + if (m_canvas) m_canvas->set_as_dirty(); + if (m_canvas_widget) m_canvas_widget->Refresh(); +} + +// Sync the Design bed to the CURRENT printer bed. Done on every tab activation, not just at +// construction: the panel is built early (before the active printer profile is fully applied), +// so a one-shot read picked up the 200x200 default while the real bed (e.g. 270x270) only +// loaded later — leaving the PartPlate grid spilling past the smaller bed quad. +void DesignCanvas::refresh_bed() +{ + const DynamicPrintConfig* config = wxGetApp().plater()->config(); + if (!config) return; + const auto* bed_shape_opt = config->opt("printable_area"); + if (!bed_shape_opt) return; + double printable_height = 100.0; + const auto* ph_opt = config->opt("printable_height"); + if (ph_opt) printable_height = ph_opt->value; + m_bed.set_shape(bed_shape_opt->values, printable_height, "", false); +} + +bool DesignCanvas::is_sketching() const { return m_sketch_tool.is_active(); } + +void DesignCanvas::cancel_sketch() +{ + m_sketch_tool.cancel(); + if (m_canvas) m_canvas->set_as_dirty(); + if (m_canvas_widget) m_canvas_widget->Refresh(); +} + +void DesignCanvas::set_on_sketch_commit(std::function cb) +{ + m_on_sketch_commit = std::move(cb); +} + +void DesignCanvas::set_on_sketch_entities_commit( + std::function&, + const std::vector&, + const SketchPlane&)> cb) +{ + m_on_sketch_entities_commit = std::move(cb); +} + +void DesignCanvas::set_on_segment_drawn(std::function cb) +{ + m_sketch_tool.on_segment_drawn = std::move(cb); +} + +void DesignCanvas::set_on_cursor_metrics(std::function cb) +{ + m_sketch_tool.on_cursor_metrics = std::move(cb); +} + +void DesignCanvas::set_on_solve_state(std::function cb) +{ + m_sketch_tool.on_solve_state = std::move(cb); +} + +void DesignCanvas::apply_segment_length(double len) +{ + m_sketch_tool.apply_segment_length(len); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +void DesignCanvas::keep_segment_as_drawn() +{ + m_sketch_tool.keep_segment_as_drawn(); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +void DesignCanvas::set_on_sketch_selection_changed(std::function cb) +{ + m_sketch_tool.on_selection_changed = std::move(cb); +} + +void DesignCanvas::set_on_sketch_face_selected(std::function cb) +{ + m_sketch_tool.on_face_selected = std::move(cb); +} + +void DesignCanvas::set_on_display_sketch_selected(std::function cb) +{ + m_sketch_tool.on_display_sketch_selected = std::move(cb); +} + +std::vector DesignCanvas::selected_loop_entities() const +{ + return m_sketch_tool.selected_loop_entities(); +} + +std::vector> DesignCanvas::region_entity_indices(const std::vector& ents) const +{ + return m_sketch_tool.region_entity_indices(ents); +} + +void DesignCanvas::clear_loop_pick() +{ + m_sketch_tool.clear_display_pick(); +} + +void DesignCanvas::set_solid_pick(const std::vector* bodies, const TriangleMesh* mesh, + const std::vector* tri_face, const std::vector* tri_body, + const std::vector* visible, + const std::vector* xform) +{ + m_color_bodies = bodies; // stable address (m_doc.bodies); reload() reads colour overrides + m_sketch_tool.set_solid_pick(bodies, mesh, tri_face, tri_body, visible, xform); +} + +// Effective display colour for a body: per-body override (Color tool) when set, else the +// auto body-index palette. body_palette() is the file-static helper defined above reload(). +ColorRGBA DesignCanvas::body_color(int body) const +{ + if (m_color_bodies != nullptr && body >= 0 && body < int(m_color_bodies->size()) + && (*m_color_bodies)[body].has_color) + return (*m_color_bodies)[body].color; + return body_palette(body); +} + +void DesignCanvas::begin_move_body(int body, const Vec3d& pivot, const Transform3d& base_xform) +{ + m_sketch_tool.set_move_gizmo(body, pivot, base_xform); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } // llvmpipe: force repaint +} + +void DesignCanvas::clear_move_gizmo() +{ + m_sketch_tool.clear_move_gizmo(); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +bool DesignCanvas::moving_body() const { return m_sketch_tool.moving_body(); } + +void DesignCanvas::set_on_body_move_changed(std::function cb) +{ + m_sketch_tool.on_body_move_changed = std::move(cb); +} + +bool DesignCanvas::begin_fillet_gizmo(const Vec3d& body_centroid, double radius) +{ + const bool ok = m_sketch_tool.set_fillet_gizmo(body_centroid, radius); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } // llvmpipe: force repaint + return ok; +} + +void DesignCanvas::clear_fillet_gizmo() +{ + m_sketch_tool.clear_fillet_gizmo(); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +bool DesignCanvas::filleting() const { return m_sketch_tool.filleting(); } + +void DesignCanvas::set_on_fillet_radius_changed(std::function cb) +{ + m_sketch_tool.on_fillet_radius_changed = std::move(cb); +} + +void DesignCanvas::begin_hole_gizmo(const SketchPlane& plane, double x, double y, + double diameter, double depth, bool through) +{ + m_sketch_tool.set_hole_gizmo(plane, x, y, diameter, depth, through); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } // llvmpipe: force repaint +} + +void DesignCanvas::set_hole_face_bounds(bool has, double umin, double umax, double vmin, double vmax) +{ + m_sketch_tool.set_hole_face_bounds(has, umin, umax, vmin, vmax); +} + +void DesignCanvas::clear_hole_gizmo() +{ + m_sketch_tool.clear_hole_gizmo(); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +bool DesignCanvas::holing() const { return m_sketch_tool.holing(); } + +void DesignCanvas::set_on_hole_changed(std::function cb) +{ + m_sketch_tool.on_hole_changed = std::move(cb); +} + +void DesignCanvas::begin_thread_gizmo(const SketchPlane& plane, double x, double y, + double radius, double height) +{ + m_sketch_tool.set_thread_gizmo(plane, x, y, radius, height); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } // llvmpipe: force repaint +} + +void DesignCanvas::clear_thread_gizmo() +{ + m_sketch_tool.clear_thread_gizmo(); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +bool DesignCanvas::threading() const { return m_sketch_tool.threading(); } + +void DesignCanvas::set_on_thread_changed(std::function cb) +{ + m_sketch_tool.on_thread_changed = std::move(cb); +} + +void DesignCanvas::begin_shell_gizmo(const Vec3d& face_centroid, const Vec3d& inward_dir, + double thickness) +{ + m_sketch_tool.set_shell_gizmo(face_centroid, inward_dir, thickness); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } // llvmpipe: force repaint +} + +void DesignCanvas::clear_shell_gizmo() +{ + m_sketch_tool.clear_shell_gizmo(); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +bool DesignCanvas::shelling() const { return m_sketch_tool.shelling(); } + +void DesignCanvas::set_on_shell_thickness_changed(std::function cb) +{ + m_sketch_tool.on_shell_thickness_changed = std::move(cb); +} + +void DesignCanvas::begin_revolve_gizmo(const SketchPlane& plane, const Vec2d& centroid, + int axis_sel, double angle, bool flip) +{ + m_sketch_tool.set_revolve_gizmo(plane, centroid, axis_sel, angle, flip); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } // llvmpipe: force repaint +} + +void DesignCanvas::clear_revolve_gizmo() +{ + m_sketch_tool.clear_revolve_gizmo(); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +bool DesignCanvas::revolving() const { return m_sketch_tool.revolving(); } + +void DesignCanvas::set_on_revolve_angle_changed(std::function cb) +{ + m_sketch_tool.on_revolve_angle_changed = std::move(cb); +} + +void DesignCanvas::begin_pattern_gizmo(const SketchPlane& plane, const Vec3d& body_centroid, + bool circular, int count, int dir, double spacing, double angle) +{ + m_sketch_tool.set_pattern_gizmo(plane, body_centroid, circular, count, dir, spacing, angle); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } // llvmpipe: force repaint +} + +void DesignCanvas::clear_pattern_gizmo() +{ + m_sketch_tool.clear_pattern_gizmo(); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +bool DesignCanvas::patterning() const { return m_sketch_tool.patterning(); } + +void DesignCanvas::set_on_pattern_changed(std::function cb) +{ + m_sketch_tool.on_pattern_changed = std::move(cb); +} + +void DesignCanvas::set_on_solid_selection_changed(std::function cb) +{ + m_sketch_tool.on_solid_selection_changed = std::move(cb); +} + +void DesignCanvas::set_on_place_on_face(std::function cb) +{ + m_sketch_tool.on_place_on_face = std::move(cb); +} + +void DesignCanvas::select_body(int body) +{ + m_sketch_tool.select_body(body); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } // redraw the overlay (llvmpipe) +} + +void DesignCanvas::set_extrude_gizmo(const SketchPlane& plane, const Vec2d& centroid, + double depth, double depth2, bool two_sided, bool flip) +{ + m_sketch_tool.set_extrude_gizmo(plane, centroid, depth, depth2, two_sided, flip); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } // llvmpipe: force repaint +} + +void DesignCanvas::clear_extrude_gizmo() +{ + m_sketch_tool.clear_extrude_gizmo(); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +void DesignCanvas::set_on_extrude_depth_changed(std::function cb) +{ + m_sketch_tool.on_extrude_depth_changed = std::move(cb); +} + +void DesignCanvas::set_on_sketch_exit(std::function cb) +{ + m_sketch_tool.on_exit = std::move(cb); +} + +void DesignCanvas::set_on_move_exit(std::function cb) +{ + m_sketch_tool.on_move_exit = std::move(cb); +} + +void DesignCanvas::set_on_undo_redo(std::function cb) +{ + m_sketch_tool.on_undo_redo = std::move(cb); +} + +void DesignCanvas::set_display_sketches(std::vector ds) +{ + m_sketch_tool.set_display_sketches(std::move(ds)); + // Direct render: under llvmpipe a scheduled Refresh() often doesn't repaint + // unless some other event (e.g. a modal close) forces it, so programmatic + // overlay changes (hide/show, re-solve) could leave a stale overlay. + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +void DesignCanvas::set_datum_planes(std::vector planes) +{ + m_sketch_tool.set_datum_planes(std::move(planes)); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } // llvmpipe: force repaint +} + +void DesignCanvas::set_readout(const std::string& text) +{ + if (!m_hud || !m_hud_label || !m_canvas_widget) return; + if (text == m_hud_last) return; // only touch the WM on a real change + m_hud_last = text; + if (text.empty()) { m_hud->Hide(); return; } + m_hud_label->SetLabel(wxString::FromUTF8(text)); + m_hud->Fit(); + // Anchor to the canvas's bottom-right corner with a small margin (screen coords). + const wxSize cs = m_canvas_widget->GetClientSize(); + const wxSize hs = m_hud->GetSize(); + const wxPoint br = m_canvas_widget->ClientToScreen( + wxPoint(cs.GetWidth() - hs.GetWidth() - 12, cs.GetHeight() - hs.GetHeight() - 12)); + if (!m_hud->IsShown()) m_hud->Show(); // Show before Move (GTK ignores pre-map Move) + m_hud->Move(br); + m_hud->Raise(); +} + +void DesignCanvas::set_body_highlight(bool on) +{ + if (m_body_selected == on) return; + m_body_selected = on; + reload(true); // recolours the body volume (selected = cyan tint) +} + +void DesignCanvas::set_body_translucent(bool on) +{ + if (m_body_translucent == on) return; + m_body_translucent = on; + reload(true); // re-applies object-0 alpha so the solid fades for the fillet preview +} + +void DesignCanvas::set_body_hidden(bool on) +{ + if (m_body_hidden == on) return; + m_body_hidden = on; + reload(true); // hides/show base bodies + flips the ghost opaque/faint for preview-only mode +} + +void DesignCanvas::delete_selected_sketch_entities() +{ + m_sketch_tool.delete_selected(); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +void DesignCanvas::clear_sketch_selection() +{ + m_sketch_tool.clear_selection(); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +DesignSketchTool::DimType DesignCanvas::sketch_dimension_kind() const +{ + return m_sketch_tool.dimension_kind(); +} + +double DesignCanvas::sketch_dimension_current() const +{ + return m_sketch_tool.dimension_current(); +} + +void DesignCanvas::apply_sketch_dimension(double v) +{ + m_sketch_tool.apply_dimension(v); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +void DesignCanvas::open_inline_value(double current, std::function commit, + std::function cancel) +{ + if (!m_inline_editor || !m_canvas_widget) { if (cancel) cancel(); return; } + // Host-driven value entry (committed-feature Constrain path): the trigger is a toolbar + // button. Anchor the field OVER the picked geometry (same as the draw-then-edit tools) when + // the tool can project it; else fall back to the viewport centre, where the sketch is in + // view. GetScreenRect collapses GetClientSize()+ClientToScreen() into one call; if the GL + // canvas reports degenerate geometry (transiently, right after a re-layout), fall back to the + // always-realised top-level window so the editor never lands in the top-left corner. + wxRect r = m_canvas_widget->GetScreenRect(); + if (r.GetWidth() <= 1 || r.GetHeight() <= 1) { + if (wxWindow* top = wxGetTopLevelParent(m_canvas_widget)) + r = top->GetScreenRect(); + } + wxPoint scr(r.GetLeft() + r.GetWidth() / 2, r.GetTop() + r.GetHeight() / 2); + wxPoint anchor; + if (m_sketch_tool.constrain_value_anchor(anchor)) { // device px in the canvas viewport + const double s = m_canvas_widget->GetContentScaleFactor(); + scr = m_canvas_widget->ClientToScreen(wxPoint(int(anchor.x / s), int(anchor.y / s))); + } + // Freeze the canvas so focus-follows-mouse can't steal keyboard focus off the field — the + // same fix the draw-then-edit path uses (cursor focus stays on the field, no pre-click). + m_sketch_tool.set_inline_busy(true); + m_inline_editor->open(scr, current, + [this, commit](double v) { + m_sketch_tool.set_inline_busy(false); + if (commit) commit(v); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } + }, + [this, cancel]() { + m_sketch_tool.set_inline_busy(false); + if (cancel) cancel(); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } + }); +} + +void DesignCanvas::set_on_dimension_pick_complete(std::function cb) +{ + m_sketch_tool.on_dimension_pick_complete = std::move(cb); +} + +DesignSketchTool::DimType DesignCanvas::pending_dimension_type() const +{ + return m_sketch_tool.pending_dimension_type(); +} + +void DesignCanvas::set_sketch_dimension_value(double v) +{ + m_sketch_tool.set_dimension_value(v); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +void DesignCanvas::cancel_sketch_dimension() +{ + m_sketch_tool.cancel_dimension_value(); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +void DesignCanvas::begin_constrain(const SketchProfile& prof, const SketchPlane& plane) +{ + m_sketch_tool.begin_constrain(prof, plane); + // The overlay must appear immediately (no mouse move to trigger a repaint); + // a direct render() is the proven path under llvmpipe. + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +void DesignCanvas::begin_imported_transform( + int feat, const std::vector>>& base_regions, + const SketchPlane& plane, const Vec2d& offset, double scale_x, double scale_y) +{ + m_sketch_tool.begin_imported_transform(feat, base_regions, plane, offset, scale_x, scale_y); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +void DesignCanvas::set_on_imported_transform(std::function cb) +{ + m_sketch_tool.on_imported_transform = std::move(cb); +} + +void DesignCanvas::end_constrain() +{ + // cancel() clears m_active + the picked-segment/entity indices, so the + // constrain overlay (highlighted picks) disappears on the next render. + m_sketch_tool.cancel(); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +bool DesignCanvas::is_constraining() const { return m_sketch_tool.is_constraining(); } + +bool DesignCanvas::selected_segment(int& a, int& b) const +{ + return m_sketch_tool.selected_segment(a, b); +} + +void DesignCanvas::update_constrain_profile(const std::vector& pts) +{ + m_sketch_tool.set_profile_points(pts); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +void DesignCanvas::begin_constrain_entities(const std::vector& ents, + const SketchPlane& plane) +{ + m_sketch_tool.begin_constrain_entities(ents, plane); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +bool DesignCanvas::is_constraining_entities() const +{ + return m_sketch_tool.is_constraining_entities(); +} + +bool DesignCanvas::selected_constrain_entities(int& e0, int& e1) const +{ + return m_sketch_tool.selected_constrain_entities(e0, e1); +} + +int DesignCanvas::selected_constrain_axis() const +{ + return m_sketch_tool.pick2(); +} + +bool DesignCanvas::pick0_point(Vec2d& out) const +{ + return m_sketch_tool.pick0_point(out); +} + +void DesignCanvas::update_constrain_entities(const std::vector& ents) +{ + m_sketch_tool.set_constrain_entities(ents); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +void DesignCanvas::set_constraint_highlight(std::vector entities) +{ + m_sketch_tool.set_constraint_highlight(std::move(entities)); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +void DesignCanvas::set_constraint_glyphs(std::vector cons) +{ + m_sketch_tool.set_constraint_glyphs(std::move(cons)); + if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/DesignCanvas.hpp b/src/slic3r/GUI/DesignCanvas.hpp new file mode 100644 index 0000000000..f1010c584a --- /dev/null +++ b/src/slic3r/GUI/DesignCanvas.hpp @@ -0,0 +1,236 @@ +#ifndef slic3r_DesignCanvas_hpp_ +#define slic3r_DesignCanvas_hpp_ + +#include + +#include +#include +#include + +#include "3DBed.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/SketchEngine.hpp" +#include "DesignSketchTool.hpp" + +class wxGLCanvas; +class wxFrame; +class wxStaticText; + +namespace Slic3r { + +class TriangleMesh; + +namespace GUI { + +class GLCanvas3D; +class SketchInlineEditor; + +class DesignCanvas : public wxPanel +{ +public: + explicit DesignCanvas(wxWindow* parent); + ~DesignCanvas() override; + + void set_mesh(const TriangleMesh& mesh); + // Multi-body display: one GLVolume per body, each coloured distinctly (per-body colour). + // `visible` (optional, indexed by body) hides bodies whose flag is false. + void set_bodies(const std::vector& body_meshes, + const std::vector& visible = {}); + void clear_mesh(); + + void set_preview_mesh(const TriangleMesh& mesh); + void clear_preview(); + + void fit_view(); + void set_view(const std::string& view_name); + + void begin_sketch(const SketchPlane& plane, DesignSketchTool::Mode mode); + // Re-open a committed entity sketch for full in-canvas editing (load geometry + + // constraints, re-detect feature groups). Re-commits via finish_sketch(). + void edit_sketch(const std::vector& entities, + const std::vector& constraints, + const SketchPlane& plane); + void set_sketch_tool(DesignSketchTool::Mode mode); + void set_sketch_construction(bool c); + void set_sketch_polygon_sides(int n); + void set_sketch_polygon_circumscribed(bool c); + void finish_sketch(); + bool is_sketching() const; + void refresh_bed(); // re-sync the bed to the current printer (call on tab activation) + void cancel_sketch(); + void set_on_sketch_commit(std::function cb); + void set_on_sketch_entities_commit( + std::function&, + const std::vector&, + const SketchPlane&)> cb); + + // Line tool: pending-segment length entry + live readout (Phase 2). + void set_on_segment_drawn(std::function cb); + void set_on_cursor_metrics(std::function cb); + void set_on_solve_state(std::function cb); // dof, ok, has_constraints + void apply_segment_length(double len); // exact length, then commit & repaint + void keep_segment_as_drawn(); // commit as-drawn & repaint + + // Sketch selection (Mode::Select). + void set_on_sketch_selection_changed(std::function cb); + void set_on_sketch_face_selected(std::function cb); // closed loop clicked + void set_on_display_sketch_selected(std::function cb); // committed loop clicked: (feature, region) + std::vector selected_loop_entities() const; // entities of the click-selected loop + std::vector> region_entity_indices(const std::vector& ents) const; + void clear_loop_pick(); // drop the click-selected loop highlight (e.g. after extrude) + // Solid whole/face/edge selection: point the tool at the bodies + concatenated + // tessellation (with per-triangle face & body ids), and a callback fired on each + // whole->face->edge cycle (level, body index, face id, edge id). + void set_solid_pick(const std::vector* bodies, const TriangleMesh* mesh, + const std::vector* tri_face, const std::vector* tri_body, + const std::vector* visible = nullptr, + const std::vector* xform = nullptr); + void set_on_solid_selection_changed(std::function cb); + void set_on_place_on_face(std::function cb); // F key: Place on Face + void select_body(int body); // Parts-list -> highlight a whole body by index + // Effective display colour of a body: the per-body override (Color tool) when set, + // otherwise the auto body-index palette. Single source of truth shared with reload(). + ColorRGBA body_color(int body) const; + // Move-body gizmo (M5): three world-axis drag arrows on a body; drag fires the move + // callback with the body index + accumulated translation (display-only, host applies it). + void begin_move_body(int body, const Vec3d& pivot, const Transform3d& base_xform); + void clear_move_gizmo(); + bool moving_body() const; + void set_on_body_move_changed(std::function cb); + // Visual Fillet/Chamfer radius gizmo: when a solid edge is picked, anchor a radius arrow on + // it; drag/edit fire the radius callback. Returns false if no edge is currently picked. + bool begin_fillet_gizmo(const Vec3d& body_centroid, double radius); + void clear_fillet_gizmo(); + bool filleting() const; + void set_on_fillet_radius_changed(std::function cb); + // Visual Hole gizmo: the panel feeds the hole plane + position + diameter/depth/through while + // its Hole card is open; drag/edit fire the hole callback (x, y, diameter, depth). + void begin_hole_gizmo(const SketchPlane& plane, double x, double y, + double diameter, double depth, bool through); + void set_hole_face_bounds(bool has, double umin, double umax, double vmin, double vmax); + void clear_hole_gizmo(); + bool holing() const; + void set_on_hole_changed(std::function cb); + // Visual Thread gizmo: footprint circle + radius/length arrows + draggable centre. + void begin_thread_gizmo(const SketchPlane& plane, double x, double y, + double radius, double height); + void clear_thread_gizmo(); + bool threading() const; + void set_on_thread_changed(std::function cb); + // Visual Shell gizmo: inward thickness arrow at the picked open-face centroid. + void begin_shell_gizmo(const Vec3d& face_centroid, const Vec3d& inward_dir, double thickness); + void clear_shell_gizmo(); + bool shelling() const; + void set_on_shell_thickness_changed(std::function cb); + // Visual Revolve angle-arc gizmo: the panel feeds the sketch plane + profile centroid + axis + // (0=plane X, 1=plane Y) + angle + flip while its Revolve card is open; drag/edit fire the + // angle callback. + void begin_revolve_gizmo(const SketchPlane& plane, const Vec2d& centroid, + int axis_sel, double angle, bool flip); + void clear_revolve_gizmo(); + bool revolving() const; + void set_on_revolve_angle_changed(std::function cb); + // Visual Pattern gizmo: the panel feeds the (world XY) plane + target body centroid + mode + + // count/dir/spacing/angle while its Pattern card is open; drag/edit fire the value callback. + void begin_pattern_gizmo(const SketchPlane& plane, const Vec3d& body_centroid, bool circular, + int count, int dir, double spacing, double angle); + void clear_pattern_gizmo(); + bool patterning() const; + void set_on_pattern_changed(std::function cb); + // Visual Extrude depth-arrow gizmo (C5b): the panel feeds the profile plane + centroid + + // live depths/flags while its Extrude card is open; drag/edit fire the depth callback. + void set_extrude_gizmo(const SketchPlane& plane, const Vec2d& centroid, + double depth, double depth2, bool two_sided, bool flip); + void clear_extrude_gizmo(); + void set_on_extrude_depth_changed(std::function cb); + void set_on_sketch_exit(std::function cb); // Esc -> exit the tool + void set_on_undo_redo(std::function cb); // Ctrl+Z / Ctrl+Shift+Z + // Persistently draw committed sketches (un-consumed ones stay visible). + void set_display_sketches(std::vector ds); + void set_datum_planes(std::vector planes); // draw datum/reference planes + void set_body_highlight(bool on); // tint the solid when its feature is tree-selected + void set_body_translucent(bool on); // render the solid see-through (fillet/chamfer preview) + void set_body_hidden(bool on); // preview-only: hide base bodies, show only the result ghost + void set_on_move_exit(std::function cb); // right-click finished the move-body gizmo + void delete_selected_sketch_entities(); + void clear_sketch_selection(); + + // Dimension tool: act on the current sketch selection. + DesignSketchTool::DimType sketch_dimension_kind() const; + double sketch_dimension_current() const; + void apply_sketch_dimension(double v); + + // Open the in-canvas value editor at the cursor for a host-driven value (the + // committed-feature Constrain path uses this instead of a docked numeric card). + void open_inline_value(double current, std::function commit, + std::function cancel = {}); + + // Dimension tool (Mode::Dimension): click-to-place quotes. The pick-complete + // callback lets the panel pop the value card; set/cancel apply or keep the value. + void set_on_dimension_pick_complete(std::function cb); + DesignSketchTool::DimType pending_dimension_type() const; + void set_sketch_dimension_value(double v); + void cancel_sketch_dimension(); + + // Constrain mode: load a committed profile for picking + constraint editing. + void begin_constrain(const SketchProfile& prof, const SketchPlane& plane); + // Leave constrain mode and clear any picked-entity highlight from the overlay. + void end_constrain(); + bool is_constraining() const; + bool selected_segment(int& a, int& b) const; + void update_constrain_profile(const std::vector& pts); + + // Entity-aware Constrain (Fase 4.2): pick Line entities of a committed sketch. + void begin_constrain_entities(const std::vector& ents, const SketchPlane& plane); + bool is_constraining_entities() const; + + // In-canvas bbox transform of imported Text/SVG art (replaces the Move/Scale dialog). + void begin_imported_transform(int feat, + const std::vector>>& base_regions, + const SketchPlane& plane, const Vec2d& offset, + double scale_x, double scale_y); + void set_on_imported_transform(std::function cb); + bool selected_constrain_entities(int& e0, int& e1) const; + int selected_constrain_axis() const; // third pick slot (Symmetric axis), -1 if unset + bool pick0_point(Vec2d& out) const; // plane-coords of the slot-0 pick (trim/extend) + void update_constrain_entities(const std::vector& ents); + // Constraint manager (C3.4): highlight the entities referenced by a selected + // constraint (yellow tint in Constrain mode); empty clears the highlight. + void set_constraint_highlight(std::vector entities); + // Constraint glyph badges (C3.4b): the feature's constraints, drawn as iconic + // marks near their entities in Constrain mode; empty clears them. + void set_constraint_glyphs(std::vector cons); + +private: + void reload(bool keep_view); + + wxGLCanvas* m_canvas_widget{nullptr}; + GLCanvas3D* m_canvas{nullptr}; + Bed3D m_bed; + Model m_model; + bool m_first_frame{true}; + bool m_body_selected{false}; // tree selected a body feature → tint the solid + bool m_body_translucent{false};// fillet/chamfer preview → render the body see-through + bool m_body_hidden{false}; // preview-only mode → hide base bodies, ghost = the result + std::vector m_body_visible; // per-body visibility (empty => all visible) + // Live pointer to the document's bodies (stable address: m_doc.bodies), stashed by + // set_solid_pick so reload()/body_color() can read each body's colour override. + const std::vector* m_color_bodies{nullptr}; + + DesignSketchTool m_sketch_tool; + std::unique_ptr m_inline_editor; // floating in-canvas value editor + // Bottom-right viewport HUD: a borderless float label over the GL canvas showing the + // active tool's current values (fed by the tool's on_readout). Empty text hides it. + wxFrame* m_hud{nullptr}; + wxStaticText* m_hud_label{nullptr}; + std::string m_hud_last; + void set_readout(const std::string& text); + std::function m_on_sketch_commit; + std::function&, + const std::vector&, + const SketchPlane&)> m_on_sketch_entities_commit; +}; + +}} // namespace Slic3r::GUI + +#endif // slic3r_DesignCanvas_hpp_ diff --git a/src/slic3r/GUI/DesignPanel.cpp b/src/slic3r/GUI/DesignPanel.cpp new file mode 100644 index 0000000000..2c5ed69c75 --- /dev/null +++ b/src/slic3r/GUI/DesignPanel.cpp @@ -0,0 +1,5314 @@ +#include "DesignPanel.hpp" +#include "DesignCanvas.hpp" +#include "DesignSketchTool.hpp" +#include "libslic3r/GeometryEngine.hpp" // face_by_index for face-extrude gizmo anchor + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "slic3r/GUI/wxExtensions.hpp" // ScalableButton, create_scaled_bitmap +#include "Widgets/Label.hpp" // HarmonyOS Sans fonts (Head_*/Body_*) shared with the rest of Orca +#include "Widgets/DropDown.hpp" // Orca-themed combo dropdown (white/teal selector) for the tool flyouts +#include "libslic3r/SketchImport.hpp" // text_to_regions / svg_to_regions +#include "libslic3r/ThreadStandards.hpp" // ISO metric / Unified imperial thread tables +#include "libslic3r/Model.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/MainFrame.hpp" +#include "slic3r/GUI/GUI_ObjectList.hpp" + +// Apple-esque consistency: the Design tab speaks ONE uniform vocabulary — English — so it +// never reads as a half-Italian/half-English patchwork against the host's localized chrome. +// Bypass the gettext catalog for every _L() in this TU (the surrounding Orca UI still follows +// the app locale; only our tool/card/toolbar strings are pinned). One lever, whole file. +#ifdef _L +#undef _L +#endif +#define _L(s) wxString::FromUTF8(s) + +namespace Slic3r { namespace GUI { + +// Format a value with the international ('.') decimal separator regardless of the +// app's LC_NUMERIC locale (wx sets it to the user locale at startup). snprintf may +// emit a comma, so normalise it. +static wxString en_format(double v, int digits = 2) +{ + char fmt[16]; + std::snprintf(fmt, sizeof(fmt), "%%.%df", digits); + char buf[64]; + std::snprintf(buf, sizeof(buf), fmt, v); + for (char* c = buf; *c; ++c) if (*c == ',') *c = '.'; + return wxString::FromUTF8(buf); +} +// Parse a user-typed value accepting either '.' or ',' as the decimal separator. +static bool en_parse(const wxString& text, double& out) +{ + wxString t(text); + t.Replace(wxT(","), wxT(".")); + return t.ToCDouble(&out); +} + +// Design-tab chrome tokens. The dark branch returns the EXACT legacy values so the +// (correct) dark theme stays byte-identical; the light branch maps each onto Orca's +// light surface so the ribbon/sidebar follow the app theme instead of staying black. +static bool dp_dark() { return wxGetApp().dark_mode(); } +static wxColour dp_ribbon_bg() { return dp_dark() ? wxColour(0x36,0x36,0x3C) : wxColour(0xEC,0xEC,0xEE); } +static wxColour dp_ribbon_hover() { return dp_dark() ? wxColour(0x4D,0x4D,0x54) : wxColour(0xD7,0xD7,0xDB); } +static wxColour dp_panel_bg() { return dp_dark() ? wxColour(0x2D,0x2D,0x30) : wxColour(0xFB,0xFB,0xFD); } +static wxColour dp_sec_text() { return dp_dark() ? wxColour(0x81,0x81,0x83) : wxColour(0x66,0x66,0x68); } +static wxColour dp_ctl_text() { return dp_dark() ? wxColour(0xC8,0xC8,0xC8) : wxColour(0x35,0x35,0x37); } +static wxColour dp_item_text() { return dp_dark() ? wxColour(0xE0,0xE0,0xE0) : wxColour(0x2C,0x2C,0x2E); } +static wxColour dp_item_dim() { return dp_dark() ? wxColour(0x80,0x80,0x80) : wxColour(0xA0,0xA0,0xA2); } + +static wxSpinCtrlDouble* make_spin(wxWindow* parent, double val, + double mn = 0.1, double mx = 1000.0) +{ + auto* s = new wxSpinCtrlDouble(parent, wxID_ANY, "", wxDefaultPosition, wxSize(90, -1)); + s->SetRange(mn, mx); + s->SetDigits(2); + s->SetValue(val); + return s; +} + +static SketchPlane plane_from_index(int i) +{ + switch (i) { + case 1: return SketchPlane::XZ(); + case 2: return SketchPlane::YZ(); + default: return SketchPlane::XY(); + } +} + +// Inverse of plane_from_index: recover the wxChoice row from a plane's normal. +// XY normal=(0,0,1)->0, XZ normal=(0,1,0)->1, YZ normal=(1,0,0)->2. +static int index_from_plane(const SketchPlane& p) +{ + if (std::abs(p.normal.y()) > 0.5) return 1; // XZ + if (std::abs(p.normal.x()) > 0.5) return 2; // YZ + return 0; // XY +} + +// #2: a sketch plane on `face` with origin at the face centroid and normal pointing INTO the +// solid, so a positioned hole drills inward and its (x,y) read as the offset from the face +// centre. A hole is rotationally symmetric, so the arbitrary in-plane basis is harmless. +static SketchPlane face_plane_inward(const TopoDS_Face& face) +{ + SketchPlane p; + p.origin = GeometryEngine::face_centroid_world(face); + p.normal = (-GeometryEngine::face_normal_world(face)).normalized(); // inward + // Align the in-plane x-axis with the face's LONGEST straight edge so the (u,v) frame matches + // the face sides — then "distance from a side" (the hole construction dims) reads correctly. + Vec3d x(0, 0, 0); double best = 0; + for (const TopoDS_Edge& e : GeometryEngine::edges_of_face(face)) { + const std::vector pts = GeometryEngine::sample_edge_world(e); + if (pts.size() < 2) continue; + Vec3d d = pts.back() - pts.front(); + d = d - p.normal * d.dot(p.normal); // project the edge direction into the plane + const double len = d.norm(); + if (len > best) { best = len; x = d / len; } + } + if (best < 1e-9) { // curved/edgeless face: fall back to an arbitrary in-plane basis + const Vec3d ref = std::abs(p.normal.z()) < 0.9 ? Vec3d(0, 0, 1) : Vec3d(1, 0, 0); + x = ref.cross(p.normal).normalized(); + } + p.x_axis = x.normalized(); + p.y_axis = p.normal.cross(p.x_axis).normalized(); + return p; +} + +DesignPanel::DesignPanel(wxWindow* parent) + : wxPanel(parent, wxID_ANY) +{ + // Left column: a slim feature-tree + docked tool-dialog column. All form + // controls are parented to m_form so it can scroll independently of the + // live GL viewport. The tool buttons live in the top toolbar (built below). + m_form = new wxScrolledWindow(this, wxID_ANY); + // The sidebar/panel never carried an explicit background, so in light theme it + // inherited the dark window colour and stayed black. Paint it on the light surface; + // dark is left untouched (it already reads correctly via inheritance). + if (!dp_dark()) { + SetBackgroundColour(dp_panel_bg()); + m_form->SetBackgroundColour(dp_panel_bg()); + } + + auto* root = new wxBoxSizer(wxVERTICAL); + { + auto* hdr = new wxStaticText(m_form, wxID_ANY, _L("Design")); + hdr->SetFont(Label::Head_16); // Orca shared HarmonyOS section-title font + root->Add(hdr, 0, wxLEFT | wxRIGHT | wxTOP, 12); + root->AddSpacer(2); + } + + // === Top contextual toolbar (Onshape-style icon strip) === + // Parented to the panel (sits above the form/viewport row). Only the active + // mode's group is shown; the others are hidden by set_ui_mode(). + // Scrollable ribbon: on a narrow/windowed screen the far-right action bar (Confirm/Cancel) + // used to be clipped off the edge with no way to reach it. Horizontal-only scroll (vertical + // rate 0) keeps it reachable; on a wide screen the stretch spacer still pins it far-right. + m_toolbar = new wxScrolledWindow(this, wxID_ANY); + m_toolbar->SetScrollRate(15, 0); + m_toolbar->ShowScrollbars(wxSHOW_SB_DEFAULT, wxSHOW_SB_NEVER); + // Theme-aware tool ribbon: dark uses Orca's elevated surface (#36363C / hover + // #4D4D54); light maps onto the app's light chrome so the strip follows the theme. + m_toolbar->SetBackgroundColour(dp_ribbon_bg()); + + const wxColour tb_bg = dp_ribbon_bg(); + const wxColour tb_hover = dp_ribbon_hover(); + auto icon_btn = [this, tb_bg, tb_hover](const char* icon, const wxString& tip) { + // Prepare-toolbar-sized buttons (40px cell / 28px glyph) so the Design + // ribbon matches the rest of the app instead of feeling tiny. + auto* b = new ScalableButton(m_toolbar, wxID_ANY, icon, "", wxSize(52, 52), + wxDefaultPosition, wxBU_EXACTFIT | wxBORDER_NONE, false, 42); + b->SetToolTip(tip); + b->SetBackgroundColour(tb_bg); + m_tool_btns.push_back(b); + // Hover affordance, honouring the active-tool teal state. + b->Bind(wxEVT_ENTER_WINDOW, [this, b, tb_hover](wxMouseEvent& e) { + b->SetBackgroundColour(b == m_active_tool_btn ? wxColour(0x52, 0xC7, 0xB8) : tb_hover); + b->Refresh(); e.Skip(); }); + b->Bind(wxEVT_LEAVE_WINDOW, [this, b, tb_bg](wxMouseEvent& e) { + b->SetBackgroundColour(b == m_active_tool_btn ? wxColour(0x00, 0x96, 0x88) : tb_bg); + b->Refresh(); e.Skip(); }); + // Mark this tool active (teal) on press — a separate event from the + // button's command handler, so it never swallows the click action. + b->Bind(wxEVT_LEFT_DOWN, [this, b](wxMouseEvent& e) { + set_active_tool_btn(b); e.Skip(); }); + return b; + }; + // Small grey group caption (Onshape-style section hint) for each toolbar mode. + auto caption = [this](const wxString& t) { + auto* s = new wxStaticText(m_toolbar, wxID_ANY, t); + wxFont f = Label::Body_12; f.SetWeight(wxFONTWEIGHT_BOLD); + s->SetFont(f); + s->SetForegroundColour(dp_sec_text()); // Orca dark secondary text + return s; + }; + auto add_sep = [this](wxSizer* row) { + row->AddSpacer(5); + row->Add(new wxStaticLine(m_toolbar, wxID_ANY, wxDefaultPosition, wxSize(1, 22), wxLI_VERTICAL), + 0, wxALIGN_CENTER_VERTICAL); + row->AddSpacer(5); + }; + + // Shared sketch-tool selector: begins a session on first use, then switches + // the active entity tool. The Construction toggle marks following entities as + // construction geometry (excluded from the wire). + m_construction = new wxCheckBox(m_toolbar, wxID_ANY, _L("Construction")); + m_construction->SetForegroundColour(dp_ctl_text()); + auto select_tool = [this](DesignSketchTool::Mode mode, const wxString& hint) { + if (!m_viewport) return; + if (!m_viewport->is_sketching()) { + const SketchPlane plane = plane_from_choice(m_draw_plane->GetSelection()); + m_viewport->begin_sketch(plane, mode); + m_construction->SetValue(false); // a fresh session starts non-construction + } else { + m_viewport->set_sketch_tool(mode); + } + m_viewport->set_sketch_construction(m_construction->GetValue()); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(hint); + m_status->Refresh(); + }; + + // Shared flyout glyph tint (used by BOTH the feature and sketch toolbars). Re-tint each + // design_* glyph to the DropDown's resolved TEXT colour so it reads on the popup in either + // theme: text_color is 0x363636, which darkModeColorFor() maps to a light tone in dark mode + // (the popup bg is darkModeColorFor(white) = dark) and leaves dark in light mode. The alpha + // (the glyph shape) is preserved; only RGB is replaced. + // ponytail: wxBitmap(img) drops the HiDPI scale factor (no scale ctor before wx 3.1.6); the + // deploy target runs at scale 1.0, so this is exact there. + const wxColour drop_icon_col = StateColor::darkModeColorFor(wxColour(0x36, 0x36, 0x36)); + auto tint = [](wxBitmap bmp, const wxColour& c) -> wxBitmap { + if (!bmp.IsOk()) return bmp; + wxImage img = bmp.ConvertToImage(); + if (!img.HasAlpha()) img.InitAlpha(); + const int w = img.GetWidth(), h = img.GetHeight(); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + img.SetRGB(x, y, c.Red(), c.Green(), c.Blue()); + return wxBitmap(img); + }; + + // --- Feature group: Sketch / Extrude / Fillet-Chamfer / Hole / Thread / Constrain + m_tb_feature = new wxBoxSizer(wxHORIZONTAL); + auto fadd = [this](wxWindow* w) { m_tb_feature->Add(w, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 2); }; + m_tb_feature->Add(caption(_L("FEATURES")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 8); + { + // Onshape-style FEATURE flyouts: same themed-DropDown pattern as the sketch toolbar + // (tinted glyphs, Body_14 measure, content-width popup) but each entry runs an + // arbitrary action — the existing per-feature handler — instead of selecting a Mode. + struct FeatVar { const char* icon; wxString tip; wxString hint; std::function action; }; + struct FeatFlyout { + std::vector texts, tips; + std::vector icons; + std::vector> actions; + std::vector icon_names; + ScalableButton* btn = nullptr; + DropDown drop; // declared LAST: destroyed before the vectors it references + FeatFlyout() : drop(texts, tips, icons) {} + }; + auto feat_dropdown = [&](const char* def_icon, const wxString& grp, std::vector vars) { + auto* b = icon_btn(def_icon, grp); + b->SetFont(Label::Body_14); // measure popup labels in the popup's font (no truncation) + auto fo = std::make_shared(); + for (auto& v : vars) { + fo->texts.push_back(v.tip); + fo->tips.push_back(v.hint); + fo->icons.push_back(tint(create_scaled_bitmap(v.icon, m_form, 18), drop_icon_col)); + fo->actions.push_back(std::move(v.action)); + fo->icon_names.emplace_back(v.icon); + } + fo->btn = b; + fo->drop.Create(b); + fo->drop.SetUseContentWidth(true, false); + fo->drop.Invalidate(true); + FeatFlyout* fp = fo.get(); + fo->drop.Bind(wxEVT_COMBOBOX, [this, fp](wxCommandEvent& e) { + int i = e.GetInt(); + if (i >= 0 && i < (int) fp->actions.size()) { + fp->btn->SetBitmap_(fp->icon_names[i]); // button face follows the last pick + fp->actions[i](); + set_active_tool_btn(fp->btn); + } + }); + b->Bind(wxEVT_BUTTON, [b, fp](wxCommandEvent&) { + // Force a fresh content measure before Popup() (ComboBox does this via the + // private autoPosition()); otherwise the popup maps at a stale narrow size. + fp->drop.Invalidate(true); + fp->drop.SetUseContentWidth(false, false); + fp->drop.SetUseContentWidth(true, false); + wxPoint pos = b->ClientToScreen(wxPoint(0, -6)); + fp->drop.Position(pos, wxSize(0, b->GetSize().y + 12)); + fp->drop.Popup(); + }); + m_flyout_keepalive.push_back(fo); + fadd(b); + auto* chev = new wxStaticText(m_toolbar, wxID_ANY, wxString::FromUTF8("\xE2\x96\xBE")); + chev->SetForegroundColour(dp_sec_text()); + chev->SetFont(Label::Body_9); + m_tb_feature->Add(chev, 0, wxALIGN_BOTTOM | wxBOTTOM | wxRIGHT, 5); + return b; + }; + + auto* b_sketch = icon_btn("design_sketch", _L("Sketch")); + b_sketch->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + populate_plane_choices(m_draw_plane); // surface datum planes in the picker + set_ui_mode(UiMode::Sketch); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(_L("Pick a plane and a sketch tool, then draw")); + m_status->Refresh(); + }); + fadd(b_sketch); + add_sep(m_tb_feature); + // Add material: Extrude / Revolve / Sweep / Loft + feat_dropdown("design_extrude", _L("Add material (extrude / revolve / sweep / loft)"), { + {"design_extrude", _L("Extrude"), _L("Extrude a sketch profile, or push/pull a picked face"), + [this] { + // Onshape push/pull: an explicitly picked solid face (Face-level cycle, no loop + // selected) is extruded as the profile — this takes priority over re-extruding an + // already-consumed sketch (resolve_extrude_sketch always returns the last Sketch). + if (m_sel_solid_face >= 0 && !m_doc.body.IsNull() && m_sel_sketch_region < 0) { + m_extrude_face_src = m_sel_solid_face; + m_extrude_sketch_ref = -1; + open_tool(Tool::Extrude); + return; + } + m_extrude_face_src = -1; // ordinary sketch/loop extrude + m_extrude_sketch_ref = resolve_extrude_sketch(); + if (m_extrude_sketch_ref < 0) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Create a sketch, or pick a solid face, first")); + m_status->Refresh(); + return; + } + open_tool(Tool::Extrude); + }}, + {"design_revolve", _L("Revolve"), _L("Revolve a profile about an axis"), + [this] { + m_revolve_sketch_ref = resolve_extrude_sketch(); + if (m_revolve_sketch_ref < 0) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Create a sketch profile to revolve first")); + m_status->Refresh(); + return; + } + open_tool(Tool::Revolve); + }}, + {"design_sweep", _L("Sweep"), _L("Sweep a profile along a path"), + [this] { + m_sweep_profile_ref = resolve_extrude_sketch(); + m_sweep_path_ref = -1; // fresh sweep: default the picker to the first sketch + if (m_sweep_profile_ref < 0) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Create a profile sketch to sweep first")); + m_status->Refresh(); + return; + } + open_tool(Tool::Sweep); + }}, + {"design_loft", _L("Loft"), _L("Loft (skin) between two or more profiles"), + [this] { + // Loft skins 2+ profile sketches; need at least two to be meaningful. + int n = 0; + for (const auto& f : m_doc.features) + if (f.type == CadFeatureType::Sketch) ++n; + if (n < 2) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Create at least two profile sketches to loft")); + m_status->Refresh(); + return; + } + m_loft_refs.clear(); // fresh loft: nothing pre-checked + open_tool(Tool::Loft); + }}, + }); + + auto* b_pattern = icon_btn("design_pattern", _L("Pattern")); + b_pattern->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + // Pattern replicates an existing body — needs at least one solid. + if (m_doc.bodies.empty()) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Create a solid body to pattern first")); + m_status->Refresh(); + return; + } + open_tool(Tool::Pattern); + }); + fadd(b_pattern); + + auto* b_plane = icon_btn("design_plane", _L("Plane")); + b_plane->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + populate_plane_choices(m_plane_base); // refresh base list w/ existing datum planes + open_tool(Tool::Plane); + }); + fadd(b_plane); + + auto* b_boolean = icon_btn("design_boolean", _L("Boolean (combine bodies)")); + b_boolean->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + // A body-body boolean needs at least two solids to combine. + if (m_doc.bodies.size() < 2) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Boolean needs two bodies — create or import a second solid")); + m_status->Refresh(); + return; + } + populate_body_choices(); + open_tool(Tool::Boolean); + }); + fadd(b_boolean); + + auto* b_cut = icon_btn("design_cut", _L("Cut (split a body with a plane)")); + b_cut->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + // A plane cut needs at least one solid to slice. + if (m_doc.bodies.empty()) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Create a solid body to cut first")); + m_status->Refresh(); + return; + } + populate_plane_choices(m_cut_plane); + populate_body_choices(); + open_tool(Tool::Cut); + }); + fadd(b_cut); + + // Color — override the selected body's display colour (per-body, survives recompute). + auto* b_color = icon_btn("color_palette", _L("Color — set the selected body's display colour")); + b_color->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_set_body_color(); }); + fadd(b_color); + + // Dress-up: Fillet/Chamfer / Draft / Shell + feat_dropdown("design_dressup", _L("Dress-up (fillet / chamfer / draft / shell)"), { + {"design_dressup", _L("Fillet / Chamfer"), _L("Round or bevel a picked edge"), + [this] { open_tool(Tool::Dressup); }}, + {"design_draft", _L("Draft (taper a face)"), _L("Tilt a picked face by a draft angle"), + [this] { open_tool(Tool::Draft); }}, + {"design_shell", _L("Shell"), _L("Hollow the body to a wall thickness, opening a picked face"), + [this] { open_tool(Tool::Shell); }}, + }); + + // Hole / Thread — drilling into a solid (both face-aware) + feat_dropdown("design_hole", _L("Hole / thread"), { + {"design_hole", _L("Hole"), _L("Drill a hole, centred on a picked face or placed on a plane"), + [this] { + // #2: drill on the picked solid face, centred on it (origin = face centroid, + // normal = inward). Otherwise fall back to the plane dropdown. m_hole_x/y then + // read as the offset from the face centre (editable for precise placement). + m_hole_on_face = false; + m_hole_face_body = -1; + m_hole_has_bounds = false; + if (m_sel_solid_face >= 0 && m_sel_solid_body >= 0 + && m_sel_solid_body < int(m_doc.bodies.size())) { + const TopoDS_Face face = GeometryEngine::face_by_index( + m_doc.bodies[m_sel_solid_body].shape, m_sel_solid_face); + if (!face.IsNull()) { + m_hole_face_plane = face_plane_inward(face); + m_hole_on_face = true; + m_hole_face_body = m_sel_solid_body; + // Face (u,v) extents so the hole dims read from the sides (#2 Part B). + m_hole_has_bounds = GeometryEngine::face_plane_bounds( + face, m_hole_face_plane.origin, m_hole_face_plane.x_axis, + m_hole_face_plane.y_axis, m_hole_umin, m_hole_umax, m_hole_vmin, m_hole_vmax); + if (m_hole_x) m_hole_x->SetValue(0.0); // start at the face centre + if (m_hole_y) m_hole_y->SetValue(0.0); + // Reflect the face's orientation in the dropdown so it doesn't keep + // showing a stale "XY" while the hole actually drills on this face. + if (m_hole_plane) + m_hole_plane->SetSelection(index_from_plane(m_hole_face_plane)); + } + } + open_tool(Tool::Hole); + }}, + {"design_thread", _L("Thread"), _L("Thread a picked cylindrical face (hole bore or cylinder)"), + [this] { + // #3: invoke on a picked CYLINDRICAL face — a hole bore (internal thread) or a + // cylinder's lateral surface (external) — deriving axis, radius and internal/ + // external from it. Otherwise fall back to the plane dropdown. + m_thread_on_face = false; + m_thread_face_body = -1; + if (m_sel_solid_face >= 0 && m_sel_solid_body >= 0 + && m_sel_solid_body < int(m_doc.bodies.size())) { + const TopoDS_Face face = GeometryEngine::face_by_index( + m_doc.bodies[m_sel_solid_body].shape, m_sel_solid_face); + const GeometryEngine::CylinderFace cf = GeometryEngine::cylinder_of_face(face); + if (cf.ok) { + SketchPlane p; // plane on the axis (origin at the base) + p.origin = cf.base; + p.normal = cf.axis; + const Vec3d ref = std::abs(cf.axis.z()) < 0.9 ? Vec3d(0, 0, 1) : Vec3d(1, 0, 0); + p.x_axis = ref.cross(cf.axis).normalized(); + p.y_axis = cf.axis.cross(p.x_axis).normalized(); + m_thread_face_plane = p; + m_thread_on_face = true; + m_thread_face_body = m_sel_solid_body; + if (m_thread_radius) m_thread_radius->SetValue(cf.radius); + if (m_thread_height) m_thread_height->SetValue(cf.height); + if (m_thread_internal) m_thread_internal->SetValue(cf.internal); + if (m_thread_x) m_thread_x->SetValue(0.0); // on the axis + if (m_thread_y) m_thread_y->SetValue(0.0); + } else if (m_sel_solid_face >= 0) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Pick a cylindrical face (hole bore or cylinder) for a thread")); + m_status->Refresh(); + } + } + open_tool(Tool::Thread); + }}, + }); + add_sep(m_tb_feature); + // Text / SVG insert tools live in the SKETCH toolbar (they produce 2D profiles = + // sketches), not here. STEP stays in Features: it imports a whole B-rep solid. + // Import STEP — standalone: a STEP comes in as a whole editable B-rep body, not a profile. + auto* b_step = icon_btn("design_step", _L("Import STEP (editable B-rep solid)")); + b_step->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_import_step(); }); + fadd(b_step); + add_sep(m_tb_feature); + auto* b_constrain = icon_btn("design_constrain", _L("Constrain selected sketch")); + b_constrain->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + on_begin_constrain(); + if (m_viewport && (m_viewport->is_constraining() || m_viewport->is_constraining_entities())) + set_ui_mode(UiMode::Constrain); + }); + fadd(b_constrain); + } + + // --- Sketch group: plane + entity tools + Construction + Finish + m_tb_sketch = new wxBoxSizer(wxHORIZONTAL); + auto sadd = [this](wxWindow* w) { m_tb_sketch->Add(w, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 2); }; + m_tb_sketch->Add(caption(_L("SKETCH")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 8); + { + // The plane/orientation choice lives in the docked Sketch card (Phase 3), + // not in the toolbar; the toolbar carries only the drawing tools. + auto skbtn = [&](const char* icon, DesignSketchTool::Mode mode, + const wxString& tip, const wxString& hint) { + auto* b = icon_btn(icon, tip); + b->Bind(wxEVT_BUTTON, [select_tool, mode, hint](wxCommandEvent&) { select_tool(mode, hint); }); + sadd(b); + }; + // Onshape-style family flyout, rendered with Orca's themed DropDown + // (white/teal selector, #DBDBDB border, HarmonyOS Body_14) — same widget + // as the settings combo dropdowns. The button shows the current variant's + // icon; clicking drops the variants; a small chevron marks it as a group. + struct SkVar { const char* icon; DesignSketchTool::Mode mode; wxString tip; wxString hint; }; + struct ToolFlyout { + std::vector texts, tips; + std::vector icons; + std::vector modes; + std::vector hints; + std::vector icon_names; + ScalableButton* btn = nullptr; + DropDown drop; // declared LAST: destroyed before the vectors it references + ToolFlyout() : drop(texts, tips, icons) {} + }; + auto dropdown = [&](const char* def_icon, const wxString& grp, std::vector vars) { + auto* b = icon_btn(def_icon, grp); + // messureSize() measures labels with the PARENT's font (this button) but the + // popup draws them in Body_14 — so an under-sized button font truncates rows. + // The button is icon-only (no label), so giving it Body_14 is invisible and + // makes the content-width measure match the draw. + b->SetFont(Label::Body_14); + auto fo = std::make_shared(); + for (auto& v : vars) { + fo->texts.push_back(v.tip); + fo->tips.push_back(v.hint); + fo->icons.push_back(tint(create_scaled_bitmap(v.icon, m_form, 18), drop_icon_col)); + fo->modes.push_back(v.mode); + fo->hints.push_back(v.hint); + fo->icon_names.emplace_back(v.icon); + } + fo->btn = b; + fo->drop.Create(b); + fo->drop.SetUseContentWidth(true, false); + fo->drop.Invalidate(true); + ToolFlyout* fp = fo.get(); + fo->drop.Bind(wxEVT_COMBOBOX, [this, fp, select_tool](wxCommandEvent& e) { + int i = e.GetInt(); + if (i >= 0 && i < (int) fp->modes.size()) { + fp->btn->SetBitmap_(fp->icon_names[i]); + select_tool(fp->modes[i], fp->hints[i]); + set_active_tool_btn(fp->btn); + } + }); + b->Bind(wxEVT_BUTTON, [b, fp](wxCommandEvent&) { + // autoPosition()/messureSize() are private; ComboBox calls them before + // Popup() so the window is sized to its content first. Without that the + // popup maps at a stale narrow size and labels ellipsize ("Oblique + // rectang…"). Force a fresh content measure by toggling use_content_width + // (messureSize only runs when the flag actually changes), then show. + fp->drop.Invalidate(true); + fp->drop.SetUseContentWidth(false, false); + fp->drop.SetUseContentWidth(true, false); + wxPoint pos = b->ClientToScreen(wxPoint(0, -6)); + fp->drop.Position(pos, wxSize(0, b->GetSize().y + 12)); + fp->drop.Popup(); + }); + m_flyout_keepalive.push_back(fo); + sadd(b); + auto* chev = new wxStaticText(m_toolbar, wxID_ANY, wxString::FromUTF8("\xE2\x96\xBE")); + chev->SetForegroundColour(dp_sec_text()); + chev->SetFont(Label::Body_9); + m_tb_sketch->Add(chev, 0, wxALIGN_BOTTOM | wxBOTTOM | wxRIGHT, 5); + return b; + }; + skbtn("design_select", DesignSketchTool::Mode::Select, _L("Select"), + _L("Click to select; Shift to add; double-click for a whole loop")); + skbtn("design_dimension", DesignSketchTool::Mode::Dimension, _L("Dimension"), + _L("Click 2 points or a line / circle / arc to place a dimension")); + add_sep(m_tb_sketch); + dropdown("design_line", _L("Line / polyline"), { + {"design_line", DesignSketchTool::Mode::Line, _L("Line"), _L("Click start, then end — then set the exact length")}, + {"design_polyline", DesignSketchTool::Mode::Polyline, _L("Polyline"), _L("Click points; click first / right-click to close the loop")} }); + dropdown("design_rect", _L("Rectangle"), { + {"design_rect", DesignSketchTool::Mode::CornerRect, _L("Corner rectangle"), _L("Click two opposite corners")}, + {"design_crect", DesignSketchTool::Mode::CenterRect, _L("Center rectangle"), _L("Click center, then a corner")}, + {"design_rect_oblique", DesignSketchTool::Mode::ObliqueRect, _L("Oblique rectangle"), _L("Click two corners of one edge, then a point for the width")}, + {"design_rect_rounded", DesignSketchTool::Mode::RoundedRect, _L("Rounded rectangle"), _L("Click two opposite corners, then a point for the corner radius")} }); + dropdown("design_circle", _L("Circle"), { + {"design_circle", DesignSketchTool::Mode::CenterCircle, _L("Center circle"), _L("Click center, then radius")}, + {"design_circle2pt", DesignSketchTool::Mode::TwoPointCircle, _L("2-point circle"), _L("Click two ends of the diameter")}, + {"design_circle3pt", DesignSketchTool::Mode::ThreePointCircle, _L("3-point circle"), _L("Click three points on the circle")} }); + dropdown("design_arc3pt", _L("Arc"), { + {"design_arc3pt", DesignSketchTool::Mode::ThreePointArc, _L("3-point arc"), _L("Click start, end, then a point on the arc")}, + {"design_tangentarc", DesignSketchTool::Mode::TangentArc, _L("Tangent arc"), _L("Click start (on the last entity) then end")}, + {"design_arc_center", DesignSketchTool::Mode::CenterArc, _L("Center-point arc"), _L("Click center, then start, then a point for the end angle")} }); + dropdown("design_slot", _L("Slot"), { + {"design_slot", DesignSketchTool::Mode::Slot, _L("Slot"), _L("Click two centerline ends, then a point for width")}, + {"design_slot_arc", DesignSketchTool::Mode::ArcSlot, _L("Arc slot"), _L("Click center, start, end, then a point for the width")} }); + dropdown("design_ellipse", _L("Ellipse"), { + {"design_ellipse", DesignSketchTool::Mode::Ellipse, _L("Ellipse"), _L("Click center, a major-axis end, then a point for the minor axis")}, + {"design_ellipse_arc", DesignSketchTool::Mode::EllipseArc, _L("Elliptical arc"), _L("Click center, major-axis end, minor point, then arc start and end")} }); + skbtn("design_bspline", DesignSketchTool::Mode::BSpline, _L("Spline"), + _L("Click control points; double-click or right-click to finish")); + skbtn("design_point", DesignSketchTool::Mode::Point, _L("Point"), + _L("Click to place a point")); + add_sep(m_tb_sketch); + // Insert tools — Text / SVG produce a 2D profile (a sketch), so they belong with + // the sketch tools, not in the generic Features strip. Each places the art + // in-canvas, then commits via the Insert card's Confirm. + { + auto* b_text = icon_btn("design_text", _L("Text — emboss text as a profile")); + b_text->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_add_text(); }); + sadd(b_text); + auto* b_svg = icon_btn("design_svg", _L("SVG — import an outline as a profile")); + b_svg->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_import_svg(); }); + sadd(b_svg); + } + add_sep(m_tb_sketch); + // In-canvas edit-op tools (drag gizmo / click label), grouped by family. + dropdown("design_filletedge", _L("Fillet / chamfer"), { + {"design_filletedge", DesignSketchTool::Mode::Fillet, _L("Fillet"), _L("Pick two lines, then drag the arrow or click the radius to set it")}, + {"design_chamfer", DesignSketchTool::Mode::Chamfer, _L("Chamfer"), _L("Pick two lines, then drag the arrow or click the distance to set it")} }); + skbtn("design_offset", DesignSketchTool::Mode::Offset, _L("Offset"), + _L("Pick an entity, then drag the arrow or click the distance; click empty to apply")); + skbtn("design_mirror", DesignSketchTool::Mode::Mirror, _L("Mirror"), + _L("Pick a mirror-axis line, then the entities to mirror; click empty to apply")); + // Trim / Extend scissors — standalone sketch tools (NOT inside Constrain): click a + // segment to cut it back to / out to its nearest intersection. One cut per click. + skbtn("design_trim", DesignSketchTool::Mode::Trim, _L("Trim"), + _L("Click a segment to trim it back to its nearest intersection; right-click exits")); + skbtn("design_extend", DesignSketchTool::Mode::Extend, _L("Extend"), + _L("Click a line or arc to extend it to the nearest entity; right-click exits")); + dropdown("design_move", _L("Move / rotate / scale"), { + {"design_move", DesignSketchTool::Mode::Move, _L("Move (translate)"), _L("Pick entities, then drag the handle or click the distance; click empty to apply")}, + {"design_rotate", DesignSketchTool::Mode::Rotate, _L("Rotate (about centroid)"), _L("Pick entities, then drag around the pivot or click the angle; click empty to apply")}, + {"design_scale", DesignSketchTool::Mode::Scale, _L("Scale (about centroid)"), _L("Pick entities, then drag the handle or click the factor; click empty to apply")} }); + dropdown("design_array", _L("Linear / polar array"), { + {"design_array", DesignSketchTool::Mode::Array, _L("Linear array"), _L("Pick entities, drag the spacing handle, click the count; click empty to apply")}, + {"design_polararray", DesignSketchTool::Mode::PolarArray, _L("Polar array (about centroid)"), _L("Pick entities, drag the sweep handle, click the count; click empty to apply")} }); + + m_sides = new wxSpinCtrl(m_toolbar, wxID_ANY, "6", wxDefaultPosition, wxSize(50, -1)); + m_sides->SetRange(3, 64); + m_sides->SetValue(6); + auto* b_poly = icon_btn("design_polygon", _L("Polygon")); + b_poly->Bind(wxEVT_BUTTON, [this, select_tool](wxCommandEvent&) { + if (m_viewport) { + m_viewport->set_sketch_polygon_sides(m_sides->GetValue()); + m_viewport->set_sketch_polygon_circumscribed(m_poly_circ && m_poly_circ->GetValue()); + } + select_tool(DesignSketchTool::Mode::Polygon, _L("Click center then a vertex")); }); + m_sides->Bind(wxEVT_SPINCTRL, [this](wxSpinEvent&) { + if (m_viewport) m_viewport->set_sketch_polygon_sides(m_sides->GetValue()); }); + sadd(b_poly); + sadd(m_sides); + m_poly_circ = new wxCheckBox(m_toolbar, wxID_ANY, _L("Circumscribed")); + m_poly_circ->SetForegroundColour(dp_ctl_text()); + m_poly_circ->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { + if (m_viewport) m_viewport->set_sketch_polygon_circumscribed(m_poly_circ->GetValue()); }); + sadd(m_poly_circ); + add_sep(m_tb_sketch); + // Constrain — reachable mid-sketch: commits the live sketch in place and drops into + // Constrain mode, where the geometric/dimensional palette + Trim + Extend (scissors) + // are picked and applied. (Trim/Extend are pick-then-apply, so they live there, not as + // bare toolbar buttons.) + auto* b_constrain_sk = icon_btn("design_constrain", + _L("Constrain — add geometric/dimensional relations; Trim/Extend live here")); + b_constrain_sk->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { enter_constrain_inline(); }); + sadd(b_constrain_sk); + add_sep(m_tb_sketch); + m_construction->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { + if (m_viewport && m_viewport->is_sketching()) + m_viewport->set_sketch_construction(m_construction->GetValue()); }); + sadd(m_construction); + add_sep(m_tb_sketch); + auto* b_del = icon_btn("design_delete", _L("Delete selected")); + b_del->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + if (m_viewport) m_viewport->delete_selected_sketch_entities(); }); + sadd(b_del); + // Finish sketch = the unified ✓ Confirm in the action bar (tool_confirm). + } + + // --- Constrain group: geometric constraints + dimensions + edit ops + Done + m_tb_constrain = new wxBoxSizer(wxHORIZONTAL); + auto cadd = [this](wxWindow* w) { m_tb_constrain->Add(w, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 2); }; + m_tb_constrain->Add(caption(_L("CONSTRAIN")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 8); + { + auto cbtn = [&](const char* icon, const wxString& tip, SketchConstraintType type) { + auto* b = icon_btn(icon, tip); + b->Bind(wxEVT_BUTTON, [this, type](wxCommandEvent&) { apply_constraint(type); }); + cadd(b); + }; + cbtn("design_c_horizontal", _L("Horizontal"), SketchConstraintType::Horizontal); + cbtn("design_c_vertical", _L("Vertical"), SketchConstraintType::Vertical); + cbtn("design_c_parallel", _L("Parallel"), SketchConstraintType::Parallel); + cbtn("design_c_perpendicular", _L("Perpendicular"), SketchConstraintType::Perpendicular); + cbtn("design_c_coincident", _L("Coincident"), SketchConstraintType::Coincident); + cbtn("design_c_equal", _L("Equal length"), SketchConstraintType::EqualLength); + cbtn("design_c_concentric", _L("Concentric"), SketchConstraintType::Concentric); + cbtn("design_c_tangent", _L("Tangent"), SketchConstraintType::Tangent); + cbtn("design_c_midpoint", _L("Midpoint"), SketchConstraintType::Midpoint); + cbtn("design_c_symmetric", _L("Symmetric"), SketchConstraintType::Symmetric); + cbtn("design_c_angle", _L("Angle"), SketchConstraintType::Angle); + cbtn("design_c_radius", _L("Radius"), SketchConstraintType::Radius); + cbtn("design_c_diameter", _L("Diameter"), SketchConstraintType::Diameter); + cbtn("design_c_fix", _L("Fix point (anchor in place)"), SketchConstraintType::Fix); + // Trim/Extend are now standalone SKETCH scissors (Mode::Trim/Extend) in the sketch + // toolbar, NOT Constrain buttons. The other edit ops (Mirror/Offset/Fillet/Chamfer/ + // Move/…) are first-class sketch tools too. Done constraining = the action-bar ✓. + } + + // Unified action bar: the ONE Confirm/Cancel surface for every tool and mode. Lives at + // the right end of the ribbon (the "tool dashboard"); shown only while a tool/mode is + // active (update_action_bar). Replaces the 13 per-card buttons + sketch Finish + Done. + m_tb_action = new wxBoxSizer(wxHORIZONTAL); + { + auto* ok = new wxButton(m_toolbar, wxID_ANY, _L("✓ Confirm")); + ok->SetForegroundColour(*wxWHITE); + ok->SetBackgroundColour(wxColour(0x00, 0x96, 0x88)); // Orca teal accent + ok->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { tool_confirm(); }); + m_confirm_btns.push_back(ok); // refresh_preview greys this on an invalid candidate + auto* no = new wxButton(m_toolbar, wxID_ANY, _L("✗ Cancel")); + no->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { tool_cancel(); }); + m_tb_action->Add(ok, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6); + m_tb_action->Add(no, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 10); + } + + // Persistent Undo/Redo group: always visible (not mode-gated like the tool groups), so + // history is reachable from Feature, Sketch and Constrain alike. These are momentary + // actions, so — unlike icon_btn — they are NOT registered in m_tool_btns and never take + // the teal active-tool highlight. They route to the SAME do_undo_redo as the keyboard + // Ctrl+Z / Ctrl+Shift+Z path, and are greyed by update_undo_redo_buttons(). + m_tb_history = new wxBoxSizer(wxHORIZONTAL); + { + auto hist_btn = [this, tb_bg, tb_hover](const char* icon, const wxString& tip) { + auto* b = new ScalableButton(m_toolbar, wxID_ANY, icon, "", wxSize(52, 52), + wxDefaultPosition, wxBU_EXACTFIT | wxBORDER_NONE, false, 42); + b->SetToolTip(tip); + b->SetBackgroundColour(tb_bg); + b->Bind(wxEVT_ENTER_WINDOW, [b, tb_hover](wxMouseEvent& e) { + if (b->IsEnabled()) { b->SetBackgroundColour(tb_hover); b->Refresh(); } e.Skip(); }); + b->Bind(wxEVT_LEAVE_WINDOW, [b, tb_bg](wxMouseEvent& e) { + b->SetBackgroundColour(tb_bg); b->Refresh(); e.Skip(); }); + return b; + }; + m_btn_undo = hist_btn("menu_undo", _L("Undo (Ctrl+Z)")); + m_btn_undo->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { do_undo_redo(false); }); + m_btn_redo = hist_btn("menu_redo", _L("Redo (Ctrl+Shift+Z)")); + m_btn_redo->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { do_undo_redo(true); }); + m_btn_undo->Enable(false); // nothing to undo/redo on a fresh document + m_btn_redo->Enable(false); + m_tb_history->Add(m_btn_undo, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 2); + m_tb_history->Add(m_btn_redo, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 2); + } + + auto* tbrow = new wxBoxSizer(wxHORIZONTAL); + tbrow->AddSpacer(8); + tbrow->Add(m_tb_history, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 5); + add_sep(tbrow); + tbrow->Add(m_tb_feature, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 5); + tbrow->Add(m_tb_sketch, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 5); + tbrow->Add(m_tb_constrain, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 5); + tbrow->AddStretchSpacer(); + tbrow->Add(m_tb_action, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 5); + m_toolbar->SetSizer(tbrow); + + // Onshape-style dialog-card header: feature icon + bold title. out receives + // the title control so open_tool() can retitle it per feature. + auto card_header = [this](const char* icon, const wxString& title, wxStaticText*& out) -> wxSizer* { + auto* h = new wxBoxSizer(wxHORIZONTAL); + auto* ic = new wxStaticBitmap(m_form, wxID_ANY, create_scaled_bitmap(icon, m_form, 18)); + out = new wxStaticText(m_form, wxID_ANY, title); + out->SetFont(Label::Head_14); // Orca shared HarmonyOS card-title font + h->Add(ic, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 8); + h->Add(out, 0, wxALIGN_CENTER_VERTICAL); + return h; + }; + + // --- Sketch dialog (shape definition only — no distance/mode) --- + auto* form = new wxFlexGridSizer(2, 6, 8); + + m_shape = new wxChoice(m_form, wxID_ANY); + m_shape->Append("Rectangle"); + m_shape->Append("Circle"); + m_shape->SetSelection(0); + form->Add(new wxStaticText(m_form, wxID_ANY, _L("Shape")), 0, wxALIGN_CENTER_VERTICAL); + form->Add(m_shape); + + m_plane = new wxChoice(m_form, wxID_ANY); + m_plane->Append("XY"); + m_plane->Append("XZ"); + m_plane->Append("YZ"); + m_plane->SetSelection(0); + form->Add(new wxStaticText(m_form, wxID_ANY, _L("Plane")), 0, wxALIGN_CENTER_VERTICAL); + form->Add(m_plane); + + m_width = make_spin(m_form, 20); + form->Add(new wxStaticText(m_form, wxID_ANY, _L("Width / X")), 0, wxALIGN_CENTER_VERTICAL); + form->Add(m_width); + + m_height = make_spin(m_form, 20); + form->Add(new wxStaticText(m_form, wxID_ANY, _L("Height / Y")), 0, wxALIGN_CENTER_VERTICAL); + form->Add(m_height); + + m_radius = make_spin(m_form, 10); + form->Add(new wxStaticText(m_form, wxID_ANY, _L("Radius")), 0, wxALIGN_CENTER_VERTICAL); + form->Add(m_radius); + + m_box_sketch = new wxBoxSizer(wxVERTICAL); + m_box_sketch->Add(card_header("design_sketch", _L("Sketch"), m_hdr_sketch), 0, wxLEFT | wxRIGHT | wxTOP, 12); + m_box_sketch->Add(new wxStaticLine(m_form), 0, wxEXPAND | wxALL, 8); + m_box_sketch->Add(form, 0, wxALL, 12); + root->Add(m_box_sketch, 0, wxEXPAND); + + // --- Extrude dialog (consumes the selected sketch) --- + m_box_extrude = new wxBoxSizer(wxVERTICAL); + m_box_extrude->Add(card_header("design_extrude", _L("Extrude"), m_hdr_extrude), 0, wxLEFT | wxRIGHT | wxTOP, 12); + m_box_extrude->Add(new wxStaticLine(m_form), 0, wxEXPAND | wxALL, 8); + m_extrude_sketch_label = new wxStaticText(m_form, wxID_ANY, _L("Sketch: —")); + m_box_extrude->Add(m_extrude_sketch_label, 0, wxLEFT | wxRIGHT | wxTOP, 12); + { + auto* eform = new wxFlexGridSizer(2, 6, 8); + + m_distance = make_spin(m_form, 10); + eform->Add(new wxStaticText(m_form, wxID_ANY, _L("Extrude dist")), 0, wxALIGN_CENTER_VERTICAL); + eform->Add(m_distance); + + // End condition (order MUST match ExtrudeEnd: Blind/Symmetric/TwoSided/ThroughAll/ + // UpToFace/UpToVertex). Up-to-face uses the currently click-selected solid face. + m_extrude_end = new wxChoice(m_form, wxID_ANY); + for (const char* s : { "Blind", "Symmetric", "Two-sided", "Through all", + "Up to face", "Up to vertex" }) + m_extrude_end->Append(s); + m_extrude_end->SetSelection(0); + eform->Add(new wxStaticText(m_form, wxID_ANY, _L("End")), 0, wxALIGN_CENTER_VERTICAL); + eform->Add(m_extrude_end); + + m_distance2 = make_spin(m_form, 5, 0.0, 100000.0); // second-side depth (Two-sided) + eform->Add(new wxStaticText(m_form, wxID_ANY, _L("2nd dist")), 0, wxALIGN_CENTER_VERTICAL); + eform->Add(m_distance2); + + m_taper = make_spin(m_form, 0.0, -89.0, 89.0); // draft angle (deg) + eform->Add(new wxStaticText(m_form, wxID_ANY, _L("Taper °")), 0, wxALIGN_CENTER_VERTICAL); + eform->Add(m_taper); + + m_mode = new wxChoice(m_form, wxID_ANY); + // Order is load-bearing: index maps to BooleanMode (New=0, Add=1, Cut=2, Intersect=3). + // Labels use Onshape wording so the choice reads as the user thinks of it. + m_mode->Append(_L("New body")); // separate coexisting solid + m_mode->Append(_L("Join")); // fuse into the target body (was "Add") + m_mode->Append(_L("Cut")); // subtract from the target body + m_mode->Append(_L("Intersect")); // keep only the overlap + m_mode->SetSelection(0); + eform->Add(new wxStaticText(m_form, wxID_ANY, _L("Result")), 0, wxALIGN_CENTER_VERTICAL); + eform->Add(m_mode); + + m_flip = new wxCheckBox(m_form, wxID_ANY, _L("Flip direction")); + eform->Add(new wxStaticText(m_form, wxID_ANY, wxEmptyString)); + eform->Add(m_flip); + + m_box_extrude->Add(eform, 0, wxLEFT | wxRIGHT | wxTOP, 12); + } + root->Add(m_box_extrude, 0, wxEXPAND); + + // --- Dress-up (Fillet / Chamfer) --- + auto* dform = new wxFlexGridSizer(2, 6, 8); + + m_dressup_type = new wxChoice(m_form, wxID_ANY); + m_dressup_type->Append("Fillet"); + m_dressup_type->Append("Chamfer"); + m_dressup_type->SetSelection(0); + dform->Add(new wxStaticText(m_form, wxID_ANY, _L("Dress-up")), 0, wxALIGN_CENTER_VERTICAL); + dform->Add(m_dressup_type); + + m_face_group = new wxChoice(m_form, wxID_ANY); + m_face_group->Append("Top"); // index 0 -> FaceGroup::Top + m_face_group->Append("Bottom"); // 1 -> Bottom + m_face_group->Append("Lateral"); // 2 -> Lateral + m_face_group->Append("All"); // 3 -> All + m_face_group->SetSelection(3); + dform->Add(new wxStaticText(m_form, wxID_ANY, _L("Edges")), 0, wxALIGN_CENTER_VERTICAL); + dform->Add(m_face_group); + + m_dressup_size = make_spin(m_form, 2.0); + dform->Add(new wxStaticText(m_form, wxID_ANY, _L("Size (r/dist)")), 0, wxALIGN_CENTER_VERTICAL); + dform->Add(m_dressup_size); + + m_box_dressup = new wxBoxSizer(wxVERTICAL); + m_box_dressup->Add(card_header("design_dressup", _L("Fillet / Chamfer"), m_hdr_dressup), 0, wxLEFT | wxRIGHT | wxTOP, 12); + m_box_dressup->Add(new wxStaticLine(m_form), 0, wxEXPAND | wxALL, 8); + m_box_dressup->Add(dform, 0, wxLEFT | wxRIGHT | wxTOP, 12); + root->Add(m_box_dressup, 0, wxEXPAND); + + // --- Hole (positioned circular cut) --- + auto* hform = new wxFlexGridSizer(2, 6, 8); + + m_hole_plane = new wxChoice(m_form, wxID_ANY); + m_hole_plane->Append("XY"); + m_hole_plane->Append("XZ"); + m_hole_plane->Append("YZ"); + m_hole_plane->SetSelection(0); + // Picking a plane here is an explicit choice: drop any on-face hijack (a stale face pick + // could keep m_hole_on_face true, so the dropdown was ignored and the hole drilled on the + // face's plane instead of the chosen XY/XZ/YZ). + m_hole_plane->Bind(wxEVT_CHOICE, [this](wxCommandEvent& e) { + m_hole_on_face = false; + m_hole_has_bounds = false; + update_hole_gizmo(); + refresh_preview(); + e.Skip(); + }); + hform->Add(new wxStaticText(m_form, wxID_ANY, _L("Hole plane")), 0, wxALIGN_CENTER_VERTICAL); + hform->Add(m_hole_plane); + + m_hole_diameter = make_spin(m_form, 6.0); + hform->Add(new wxStaticText(m_form, wxID_ANY, _L("Diameter")), 0, wxALIGN_CENTER_VERTICAL); + hform->Add(m_hole_diameter); + + m_hole_depth = make_spin(m_form, 10.0); + hform->Add(new wxStaticText(m_form, wxID_ANY, _L("Depth (blind)")), 0, wxALIGN_CENTER_VERTICAL); + hform->Add(m_hole_depth); + + m_hole_x = make_spin(m_form, 0.0, -1000.0, 1000.0); + hform->Add(new wxStaticText(m_form, wxID_ANY, _L("Pos X")), 0, wxALIGN_CENTER_VERTICAL); + hform->Add(m_hole_x); + + m_hole_y = make_spin(m_form, 0.0, -1000.0, 1000.0); + hform->Add(new wxStaticText(m_form, wxID_ANY, _L("Pos Y")), 0, wxALIGN_CENTER_VERTICAL); + hform->Add(m_hole_y); + + m_hole_through = new wxCheckBox(m_form, wxID_ANY, _L("Through")); + m_hole_through->SetValue(true); + hform->Add(new wxStaticText(m_form, wxID_ANY, _L("Mode")), 0, wxALIGN_CENTER_VERTICAL); + hform->Add(m_hole_through); + + m_box_hole = new wxBoxSizer(wxVERTICAL); + m_box_hole->Add(card_header("design_hole", _L("Hole"), m_hdr_hole), 0, wxLEFT | wxRIGHT | wxTOP, 12); + m_box_hole->Add(new wxStaticLine(m_form), 0, wxEXPAND | wxALL, 8); + m_box_hole->Add(hform, 0, wxLEFT | wxRIGHT | wxTOP, 12); + root->Add(m_box_hole, 0, wxEXPAND); + + // --- Thread (helical) --- + auto* tform = new wxFlexGridSizer(2, 6, 8); + + m_thread_plane = new wxChoice(m_form, wxID_ANY); + m_thread_plane->Append("XY"); + m_thread_plane->Append("XZ"); + m_thread_plane->Append("YZ"); + m_thread_plane->SetSelection(0); + tform->Add(new wxStaticText(m_form, wxID_ANY, _L("Thread plane")), 0, wxALIGN_CENTER_VERTICAL); + tform->Add(m_thread_plane); + + // Standard designation picker — fills pitch/depth (and nominal radius) from the + // ISO metric / Unified imperial tables. "Custom" leaves the manual spins alone. + m_thread_std = new wxChoice(m_form, wxID_ANY); + m_thread_std->Append(_L("Custom")); + for (const ThreadSpec& s : thread_standards()) + m_thread_std->Append(s.name); + m_thread_std->SetSelection(0); + m_thread_std->Bind(wxEVT_CHOICE, [this](wxCommandEvent&) { apply_thread_standard(); }); + tform->Add(new wxStaticText(m_form, wxID_ANY, _L("Standard")), 0, wxALIGN_CENTER_VERTICAL); + tform->Add(m_thread_std); + + m_thread_radius = make_spin(m_form, 5.0); + tform->Add(new wxStaticText(m_form, wxID_ANY, _L("Radius")), 0, wxALIGN_CENTER_VERTICAL); + tform->Add(m_thread_radius); + + m_thread_pitch = make_spin(m_form, 2.0); + tform->Add(new wxStaticText(m_form, wxID_ANY, _L("Pitch")), 0, wxALIGN_CENTER_VERTICAL); + tform->Add(m_thread_pitch); + + m_thread_height = make_spin(m_form, 10.0); + tform->Add(new wxStaticText(m_form, wxID_ANY, _L("Length")), 0, wxALIGN_CENTER_VERTICAL); + tform->Add(m_thread_height); + + m_thread_depth = make_spin(m_form, 1.0); + tform->Add(new wxStaticText(m_form, wxID_ANY, _L("Thread depth")), 0, wxALIGN_CENTER_VERTICAL); + tform->Add(m_thread_depth); + + m_thread_x = make_spin(m_form, 0.0, -1000.0, 1000.0); + tform->Add(new wxStaticText(m_form, wxID_ANY, _L("Pos X")), 0, wxALIGN_CENTER_VERTICAL); + tform->Add(m_thread_x); + + m_thread_y = make_spin(m_form, 0.0, -1000.0, 1000.0); + tform->Add(new wxStaticText(m_form, wxID_ANY, _L("Pos Y")), 0, wxALIGN_CENTER_VERTICAL); + tform->Add(m_thread_y); + + m_thread_internal = new wxCheckBox(m_form, wxID_ANY, _L("Internal (tapped bore)")); + m_thread_internal->SetValue(false); + // External rod uses the major radius; an internal tapped bore uses the minor + // (tap-drill) radius — re-derive the nominal radius when the role flips. + m_thread_internal->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& e) { + apply_thread_standard(); + e.Skip(); + }); + tform->Add(new wxStaticText(m_form, wxID_ANY, _L("Internal")), 0, wxALIGN_CENTER_VERTICAL); + tform->Add(m_thread_internal); + + m_box_thread = new wxBoxSizer(wxVERTICAL); + m_box_thread->Add(card_header("design_thread", _L("Thread"), m_hdr_thread), 0, wxLEFT | wxRIGHT | wxTOP, 12); + m_box_thread->Add(new wxStaticLine(m_form), 0, wxEXPAND | wxALL, 8); + m_box_thread->Add(tform, 0, wxLEFT | wxRIGHT | wxTOP, 12); + root->Add(m_box_thread, 0, wxEXPAND); + + // --- Revolve (sweep a sketch profile about an in-plane axis) --- + m_box_revolve = new wxBoxSizer(wxVERTICAL); + m_box_revolve->Add(card_header("design_extrude", _L("Revolve"), m_hdr_revolve), 0, wxLEFT | wxRIGHT | wxTOP, 12); + m_box_revolve->Add(new wxStaticLine(m_form), 0, wxEXPAND | wxALL, 8); + m_revolve_sketch_label = new wxStaticText(m_form, wxID_ANY, _L("Sketch: —")); + m_box_revolve->Add(m_revolve_sketch_label, 0, wxLEFT | wxRIGHT | wxTOP, 12); + { + auto* rform = new wxFlexGridSizer(2, 6, 8); + + m_revolve_angle = make_spin(m_form, 360.0, 1.0, 360.0); + rform->Add(new wxStaticText(m_form, wxID_ANY, _L("Angle °")), 0, wxALIGN_CENTER_VERTICAL); + rform->Add(m_revolve_angle); + + m_revolve_axis = new wxChoice(m_form, wxID_ANY); + m_revolve_axis->Append("Plane X"); + m_revolve_axis->Append("Plane Y"); + m_revolve_axis->SetSelection(0); + rform->Add(new wxStaticText(m_form, wxID_ANY, _L("Axis")), 0, wxALIGN_CENTER_VERTICAL); + rform->Add(m_revolve_axis); + + m_revolve_mode = new wxChoice(m_form, wxID_ANY); + m_revolve_mode->Append("New"); + m_revolve_mode->Append("Add"); + m_revolve_mode->Append("Cut"); + m_revolve_mode->Append("Intersect"); + m_revolve_mode->SetSelection(0); + rform->Add(new wxStaticText(m_form, wxID_ANY, _L("Mode")), 0, wxALIGN_CENTER_VERTICAL); + rform->Add(m_revolve_mode); + + m_revolve_flip = new wxCheckBox(m_form, wxID_ANY, _L("Flip direction")); + rform->Add(new wxStaticText(m_form, wxID_ANY, wxEmptyString)); + rform->Add(m_revolve_flip); + + m_box_revolve->Add(rform, 0, wxLEFT | wxRIGHT | wxTOP, 12); + } + root->Add(m_box_revolve, 0, wxEXPAND); + + // --- Sweep (sweep a profile sketch along a path sketch) --- + m_box_sweep = new wxBoxSizer(wxVERTICAL); + m_box_sweep->Add(card_header("design_extrude", _L("Sweep"), m_hdr_sweep), 0, wxLEFT | wxRIGHT | wxTOP, 12); + m_box_sweep->Add(new wxStaticLine(m_form), 0, wxEXPAND | wxALL, 8); + m_sweep_profile_label = new wxStaticText(m_form, wxID_ANY, _L("Profile: —")); + m_box_sweep->Add(m_sweep_profile_label, 0, wxLEFT | wxRIGHT | wxTOP, 12); + { + auto* sform = new wxFlexGridSizer(2, 6, 8); + + m_sweep_path = new wxChoice(m_form, wxID_ANY); + sform->Add(new wxStaticText(m_form, wxID_ANY, _L("Path")), 0, wxALIGN_CENTER_VERTICAL); + sform->Add(m_sweep_path); + + m_sweep_mode = new wxChoice(m_form, wxID_ANY); + m_sweep_mode->Append("New"); + m_sweep_mode->Append("Add"); + m_sweep_mode->Append("Cut"); + m_sweep_mode->Append("Intersect"); + m_sweep_mode->SetSelection(0); + sform->Add(new wxStaticText(m_form, wxID_ANY, _L("Mode")), 0, wxALIGN_CENTER_VERTICAL); + sform->Add(m_sweep_mode); + + m_box_sweep->Add(sform, 0, wxLEFT | wxRIGHT | wxTOP, 12); + } + root->Add(m_box_sweep, 0, wxEXPAND); + + // --- Pattern (replicate the target body: linear or circular) --- + m_box_pattern = new wxBoxSizer(wxVERTICAL); + m_box_pattern->Add(card_header("design_extrude", _L("Pattern"), m_hdr_pattern), 0, wxLEFT | wxRIGHT | wxTOP, 12); + m_box_pattern->Add(new wxStaticLine(m_form), 0, wxEXPAND | wxALL, 8); + { + auto* pform = new wxFlexGridSizer(2, 6, 8); + + m_pattern_type = new wxChoice(m_form, wxID_ANY); + m_pattern_type->Append("Linear"); + m_pattern_type->Append("Circular"); + m_pattern_type->SetSelection(0); + pform->Add(new wxStaticText(m_form, wxID_ANY, _L("Type")), 0, wxALIGN_CENTER_VERTICAL); + pform->Add(m_pattern_type); + + m_pattern_count = make_spin(m_form, 3, 1, 999); + pform->Add(new wxStaticText(m_form, wxID_ANY, _L("Count")), 0, wxALIGN_CENTER_VERTICAL); + pform->Add(m_pattern_count); + + m_pattern_spacing = make_spin(m_form, 20.0, 0.01, 100000.0); + pform->Add(new wxStaticText(m_form, wxID_ANY, _L("Spacing")), 0, wxALIGN_CENTER_VERTICAL); + pform->Add(m_pattern_spacing); + + m_pattern_dir = new wxChoice(m_form, wxID_ANY); + m_pattern_dir->Append("Plane X"); + m_pattern_dir->Append("Plane Y"); + m_pattern_dir->SetSelection(0); + pform->Add(new wxStaticText(m_form, wxID_ANY, _L("Direction")), 0, wxALIGN_CENTER_VERTICAL); + pform->Add(m_pattern_dir); + + m_pattern_angle = make_spin(m_form, 360.0, 1.0, 360.0); + pform->Add(new wxStaticText(m_form, wxID_ANY, _L("Total angle°")), 0, wxALIGN_CENTER_VERTICAL); + pform->Add(m_pattern_angle); + + m_box_pattern->Add(pform, 0, wxLEFT | wxRIGHT | wxTOP, 12); + } + root->Add(m_box_pattern, 0, wxEXPAND); + + // --- Boolean (combine two existing bodies: union / subtract / intersect) --- + m_box_boolean = new wxBoxSizer(wxVERTICAL); + m_box_boolean->Add(card_header("design_boolean", _L("Boolean"), m_hdr_boolean), 0, wxLEFT | wxRIGHT | wxTOP, 12); + m_box_boolean->Add(new wxStaticLine(m_form), 0, wxEXPAND | wxALL, 8); + { + auto* bform = new wxFlexGridSizer(2, 6, 8); + + m_bool_op = new wxChoice(m_form, wxID_ANY); + m_bool_op->Append("Union (join)"); + m_bool_op->Append("Subtract (cut)"); + m_bool_op->Append("Intersect"); + m_bool_op->SetSelection(0); + m_bool_op->Bind(wxEVT_CHOICE, [this](wxCommandEvent&) { refresh_preview(); }); + bform->Add(new wxStaticText(m_form, wxID_ANY, _L("Operation")), 0, wxALIGN_CENTER_VERTICAL); + bform->Add(m_bool_op); + + m_bool_target = new wxChoice(m_form, wxID_ANY); + m_bool_target->Bind(wxEVT_CHOICE, [this](wxCommandEvent&) { refresh_preview(); }); + bform->Add(new wxStaticText(m_form, wxID_ANY, _L("Target (kept)")), 0, wxALIGN_CENTER_VERTICAL); + bform->Add(m_bool_target); + + m_bool_tool = new wxChoice(m_form, wxID_ANY); + m_bool_tool->Bind(wxEVT_CHOICE, [this](wxCommandEvent&) { refresh_preview(); }); + bform->Add(new wxStaticText(m_form, wxID_ANY, _L("Tool")), 0, wxALIGN_CENTER_VERTICAL); + bform->Add(m_bool_tool); + + // Fuzzy tolerance (mm): the main use is a tool body cutting a destination — a small + // tolerance lets near-coincident mating faces resolve into a clean cut instead of a + // failed boolean or sliver faces. 0 = exact. + m_bool_tol = make_spin(m_form, 0.0, 0.0, 100.0); + m_bool_tol->Bind(wxEVT_SPINCTRLDOUBLE, [this](wxSpinDoubleEvent&) { refresh_preview(); }); + bform->Add(new wxStaticText(m_form, wxID_ANY, _L("Tolerance")), 0, wxALIGN_CENTER_VERTICAL); + bform->Add(m_bool_tol); + + m_box_boolean->Add(bform, 0, wxLEFT | wxRIGHT | wxTOP, 12); + + m_bool_keep = new wxCheckBox(m_form, wxID_ANY, _L("Keep tool body")); + m_bool_keep->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { refresh_preview(); }); + m_box_boolean->Add(m_bool_keep, 0, wxLEFT | wxRIGHT | wxTOP, 12); + } + root->Add(m_box_boolean, 0, wxEXPAND); + + // --- Cut (split a body with a plane): parameters only; ✓/✗ live on the ribbon --- + m_box_cut = new wxBoxSizer(wxVERTICAL); + m_box_cut->Add(card_header("design_cut", _L("Cut"), m_hdr_cut), 0, wxLEFT | wxRIGHT | wxTOP, 12); + m_box_cut->Add(new wxStaticLine(m_form), 0, wxEXPAND | wxALL, 8); + { + auto* cform = new wxFlexGridSizer(2, 6, 8); + + m_cut_target = new wxChoice(m_form, wxID_ANY); + m_cut_target->Bind(wxEVT_CHOICE, [this](wxCommandEvent&) { refresh_preview(); }); + cform->Add(new wxStaticText(m_form, wxID_ANY, _L("Body")), 0, wxALIGN_CENTER_VERTICAL); + cform->Add(m_cut_target); + + m_cut_plane = new wxChoice(m_form, wxID_ANY); + m_cut_plane->Bind(wxEVT_CHOICE, [this](wxCommandEvent&) { refresh_preview(); }); + cform->Add(new wxStaticText(m_form, wxID_ANY, _L("Plane")), 0, wxALIGN_CENTER_VERTICAL); + cform->Add(m_cut_plane); + + m_cut_offset = make_spin(m_form, 0.0, -10000.0, 10000.0); + m_cut_offset->Bind(wxEVT_SPINCTRLDOUBLE, [this](wxSpinDoubleEvent&) { refresh_preview(); }); + cform->Add(new wxStaticText(m_form, wxID_ANY, _L("Offset")), 0, wxALIGN_CENTER_VERTICAL); + cform->Add(m_cut_offset); + + m_box_cut->Add(cform, 0, wxLEFT | wxRIGHT | wxTOP, 12); + // The cut always leaves BOTH pieces as separate bodies (a non-destructive split); + // delete one from the tree afterwards if you only want a half. + } + root->Add(m_box_cut, 0, wxEXPAND); + + // --- Insert (Text / SVG placement): Confirm/Cancel for the in-canvas art transform --- + m_box_insert = new wxBoxSizer(wxVERTICAL); + m_box_insert->Add(card_header("design_text", _L("Insert"), m_hdr_insert), 0, wxLEFT | wxRIGHT | wxTOP, 12); + m_box_insert->Add(new wxStaticLine(m_form), 0, wxEXPAND | wxALL, 8); + m_box_insert->Add(new wxStaticText(m_form, wxID_ANY, + _L("Drag a corner to size, the centre to move.\nConfirm or Cancel in the toolbar above.")), + 0, wxLEFT | wxRIGHT | wxBOTTOM, 12); + root->Add(m_box_insert, 0, wxEXPAND); + + // --- Plane (datum/reference plane: offset + tilt from a base plane; no solid) --- + m_box_plane = new wxBoxSizer(wxVERTICAL); + m_box_plane->Add(card_header("design_sketch", _L("Plane"), m_hdr_plane), 0, wxLEFT | wxRIGHT | wxTOP, 12); + m_box_plane->Add(new wxStaticLine(m_form), 0, wxEXPAND | wxALL, 8); + { + auto* plform = new wxFlexGridSizer(2, 6, 8); + + m_plane_base = new wxChoice(m_form, wxID_ANY); + populate_plane_choices(m_plane_base); // XY/XZ/YZ + any existing datum planes + plform->Add(new wxStaticText(m_form, wxID_ANY, _L("Base")), 0, wxALIGN_CENTER_VERTICAL); + plform->Add(m_plane_base); + + m_plane_offset = make_spin(m_form, 20.0, -100000.0, 100000.0); + plform->Add(new wxStaticText(m_form, wxID_ANY, _L("Offset")), 0, wxALIGN_CENTER_VERTICAL); + plform->Add(m_plane_offset); + + m_plane_tilt = make_spin(m_form, 0.0, -180.0, 180.0); + plform->Add(new wxStaticText(m_form, wxID_ANY, _L("Tilt°")), 0, wxALIGN_CENTER_VERTICAL); + plform->Add(m_plane_tilt); + + m_plane_tilt_axis = new wxChoice(m_form, wxID_ANY); + m_plane_tilt_axis->Append("Base X"); + m_plane_tilt_axis->Append("Base Y"); + m_plane_tilt_axis->SetSelection(0); + plform->Add(new wxStaticText(m_form, wxID_ANY, _L("Tilt axis")), 0, wxALIGN_CENTER_VERTICAL); + plform->Add(m_plane_tilt_axis); + + m_box_plane->Add(plform, 0, wxLEFT | wxRIGHT | wxTOP, 12); + } + root->Add(m_box_plane, 0, wxEXPAND); + + // --- Loft (skin a solid through 2+ ordered profile sketches) --- + m_box_loft = new wxBoxSizer(wxVERTICAL); + m_box_loft->Add(card_header("design_extrude", _L("Loft"), m_hdr_loft), 0, wxLEFT | wxRIGHT | wxTOP, 12); + m_box_loft->Add(new wxStaticLine(m_form), 0, wxEXPAND | wxALL, 8); + m_box_loft->Add(new wxStaticText(m_form, wxID_ANY, _L("Profiles (check 2+, in order):")), + 0, wxLEFT | wxRIGHT | wxTOP, 12); + m_loft_list = new wxCheckListBox(m_form, wxID_ANY, wxDefaultPosition, wxSize(-1, 120)); + m_loft_list->Bind(wxEVT_CHECKLISTBOX, [this](wxCommandEvent&) { refresh_preview(); }); + m_box_loft->Add(m_loft_list, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 12); + { + auto* lform = new wxFlexGridSizer(2, 6, 8); + + m_loft_mode = new wxChoice(m_form, wxID_ANY); + m_loft_mode->Append("New"); + m_loft_mode->Append("Add"); + m_loft_mode->Append("Cut"); + m_loft_mode->Append("Intersect"); + m_loft_mode->SetSelection(0); + lform->Add(new wxStaticText(m_form, wxID_ANY, _L("Mode")), 0, wxALIGN_CENTER_VERTICAL); + lform->Add(m_loft_mode); + + m_box_loft->Add(lform, 0, wxLEFT | wxRIGHT | wxTOP, 12); + } + m_loft_ruled = new wxCheckBox(m_form, wxID_ANY, _L("Ruled (straight) sections")); + m_box_loft->Add(m_loft_ruled, 0, wxLEFT | wxRIGHT | wxTOP, 12); + root->Add(m_box_loft, 0, wxEXPAND); + + // --- Shell (hollow the current body to a wall thickness, removing one picked face) --- + auto* sform = new wxFlexGridSizer(2, 6, 8); + m_shell_thickness = make_spin(m_form, 2.0, 0.01, 100000.0); + sform->Add(new wxStaticText(m_form, wxID_ANY, _L("Thickness")), 0, wxALIGN_CENTER_VERTICAL); + sform->Add(m_shell_thickness); + m_shell_face_label = new wxStaticText(m_form, wxID_ANY, _L("(all faces — closed hollow)")); + sform->Add(new wxStaticText(m_form, wxID_ANY, _L("Open face")), 0, wxALIGN_CENTER_VERTICAL); + sform->Add(m_shell_face_label, 0, wxALIGN_CENTER_VERTICAL); + + m_box_shell = new wxBoxSizer(wxVERTICAL); + m_box_shell->Add(card_header("design_dressup", _L("Shell"), m_hdr_shell), 0, wxLEFT | wxRIGHT | wxTOP, 12); + m_box_shell->Add(new wxStaticLine(m_form), 0, wxEXPAND | wxALL, 8); + m_box_shell->Add(new wxStaticText(m_form, wxID_ANY, + _L("Pick a solid face to open it, then set the wall thickness.")), + 0, wxLEFT | wxRIGHT, 12); + m_box_shell->Add(sform, 0, wxLEFT | wxRIGHT | wxTOP, 12); + root->Add(m_box_shell, 0, wxEXPAND); + + // --- Draft (taper a single picked solid face about the body bottom) --- + auto* drform = new wxFlexGridSizer(2, 6, 8); + m_draft_angle = make_spin(m_form, 5.0, -89.0, 89.0); + drform->Add(new wxStaticText(m_form, wxID_ANY, _L("Angle (°)")), 0, wxALIGN_CENTER_VERTICAL); + drform->Add(m_draft_angle); + m_draft_face_label = new wxStaticText(m_form, wxID_ANY, _L("(pick a side face)")); + drform->Add(new wxStaticText(m_form, wxID_ANY, _L("Face")), 0, wxALIGN_CENTER_VERTICAL); + drform->Add(m_draft_face_label, 0, wxALIGN_CENTER_VERTICAL); + + m_box_draft = new wxBoxSizer(wxVERTICAL); + m_box_draft->Add(card_header("design_dressup", _L("Draft"), m_hdr_draft), 0, wxLEFT | wxRIGHT | wxTOP, 12); + m_box_draft->Add(new wxStaticLine(m_form), 0, wxEXPAND | wxALL, 8); + m_box_draft->Add(new wxStaticText(m_form, wxID_ANY, + _L("Pick a side face, then set the draft angle. The face pivots about the body base.")), + 0, wxLEFT | wxRIGHT, 12); + m_box_draft->Add(drform, 0, wxLEFT | wxRIGHT | wxTOP, 12); + root->Add(m_box_draft, 0, wxEXPAND); + + // --- Docked value-entry card (Onshape Button->Dialog->Confirm for dimensions) --- + m_box_value = new wxBoxSizer(wxVERTICAL); + { + // Header title doubles as the operation label (set by request_value()). + m_box_value->Add(card_header("design_constrain", _L("Value"), m_value_label), 0, wxLEFT | wxRIGHT | wxTOP, 12); + m_box_value->Add(new wxStaticLine(m_form), 0, wxEXPAND | wxALL, 8); + auto* vrow = new wxBoxSizer(wxHORIZONTAL); + // wxTE_PROCESS_ENTER so the user can just type a value and press Enter to + // apply it (the natural CAD-dimension gesture), not only click Confirm. + // Plain text field (not a spin control): on wxGTK the native GtkSpinButton + // formats per the user locale (comma) with no clean override, so we own the + // formatting here to guarantee international '.' decimals. + m_value_input = new wxTextCtrl(m_form, wxID_ANY, "", wxDefaultPosition, + wxSize(90, -1), wxTE_PROCESS_ENTER); + m_value_input->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { confirm_value(); }); + vrow->Add(new wxStaticText(m_form, wxID_ANY, _L("Value")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 8); + vrow->Add(m_value_input, 0, wxALIGN_CENTER_VERTICAL); + m_box_value->Add(vrow, 0, wxLEFT | wxRIGHT | wxTOP, 12); + + auto* row = new wxBoxSizer(wxHORIZONTAL); + auto* ok = new wxButton(m_form, wxID_ANY, _L("✓ Confirm")); + ok->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { confirm_value(); }); + auto* no = new wxButton(m_form, wxID_ANY, _L("✗ Cancel")); + no->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { cancel_value(); }); + row->Add(ok, 0, wxRIGHT, 8); + row->Add(no, 0); + m_box_value->Add(row, 0, wxALL, 12); + } + root->Add(m_box_value, 0, wxEXPAND); + + // --- Sketch-entry card (Phase 3): plane/orientation, opens on "New sketch", + // persists until Finish. The toolbar holds only the drawing tools. --- + m_box_sketch_session = new wxBoxSizer(wxVERTICAL); + m_box_sketch_session->Add(card_header("design_sketch", _L("Sketch"), m_hdr_sketch_session), + 0, wxLEFT | wxRIGHT | wxTOP, 12); + m_box_sketch_session->Add(new wxStaticLine(m_form), 0, wxEXPAND | wxALL, 8); + { + auto* prow = new wxBoxSizer(wxHORIZONTAL); + prow->Add(new wxStaticText(m_form, wxID_ANY, _L("Plane")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 8); + m_draw_plane = new wxChoice(m_form, wxID_ANY); + m_draw_plane->Append("XY"); m_draw_plane->Append("XZ"); m_draw_plane->Append("YZ"); + m_draw_plane->SetSelection(0); + prow->Add(m_draw_plane, 0, wxALIGN_CENTER_VERTICAL); + m_box_sketch_session->Add(prow, 0, wxLEFT | wxRIGHT | wxTOP, 12); + auto* hint = new wxStaticText(m_form, wxID_ANY, + _L("Pick a plane, then draw. Finish (✓) when done.")); + hint->SetForegroundColour(dp_sec_text()); + m_box_sketch_session->Add(hint, 0, wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, 12); + } + root->Add(m_box_sketch_session, 0, wxEXPAND); + + // --- Constraint-manager card (C3.4): list of the constrained sketch's + // entity-constraints; each row selects (highlights) + deletes. Shown only + // in Constrain mode; rebuilt by rebuild_constraint_list(). + m_box_constraints = new wxBoxSizer(wxVERTICAL); + m_box_constraints->Add(card_header("design_constrain", _L("Constraints"), m_hdr_constraints), + 0, wxLEFT | wxRIGHT | wxTOP, 12); + m_box_constraints->Add(new wxStaticLine(m_form), 0, wxEXPAND | wxALL, 8); + m_constraint_rows = new wxBoxSizer(wxVERTICAL); + m_box_constraints->Add(m_constraint_rows, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 12); + root->Add(m_box_constraints, 0, wxEXPAND); + + root->Add(new wxStaticText(m_form, wxID_ANY, _L("Feature tree")), 0, wxLEFT | wxTOP, 12); + m_tree = new wxTreeCtrl(m_form, wxID_ANY, wxDefaultPosition, wxSize(-1, 140), + wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES | + wxTR_FULL_ROW_HIGHLIGHT | wxBORDER_SIMPLE); + if (!dp_dark()) m_tree->SetBackgroundColour(dp_panel_bg()); + // Per-feature-type icons (indices match tree_icon_for): sketch/extrude/dressup/hole/thread. + m_tree_images = new wxImageList(16, 16); + m_tree_images->Add(create_scaled_bitmap("design_sketch", nullptr, 16)); // 0 Sketch + m_tree_images->Add(create_scaled_bitmap("design_extrude", nullptr, 16)); // 1 Extrude + m_tree_images->Add(create_scaled_bitmap("design_dressup", nullptr, 16)); // 2 Fillet/Chamfer + m_tree_images->Add(create_scaled_bitmap("design_hole", nullptr, 16)); // 3 Hole + m_tree_images->Add(create_scaled_bitmap("design_thread", nullptr, 16)); // 4 Thread + m_tree_images->Add(create_scaled_bitmap("design_dressup", nullptr, 16)); // 5 Shell + m_tree->AssignImageList(m_tree_images); + root->Add(m_tree, 0, wxEXPAND | wxALL, 12); + + // Selecting a body-producing feature (Extrude/Fillet/Chamfer/Hole/Thread) in the + // tree highlights the solid in the viewport; a Sketch row clears the highlight + // (its face is already shown via the persistent sketch overlay). + m_tree->Bind(wxEVT_TREE_SEL_CHANGED, [this](wxTreeEvent&) { + if (!m_viewport) return; + // A Parts-list body row: highlight that body and make it the op target. + const int bsel = tree_body_selection(); + if (bsel >= 0) { + m_viewport->set_body_highlight(false); // the per-body overlay does the tint + m_viewport->select_body(bsel); + m_sel_solid_body = bsel; + m_sel_solid_face = m_sel_solid_edge = -1; + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(wxString::Format(_L("Body %d selected — next Extrude / Fillet acts on it"), bsel + 1)); + m_status->Refresh(); + return; + } + const int sel = tree_selection(); + const bool body = (sel >= 0 && sel < int(m_doc.features.size()) && + m_doc.features[sel].type != CadFeatureType::Sketch && + !m_doc.body.IsNull()); + m_viewport->set_body_highlight(body); + }); + + // Feature-tree edit row: act on the selected feature (delete / reorder). + { + auto* trow = new wxBoxSizer(wxHORIZONTAL); + auto edit_btn = [this](const char* icon, const wxString& tip) { + // Enlarged to match the main ribbon's weight (largest that fits 6 + // across the ~264px form column). + auto* b = new ScalableButton(m_form, wxID_ANY, icon, "", wxSize(36, 36), + wxDefaultPosition, wxBU_EXACTFIT | wxBORDER_NONE, false, 30); + b->SetToolTip(tip); + return b; + }; + auto* edit = edit_btn("design_edit", _L("Edit")); + edit->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_edit_feature(); }); + auto* move = edit_btn("design_move", _L("Move body / Scale imported Text-SVG")); + move->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + const int sel = tree_selection(); + if (sel != wxNOT_FOUND && sel < int(m_doc.features.size()) && + !m_doc.features[sel].imported_regions.empty()) { + on_transform_imported(sel); + } else if (m_sel_solid_body >= 0 && m_sel_solid_body < int(m_doc.bodies.size())) { + on_move_body(); // translate the selected body with the 3-axis gizmo + } else { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Select a body to move it, or an imported Text/SVG to scale")); + m_status->Refresh(); + } + }); + auto* vis = edit_btn("design_eye", _L("Show / hide")); + vis->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_toggle_visibility(); }); + auto* del = edit_btn("design_delete", _L("Delete")); + del->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_delete_feature(); }); + auto* up = edit_btn("design_moveup", _L("Move up")); + up->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_move_feature(-1); }); + auto* down = edit_btn("design_movedown", _L("Move down")); + down->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_move_feature(+1); }); + trow->Add(edit, 0, wxRIGHT, 4); + trow->Add(move, 0, wxRIGHT, 4); + trow->Add(vis, 0, wxRIGHT, 4); + trow->Add(del, 0, wxRIGHT, 4); + trow->Add(up, 0, wxRIGHT, 4); + trow->Add(down, 0); + root->Add(trow, 0, wxLEFT | wxRIGHT | wxBOTTOM, 12); + } + + // Prepare's "Place on Face (F)" for the selected body: pick a face, lay it flat on the bed. + auto* place = new wxButton(m_form, wxID_ANY, _L("Place on Face (F)")); + place->SetToolTip(_L("Select a body face (click a solid, click again to a face), then lay that face on the bed")); + place->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { place_on_face(); }); + root->Add(place, 0, wxLEFT | wxRIGHT | wxBOTTOM, 12); + + m_status = new wxStaticText(m_form, wxID_ANY, ""); + root->Add(m_status, 0, wxLEFT | wxRIGHT | wxBOTTOM, 12); + + // DoF / constraint-state readout (P3). Dedicated line so it never clobbers the + // tool hint in m_status; updated by the on_solve_state callback after each solve. + m_dof_status = new wxStaticText(m_form, wxID_ANY, ""); + { + wxFont f = m_dof_status->GetFont(); + f.SetWeight(wxFONTWEIGHT_BOLD); + m_dof_status->SetFont(f); + } + root->Add(m_dof_status, 0, wxLEFT | wxRIGHT | wxBOTTOM, 12); + + auto* commit = new wxButton(m_form, wxID_ANY, _L("Commit to Plate")); + commit->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_commit(); }); + root->Add(commit, 0, wxALL, 12); + + m_shape->Bind(wxEVT_CHOICE, [this](wxCommandEvent& e) { on_shape_changed(); e.Skip(); }); + on_shape_changed(); + + // Any parameter edit refreshes the translucent preview. Command events from the + // spin/choice/checkbox children propagate up to m_form, so one binding each suffices. + m_form->Bind(wxEVT_SPINCTRLDOUBLE, [this](wxSpinDoubleEvent& e) { refresh_preview(); e.Skip(); }); + m_form->Bind(wxEVT_CHOICE, [this](wxCommandEvent& e) { refresh_preview(); e.Skip(); }); + m_form->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& e) { refresh_preview(); e.Skip(); }); + + m_form->SetSizer(root); + + // Start with every tool dialog hidden (only the toolbar + tree + Commit show). + root->Show(m_box_sketch, false, true); + root->Show(m_box_extrude, false, true); + root->Show(m_box_revolve, false, true); + root->Show(m_box_sweep, false, true); + root->Show(m_box_pattern, false, true); + root->Show(m_box_plane, false, true); + root->Show(m_box_loft, false, true); + root->Show(m_box_draft, false, true); + root->Show(m_box_boolean, false, true); + root->Show(m_box_cut, false, true); + root->Show(m_box_insert, false, true); + root->Show(m_box_dressup, false, true); + root->Show(m_box_hole, false, true); + root->Show(m_box_thread, false, true); + root->Show(m_box_shell, false, true); + root->Show(m_box_value, false, true); + root->Show(m_box_sketch_session, false, true); + root->Show(m_box_constraints, false, true); + + m_form->FitInside(); + m_form->SetScrollRate(10, 10); + m_form->SetMinSize(wxSize(264, -1)); + + // Right column: a small view toolbar over the live 3D viewport that mirrors + // the CadDocument body. + m_viewport = new DesignCanvas(this); + + m_viewport->set_on_sketch_commit([this](const SketchProfile& prof, const SketchPlane& plane) { + m_doc.checkpoint(); // undo boundary: committing a sketch + m_feature_counter++; + m_doc.add_sketch_profile(prof, plane, "Sketch" + std::to_string(m_feature_counter)); + m_doc.recompute(); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(_L("Sketch created — select it and Extrude")); + refresh_tree(); + }); + + m_viewport->set_on_sketch_entities_commit( + [this](const std::vector& ents, + const std::vector& cons, + const SketchPlane& plane) { + if (ents.empty()) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Sketch empty — nothing committed")); + m_status->Refresh(); + return; + } + m_doc.checkpoint(); // undo boundary: committing / re-editing an entity sketch + // Re-edit of a committed entity sketch: REPLACE it in place (keep its name + + // tree position) instead of appending a duplicate. + if (m_edit_index >= 0 && m_edit_index < int(m_doc.features.size()) && + m_doc.features[m_edit_index].type == CadFeatureType::Sketch) { + CadFeature edited = m_doc.features[m_edit_index]; + edited.entities = ents; + edited.entity_constraints = cons; + edited.plane = plane; + if (m_doc.replace_feature(m_edit_index, edited)) { + if (!cons.empty()) m_doc.solve_sketch_feature(m_edit_index); + m_doc.recompute(); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(_L("Sketch updated")); + m_edit_index = -1; + refresh_tree(); + sync_sketch_display(); + } + return; + } + m_feature_counter++; + const int sk = m_doc.add_sketch_entities(ents, plane, + "Sketch" + std::to_string(m_feature_counter), cons); + if (!cons.empty()) m_doc.solve_sketch_feature(sk); // enforce driving dimensions + m_doc.recompute(); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(cons.empty() + ? _L("Sketch created — select it and Extrude") + : wxString::Format(_L("Sketch created (%zu driving dims) — select it and Extrude"), + cons.size())); + refresh_tree(); + sync_sketch_display(); // keep the just-committed sketch visible as a face + }); + + // Live length/angle readout while drawing a Line/Polyline segment. + m_viewport->set_on_cursor_metrics([this](double len, double ang_deg, bool locked) { + double a = ang_deg; if (a < 0.0) a += 360.0; // show bearing 0..360 + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(wxString::Format(L"L %.2f mm %.1f°%s", + len, a, locked ? L" (locked)" : L"")); + m_status->Refresh(); + }); + + // DoF feedback (P3): after each live solve, report constraint state on its own + // line. Green = fully constrained, red = conflicting, neutral = N remaining DoF. + m_viewport->set_on_solve_state([this](int dof, bool ok, bool has_constraints) { + if (!m_dof_status) return; + if (!has_constraints) { + m_dof_status->SetLabel(wxString()); + } else if (!ok) { + m_dof_status->SetForegroundColour(wxColour(235, 80, 80)); + m_dof_status->SetLabel(_L("✗ Conflicting constraints")); + } else if (dof == 0) { + m_dof_status->SetForegroundColour(wxColour(80, 200, 110)); + m_dof_status->SetLabel(_L("✓ Fully constrained")); + } else if (dof > 0) { + m_dof_status->SetForegroundColour(dp_ctl_text()); + m_dof_status->SetLabel(wxString::Format(_L("%d degrees of freedom"), dof)); + } else { + m_dof_status->SetLabel(wxString()); + } + m_dof_status->Refresh(); + m_form->Layout(); + }); + + // Selection (Select tool): reflect the count in the status line. + m_viewport->set_on_sketch_selection_changed([this](int count) { + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(count > 0 + ? wxString::Format(_L("%d selected — Delete removes them"), count) + : _L("Click to select; click a filled face to extrude; Shift to add")); + m_status->Refresh(); + }); + + // Onshape flow: clicking inside a closed-loop face commits the sketch and opens + // the Extrude dialog (with a ghost preview) targeting that sketch. + m_viewport->set_on_sketch_face_selected([this]() { + if (!m_viewport) return; + m_viewport->finish_sketch(); // commit live sketch (synchronous) + m_extrude_sketch_ref = resolve_extrude_sketch(); + if (m_extrude_sketch_ref < 0) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Could not resolve the sketch to extrude")); + m_status->Refresh(); + return; + } + set_ui_mode(UiMode::Feature); + open_tool(Tool::Extrude); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(_L("Face selected — set the depth and Confirm")); + m_status->Refresh(); + }); + + // Clicking a committed sketch loop on the plate (no live session) selects THAT loop: + // the viewport highlights only it (cyan) and its Sketch feature's tree row is selected. + // The (feature, region) pair is remembered so Extrude builds just that one loop. + m_viewport->set_on_display_sketch_selected([this](int feat, int region) { + if (feat < 0 || feat >= int(m_doc.features.size())) return; + m_sel_sketch_feat = feat; + m_sel_sketch_region = region; + // Last pick wins (symmetric with the solid-pick handler): selecting a sketch loop drops + // any stale solid face/edge pick so Extrude treats this loop as the profile. + m_sel_solid_face = m_sel_solid_edge = -1; + set_tree_selection(feat); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(region >= 0 + ? _L("Loop selected — Extrude it, or Edit / Delete the sketch") + : _L("Sketch selected — Extrude it, or Edit / Delete from the tree")); + m_status->Refresh(); + }); + + // F key (Prepare's Place on Face): the tool forwards it here when the Design viewport + // has focus; we lay the selected body face on the bed. Returns false when no face is + // selected so the key can fall through to the default handler. + m_viewport->set_on_place_on_face([this]() { return place_on_face(); }); + + // Clicking a solid cycles whole -> face -> edge. The tool draws the cyan overlay for ALL + // levels now (per-body, so other bodies stay untinted) — no whole-compound set_body_highlight. + m_viewport->set_on_solid_selection_changed([this](int level, int body, int face, int edge) { + // A pick that fell through the move gizmo (clicked off the arrows) exits move mode. + if (m_viewport->moving_body()) m_viewport->clear_move_gizmo(); + // Remember which body + face/edge so Extrude / dress-up target the RIGHT body. + m_sel_solid_body = (level >= 1) ? body : -1; + m_sel_solid_face = (level >= 2) ? face : -1; + m_sel_solid_edge = (level == 3) ? edge : -1; + // Last pick wins: selecting a solid drops any stale committed-sketch loop selection. + // Otherwise a leftover loop keeps `m_sel_sketch_region >= 0`, which blocks the face + // push/pull branch in Extrude (`m_sel_solid_face >= 0 && m_sel_sketch_region < 0`) and + // makes Extrude build a DETACHED new body from the last sketch instead of push/pulling + // the face the user just clicked. + if (level >= 1) { m_sel_sketch_region = -1; m_sel_sketch_feat = -1; } + // If the Fillet/Chamfer card is open, re-anchor (or drop) the radius arrow on the new pick + // and rebuild the ghost — once an edge is picked the preview-only mode hides the base body. + if (m_active == Tool::Dressup) { update_fillet_gizmo(); refresh_preview(); } + // If the Shell card is open, a face pick chooses the open face: update the label + gizmo + // + ghost so the hollow updates live. + if (m_active == Tool::Shell) { + m_shell_face_label->SetLabel(m_sel_solid_face >= 0 + ? wxString::Format(_L("Face %d"), m_sel_solid_face) + : _L("(all faces — closed hollow)")); + refresh_preview(); // rebuilds the shell ghost + re-anchors the thickness gizmo + } + // Draft card open: a face pick chooses the face to taper; update label + ghost live. + if (m_active == Tool::Draft) { + m_draft_face_label->SetLabel(m_sel_solid_face >= 0 + ? wxString::Format(_L("Face %d"), m_sel_solid_face) + : _L("(pick a side face)")); + refresh_preview(); + } + m_status->SetForegroundColour(wxNullColour); + const int nb = int(m_doc.bodies.size()); + const wxString bodytag = (nb > 1) ? wxString::Format(_L("Body %d "), body + 1) : wxString(); + m_status->SetLabel(level == 1 ? bodytag + _L("selected (whole) — click again for a face") + : level == 2 ? bodytag + wxString::Format(_L("face %d selected — Extrude to push/pull it, or click again for an edge"), face) + : level == 3 ? bodytag + wxString::Format(_L("edge %d selected — open Fillet/Chamfer to dress it, or click again to reset"), edge) + : _L("Nothing selected")); + m_status->Refresh(); + }); + + // Visual Extrude gizmo (C5b): dragging/editing the in-canvas depth arrow writes the + // matching spin field and re-previews (which re-feeds the gizmo with the new depth). + m_viewport->set_on_extrude_depth_changed([this](double depth, bool second) { + if (second) { if (m_distance2) m_distance2->SetValue(depth); } + else { if (m_distance) m_distance->SetValue(depth); } + refresh_preview(); + }); + + // Move-body gizmo (M5): each drag/edit reports the body's new translation. Store it as a + // display-only per-body transform and re-feed the moved meshes (the OCCT shape is untouched, + // so face/edge ids the dress-up ops target stay valid). + m_viewport->set_on_body_move_changed([this](int body, const Transform3d& xform) { + sync_body_xform(); + if (body < 0 || body >= int(m_body_xform.size())) return; + m_body_xform[body] = xform; // full move+rotate transform, baked into the mesh at Commit + feed_bodies(); // rebuilds the transformed meshes in place + refreshes display + pick + const int nb = int(m_doc.bodies.size()); + const wxString tag = (nb > 1) ? wxString::Format(_L("Body %d "), body + 1) : wxString(); + const Vec3d t = xform.translation(); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(tag + wxString::Format(_L("placed (%.1f, %.1f, %.1f) mm — drag arrows to move, rings to rotate"), + t.x(), t.y(), t.z())); + m_status->Refresh(); + }); + + // Fillet/Chamfer radius gizmo: dragging (or editing) the edge-anchored arrow writes the + // Dress-up size and refreshes the ghost. SetValue is silent in wx, so refresh explicitly. + m_viewport->set_on_fillet_radius_changed([this](double radius) { + if (m_dressup_size) m_dressup_size->SetValue(radius); + refresh_preview(); // rebuilds the candidate fillet ghost at the new radius + }); + + // Hole gizmo: dragging/editing the centre, diameter, or depth handle writes the four Hole-card + // spins and refreshes the ghost. SetValue is silent in wx, so refresh explicitly. + m_viewport->set_on_hole_changed([this](double x, double y, double diameter, double depth) { + if (m_hole_x) m_hole_x->SetValue(x); + if (m_hole_y) m_hole_y->SetValue(y); + if (m_hole_diameter) m_hole_diameter->SetValue(diameter); + if (m_hole_depth) m_hole_depth->SetValue(depth); + refresh_preview(); // rebuilds the candidate hole ghost at the new position/size + }); + + // Thread gizmo: dragging/editing the centre, radius, or length handle writes the Thread-card + // spins and refreshes the ghost (SetValue is silent in wx). + m_viewport->set_on_thread_changed([this](double x, double y, double radius, double height) { + if (m_thread_x) m_thread_x->SetValue(x); + if (m_thread_y) m_thread_y->SetValue(y); + if (m_thread_radius) m_thread_radius->SetValue(radius); + if (m_thread_height) m_thread_height->SetValue(height); + refresh_preview(); + }); + + // Shell gizmo: dragging/editing the inward thickness arrow writes the Shell-card thickness + // and refreshes the ghost (SetValue is silent in wx). + m_viewport->set_on_shell_thickness_changed([this](double thickness) { + if (m_shell_thickness) m_shell_thickness->SetValue(thickness); + refresh_preview(); + }); + + m_viewport->set_on_revolve_angle_changed([this](double angle) { + if (m_revolve_angle) m_revolve_angle->SetValue(angle); + refresh_preview(); + }); + + m_viewport->set_on_pattern_changed([this](double value) { + // Linear drag feeds spacing; circular drag feeds angle. The card knows which is live. + if (m_pattern_type && m_pattern_type->GetSelection() == 1) { + if (m_pattern_angle) m_pattern_angle->SetValue(value); + } else if (m_pattern_spacing) { + m_pattern_spacing->SetValue(value); + } + refresh_preview(); + }); + + // Esc exits the active sketch tool: drop the live session, restore Feature mode + + // the committed-sketch overlay (an in-progress draw is discarded). The tool's layered + // request_exit only calls this once it's an idle Select session. + m_viewport->set_on_sketch_exit([this]() { + // While placing imported Text/SVG art, right-click = Confirm (keep the art) — the + // Insert card is the explicit gate, this is the in-canvas shortcut to it. + if (m_active == Tool::Insert) { finalize_insert(); return; } + if (m_viewport) m_viewport->cancel_sketch(); + m_edit_index = -1; + set_ui_mode(UiMode::Feature); + sync_sketch_display(); + refresh_tree(); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(_L("Tool exited")); + m_status->Refresh(); + }); + + // Ctrl+Z / Ctrl+Shift+Z (Ctrl+Y) from the viewport → feature-history undo/redo. + m_viewport->set_on_undo_redo([this](bool redo) { do_undo_redo(redo); }); + + // Esc = the unified Cancel everywhere. Feature cards had no key exit (only the button); + // CHAR_HOOK on the panel catches Esc from the card or viewport and routes to tool_cancel. + // Sketch/Constrain keep the viewport's per-gesture Esc (abort the current point first), + // so we only intercept Esc here when a feature/insert card is the thing to dismiss. + Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& e) { + const bool dismissable = m_active != Tool::None || (m_viewport && m_viewport->moving_body()); + if (e.GetKeyCode() == WXK_ESCAPE && dismissable) { tool_cancel(); return; } + e.Skip(); + }); + + // Right-click finishes the move gizmo in the viewport; mirror that on the panel so the + // action bar (shown while moving) hides and the move state clears. + m_viewport->set_on_move_exit([this]() { m_move_body = -1; update_action_bar(); }); + + // The Line tool's length and the Dimension tool's value are both entered in-canvas now + // (live quote labels + the floating SketchInlineEditor), so the old docked-card + // callbacks (on_segment_drawn / on_dimension_pick_complete) are no longer wired. + + // Imported-art bbox transform streams the live offset/scale back here; write them to + // the feature and re-sync the overlay so the art tracks the drag. + m_viewport->set_on_imported_transform([this](int feat, Vec2d off, double sx, double sy) { + if (feat < 0 || feat >= int(m_doc.features.size())) return; + CadFeature& f = m_doc.features[feat]; + if (f.imported_regions.empty()) return; + f.import_offset = off; + f.import_scale_x = sx; + f.import_scale_y = sy; + m_doc.recompute(); + sync_sketch_display(); + }); + + auto* vcol = new wxBoxSizer(wxVERTICAL); + // The bottom 3D-navigator orb handles all view orientation, so no separate view buttons. + // Fit view is a double-click on the viewport (the tool intercepts it -> zoom_to_volumes). + vcol->Add(m_viewport, 1, wxEXPAND); + + // Onshape layout: top toolbar over [ slim left column | center viewport ]. + auto* body = new wxBoxSizer(wxHORIZONTAL); + body->Add(m_form, 0, wxEXPAND); + body->Add(vcol, 1, wxEXPAND); + + auto* outer = new wxBoxSizer(wxVERTICAL); + outer->Add(m_toolbar, 0, wxEXPAND); + outer->Add(new wxStaticLine(this, wxID_ANY), 0, wxEXPAND); + outer->Add(body, 1, wxEXPAND); + SetSizer(outer); + + set_ui_mode(UiMode::Feature); +} + +void DesignPanel::set_active_tool_btn(ScalableButton* b) +{ + // Onshape-style: the active tool's button gets the Orca accent (teal); the + // rest revert to the ribbon surface. nullptr clears the whole strip. + m_active_tool_btn = b; + const wxColour bg = dp_ribbon_bg(), teal(0x00, 0x96, 0x88); + for (auto* btn : m_tool_btns) { + if (btn == nullptr) continue; + btn->SetBackgroundColour(btn == b ? teal : bg); + btn->Refresh(); + } +} + +void DesignPanel::set_ui_mode(UiMode m) +{ + m_ui_mode = m; + wxSizer* s = m_toolbar->GetSizer(); + s->Show(m_tb_feature, m == UiMode::Feature, true); + s->Show(m_tb_sketch, m == UiMode::Sketch, true); + s->Show(m_tb_constrain, m == UiMode::Constrain, true); + m_toolbar->Layout(); + m_toolbar->FitInside(); // refresh the horizontal scroll range for the new group widths + set_active_tool_btn(nullptr); // no tool selected right after a mode switch + // Phase 3: the docked Sketch card (plane/orientation) shows for the whole Sketch + // session and hides on Finish/Constrain. + if (m_box_sketch_session != nullptr && m_form != nullptr && m_form->GetSizer() != nullptr) { + if (m == UiMode::Sketch && m_hdr_sketch_session != nullptr) + m_hdr_sketch_session->SetLabel(wxString::Format(_L("Sketch %d"), m_feature_counter + 1)); + m_form->GetSizer()->Show(m_box_sketch_session, m == UiMode::Sketch, true); + m_form->Layout(); + m_form->FitInside(); + } + // Constraint-manager card follows Constrain mode; rebuilt from the active feature. + if (m_box_constraints != nullptr && m_form != nullptr && m_form->GetSizer() != nullptr) { + if (m == UiMode::Constrain) + rebuild_constraint_list(); + else + m_form->GetSizer()->Show(m_box_constraints, false, true); + m_form->Layout(); + m_form->FitInside(); + } + update_action_bar(); // Sketch/Constrain modes show the unified ✓/✗; Feature idle hides it +} + +void DesignPanel::on_shape_changed() +{ + bool rect = (m_shape->GetSelection() == 0); + m_width->Enable(rect); + m_height->Enable(rect); + m_radius->Enable(!rect); +} + +void DesignPanel::set_status_ok() +{ + m_status->SetLabel(wxString::Format(_L("OK — %zu triangles"), + m_doc.display_mesh.its.indices.size())); + if (m_viewport != nullptr) { + m_viewport->clear_move_gizmo(); // a recompute invalidates the gizmo's body centroid + rebuild_disp_meshes(); // apply per-body Move transforms to the display/pick meshes + // Point the solid-pick at the fresh body + TRANSFORMED pick mesh (stable address) + the + // per-body xform vector (for edge sampling). Resets the whole/face/edge selection, whose + // ids invalidate on every recompute. Null body is handled inside. + m_viewport->set_solid_pick(&m_doc.bodies, &m_disp_pick_mesh, + &m_doc.display_tri_face, &m_doc.display_tri_body, + &m_body_visible, &m_body_xform); + feed_bodies(); + } + sync_sketch_display(); +} + +// Draw every committed sketch that no enabled Extrude consumes, so a sketch stays +// visible (as a translucent face + outline) when it is not part of the solid — e.g. +// after its Extrude is removed, or right after Finish. +void DesignPanel::sync_sketch_display() +{ + if (m_viewport == nullptr) return; + const int n = int(m_doc.features.size()); + std::vector consumed(n, false); + // Per-loop extrudes (sketch_ref < 0) carry a verbatim copy of the one loop they + // consumed; collect those so that loop is hidden from its source sketch overlay. + std::vector> consumed_loops; + for (const CadFeature& f : m_doc.features) { + if (f.type != CadFeatureType::Extrude || !f.enabled) continue; + if (f.sketch_ref >= 0 && f.sketch_ref < n) consumed[f.sketch_ref] = true; + else if (f.sketch_ref < 0 && !f.entities.empty()) consumed_loops.push_back(f.entities); + } + // Two loops match when their entities are the same geometry in the same order — the + // per-loop extrude stored a verbatim copy, so this is an exact comparison. + auto same_loop = [](const std::vector& a, const std::vector& b) { + if (a.size() != b.size() || a.empty()) return false; + auto eq = [](const Vec2d& u, const Vec2d& v) { return (u - v).squaredNorm() < 1e-10; }; + for (size_t k = 0; k < a.size(); ++k) { + const SketchEntity& x = a[k]; const SketchEntity& y = b[k]; + if (x.type != y.type || !eq(x.p0, y.p0) || !eq(x.p1, y.p1) || + !eq(x.center, y.center) || std::abs(x.radius - y.radius) > 1e-7) return false; + } + return true; + }; + + std::vector ds; + for (int i = 0; i < n; ++i) { + const CadFeature& f = m_doc.features[i]; + if (f.type != CadFeatureType::Sketch || consumed[i] || !f.enabled) + continue; + if (!f.entities.empty()) { + if (consumed_loops.empty()) { + ds.push_back({ f.entities, f.plane, i }); + } else { + // Drop the entities of any loop already extruded; keep the rest (other + // loops + non-loop entities) so they stay visible and selectable. + std::vector drop(f.entities.size(), 0); + for (const std::vector& loop : m_viewport->region_entity_indices(f.entities)) { + std::vector es; + for (int ei : loop) + if (ei >= 0 && ei < int(f.entities.size())) es.push_back(f.entities[ei]); + bool gone = false; + for (const std::vector& c : consumed_loops) + if (same_loop(es, c)) { gone = true; break; } + if (gone) + for (int ei : loop) + if (ei >= 0 && ei < int(drop.size())) drop[ei] = 1; + } + std::vector shown; + for (int ei = 0; ei < int(f.entities.size()); ++ei) + if (!drop[ei]) shown.push_back(f.entities[ei]); + if (!shown.empty()) ds.push_back({ std::move(shown), f.plane, i }); + } + } else if (!f.imported_regions.empty()) { + // Imported art (Text/SVG) carries no solver entities; synthesize + // closed line loops from each region contour so it shows as an + // outline overlay (display only — never stored on the feature). + // Apply the feature's placement transform so the overlay tracks + // moves / scales. + const auto regions = transform_regions(f.imported_regions, f.import_offset, + f.import_scale_x, f.import_scale_y); + std::vector lines; + for (const auto& region : regions) + for (const auto& contour : region) { + const int m = int(contour.size()); + for (int k = 0; k < m; ++k) { + SketchEntity e; + e.type = SketchEntity::Type::Line; + e.p0 = contour[k]; + e.p1 = contour[(k + 1) % m]; + lines.push_back(e); + } + } + if (!lines.empty()) + ds.push_back({ std::move(lines), f.plane, i }); + } + } + m_viewport->set_display_sketches(std::move(ds)); +} + +void DesignPanel::on_add_text() +{ + wxTextEntryDialog dlg(this, _L("Text to insert:"), _L("Text"), wxEmptyString); + if (dlg.ShowModal() != wxID_OK) + return; + const wxString text = dlg.GetValue(); + if (text.empty()) + return; + const std::string utf8(text.ToUTF8().data()); + // Insert at a default height; resize in-canvas via the bbox handles (Move/Scale). + add_imported_sketch(text_to_regions(utf8, 10.0), _L("Text")); +} + +void DesignPanel::on_import_svg() +{ + wxFileDialog dlg(this, _L("Import SVG"), wxEmptyString, wxEmptyString, + "SVG files (*.svg)|*.svg|All files|*.*", + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dlg.ShowModal() != wxID_OK) + return; + const std::string path(dlg.GetPath().ToUTF8().data()); + // Import at 1:1; resize in-canvas via the bbox handles (Move/Scale). + add_imported_sketch(svg_to_regions(path, 1.0), _L("SVG")); +} + +void DesignPanel::on_import_step() +{ + wxFileDialog dlg(this, _L("Import STEP"), wxEmptyString, wxEmptyString, + "STEP files (*.step;*.stp)|*.step;*.stp|All files|*.*", + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dlg.ShowModal() != wxID_OK) + return; + const std::string path(dlg.GetPath().ToUTF8().data()); + std::string err; + // Keep the OCCT B-rep (don't mesh it like the slicer importer): each top-level solid + // becomes a coexisting CadBody, fully editable by the on-face/edge feature tools. + const std::vector solids = GeometryEngine::read_step_solids(path, err); + if (solids.empty()) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(err.empty() ? _L("No solids found in STEP") + : (_L("STEP import failed: ") + wxString::FromUTF8(err))); + m_status->Refresh(); + return; + } + m_doc.checkpoint(); // undo boundary: importing STEP solids + for (const TopoDS_Shape& s : solids) { + m_feature_counter++; + CadFeature f; + f.type = CadFeatureType::Import; + f.name = std::string("STEP") + std::to_string(m_feature_counter); + f.imported_solid = s; + f.mode = BooleanMode::New; // each solid is its own coexisting body + m_doc.features.push_back(f); + } + if (!m_doc.recompute()) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("STEP import failed: ") + wxString::FromUTF8(m_doc.error)); + m_status->Refresh(); + return; + } + set_ui_mode(UiMode::Feature); // imported solids live in the feature timeline + refresh_tree(); + set_tree_selection(int(m_doc.features.size()) - 1); + set_status_ok(); // canonical post-recompute viewport/pick/parts refresh + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(wxString::Format( + _L("Imported %d solid(s) — pick a face or edge, then Fillet / Cut / Shell to modify"), + int(solids.size()))); + m_status->Refresh(); +} + +void DesignPanel::add_imported_sketch( + const std::vector>>& regions, + const wxString& base_name) +{ + if (regions.empty()) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("No importable geometry found")); + m_status->Refresh(); + return; + } + m_doc.checkpoint(); // undo boundary: importing Text/SVG art + m_feature_counter++; + CadFeature f; + f.type = CadFeatureType::Sketch; + f.name = std::string(base_name.ToUTF8().data()) + std::to_string(m_feature_counter); + f.imported_regions = regions; + + // #4: when a solid face is selected, drop the art ON that face, centred on it (ready to + // engrave). Otherwise place it on the draw-plane dropdown at the plane origin (legacy). + bool on_face = false; + if (m_sel_solid_face >= 0 && m_sel_solid_body >= 0 && m_sel_solid_body < int(m_doc.bodies.size())) { + const TopoDS_Face face = + GeometryEngine::face_by_index(m_doc.bodies[m_sel_solid_body].shape, m_sel_solid_face); + if (!face.IsNull()) { + f.plane = SketchPlane::from_face(face); + const Vec3d cw = GeometryEngine::face_centroid_world(face); + const Vec2d c_uv = f.plane.project(cw, f.plane.normal); // face centre in plane (u,v) + Vec2d lo(1e30, 1e30), hi(-1e30, -1e30); // bbox of the imported art + for (const auto& reg : regions) + for (const auto& loop : reg) + for (const Vec2d& p : loop) { lo = lo.cwiseMin(p); hi = hi.cwiseMax(p); } + f.import_offset = c_uv - 0.5 * (lo + hi); // centre the art on the face + f.import_on_face = true; + f.import_face_body = m_sel_solid_body; + on_face = true; + } + } + if (!on_face) + f.plane = m_draw_plane ? plane_from_choice(m_draw_plane->GetSelection()) + : SketchPlane::XY(); + + // Drop the live face selection (its body is now remembered on import_face_body): otherwise + // the next Extrude would push/pull that face instead of extruding the placed art. + m_sel_solid_face = m_sel_solid_edge = m_sel_solid_body = -1; + + m_doc.features.push_back(f); + m_doc.recompute(); // a lone sketch yields an empty body; that is expected + refresh_tree(); + const int newidx = int(m_doc.features.size()) - 1; + set_tree_selection(newidx); // select the new art + sync_sketch_display(); + on_transform_imported(newidx); // in-canvas place/size gizmo ON + // The feature is provisional until the user explicitly Confirms (Onshape gate). The + // Insert card carries Confirm/Cancel; Cancel undoes this insert. + m_insert_feat = newidx; + open_insert_card(base_name); +} + +// Show the Insert Confirm/Cancel card while the imported art is being placed/sized. +void DesignPanel::open_insert_card(const wxString& base_name) +{ + m_active = Tool::Insert; + if (m_hdr_insert) m_hdr_insert->SetLabel(base_name); + wxSizer* s = m_form->GetSizer(); + s->Show(m_box_insert, true, true); + m_form->Layout(); + m_form->FitInside(); + update_action_bar(); // surface the unified ✓/✗ + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(base_name + _L(" — drag to place/size, then Confirm")); + m_status->Refresh(); +} + +// Confirm: keep the placed art and leave the placement gizmo. The feature is already in +// the timeline (added provisionally); we just tear down the transient tool/gizmo state. +void DesignPanel::finalize_insert() +{ + const int feat = m_insert_feat; + m_insert_feat = -1; + if (m_viewport) m_viewport->cancel_sketch(); // exit the TransformArt gizmo + close_tool(); // hides the Insert card, clears m_active + set_ui_mode(UiMode::Feature); // imported art lives in the feature timeline + if (feat >= 0 && feat < int(m_doc.features.size())) set_tree_selection(feat); + sync_sketch_display(); + refresh_tree(); + set_status_ok(); +} + +// Cancel: discard the provisional insert (undo restores the pre-insert feature list). +void DesignPanel::cancel_insert() +{ + m_insert_feat = -1; + if (m_viewport) m_viewport->cancel_sketch(); // exit the TransformArt gizmo + m_doc.undo(); // remove the just-added imported feature + close_tool(); + set_ui_mode(UiMode::Feature); + sync_sketch_display(); + refresh_tree(); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(_L("Insert cancelled")); + m_status->Refresh(); +} + +void DesignPanel::on_transform_imported(int feat_idx) +{ + if (feat_idx < 0 || feat_idx >= int(m_doc.features.size()) || !m_viewport) + return; + const CadFeature& f = m_doc.features[feat_idx]; + if (f.imported_regions.empty()) + return; + // In-canvas bbox handles (replaces the Move/Scale dialog): drag a corner to scale, + // the centre to move. Values stream back via set_on_imported_transform. + m_viewport->begin_imported_transform(feat_idx, f.imported_regions, f.plane, + f.import_offset, f.import_scale_x, f.import_scale_y); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(_L("Drag a corner to scale, the centre to move — right-click when done")); + m_status->Refresh(); +} + +void DesignPanel::on_add_sketch() +{ + SketchShape shape = (m_shape->GetSelection() == 1) ? SketchShape::Circle + : SketchShape::Rectangle; + SketchPlane plane = plane_from_choice(m_plane->GetSelection()); + m_feature_counter++; + m_doc.add_sketch(shape, plane, m_width->GetValue(), m_height->GetValue(), + m_radius->GetValue(), "Sketch" + std::to_string(m_feature_counter)); + m_doc.recompute(); // a lone sketch yields an empty body; that is expected + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(_L("Sketch added — select it and Extrude")); + refresh_tree(); +} + +// Extrude should consume only the clicked loop when a specific region of the resolved +// sketch is selected and that loop actually has entities. +bool DesignPanel::extrude_uses_loop() const +{ + return m_viewport != nullptr + && m_sel_sketch_region >= 0 + && m_extrude_sketch_ref >= 0 + && m_extrude_sketch_ref == m_sel_sketch_feat + && m_extrude_sketch_ref < int(m_doc.features.size()) + && !m_viewport->selected_loop_entities().empty(); +} + +void DesignPanel::on_add_extrude() +{ + BooleanMode mode = static_cast(m_mode->GetSelection()); // New/Add/Cut/Intersect + m_feature_counter++; + const std::string name = "Extrude" + std::to_string(m_feature_counter); + int idx = -1; + if (m_extrude_face_src >= 0) { + // Onshape face-extrude: the picked solid face is the profile (no sketch wire). + idx = m_doc.add_extrude_face(m_extrude_face_src, m_distance->GetValue(), false, mode, name); + m_extrude_face_src = -1; // consume the face-profile selection + } else if (extrude_uses_loop()) { + // Extrude just the selected loop (its entity subset), leaving the source sketch's + // other loops intact and still selectable. + idx = m_doc.add_extrude_entities(m_viewport->selected_loop_entities(), + m_doc.features[m_extrude_sketch_ref].plane, + m_distance->GetValue(), false, mode, name); + m_sel_sketch_region = -1; // consume the loop selection + m_viewport->clear_loop_pick(); // drop the now-stale loop highlight + } else { + idx = m_doc.add_extrude(m_extrude_sketch_ref, m_distance->GetValue(), false, mode, name); + } + // Carry the Onshape end-condition / taper / flip / up-to-face onto the new feature so the + // committed solid matches the preview (build_candidate sets the same fields). + if (idx >= 0 && idx < int(m_doc.features.size())) { + CadFeature& f = m_doc.features[idx]; + f.extrude_end = static_cast(m_extrude_end->GetSelection()); + f.distance2 = m_distance2->GetValue(); + f.taper_deg = m_taper->GetValue(); + f.flip = m_flip->GetValue(); + f.up_to_face = (f.extrude_end == ExtrudeEnd::UpToFace) ? m_sel_solid_face : -1; + f.target_body = m_sel_solid_body; // multi-body: act on the picked body (-1 = last) + // On-face Text/SVG remembers its host body even after the face pick was cleared by + // the placement recompute, so the engraving Cut hits the right solid. + if (m_extrude_sketch_ref >= 0 && m_extrude_sketch_ref < int(m_doc.features.size()) + && m_doc.features[m_extrude_sketch_ref].import_on_face) + f.target_body = m_doc.features[m_extrude_sketch_ref].import_face_body; + } + if (!m_doc.recompute()) + m_status->SetLabel(_L("Recompute error: ") + wxString::FromUTF8(m_doc.error)); + else + set_status_ok(); + refresh_tree(); +} + +void DesignPanel::on_add_dressup() +{ + if (m_doc.body.IsNull()) { + m_status->SetLabel(_L("Add a solid (sketch + extrude) first")); + return; + } + FaceGroup fg = static_cast(m_face_group->GetSelection()); // Top=0..All=3 + double sz = m_dressup_size->GetValue(); + bool fillet = (m_dressup_type->GetSelection() == 0); + + m_feature_counter++; + // A click-selected solid edge targets THAT edge; otherwise dress the whole face-group. + int didx = -1; + if (m_sel_solid_edge >= 0) { + if (fillet) + didx = m_doc.add_fillet(sz, m_sel_solid_edge, "Fillet" + std::to_string(m_feature_counter)); + else + didx = m_doc.add_chamfer(sz, m_sel_solid_edge, "Chamfer" + std::to_string(m_feature_counter)); + } else if (fillet) + didx = m_doc.add_fillet(sz, fg, "Fillet" + std::to_string(m_feature_counter)); + else + didx = m_doc.add_chamfer(sz, fg, "Chamfer" + std::to_string(m_feature_counter)); + // Dress the picked body (its face/edge ids are body-local). -1 = last body. + if (didx >= 0 && didx < int(m_doc.features.size())) + m_doc.features[didx].target_body = m_sel_solid_body; + + if (!m_doc.recompute()) + m_status->SetLabel(_L("Recompute error: ") + wxString::FromUTF8(m_doc.error)); + else + set_status_ok(); + + refresh_tree(); +} + +SketchPlane DesignPanel::hole_plane() const +{ + return m_hole_on_face ? m_hole_face_plane + : plane_from_index(m_hole_plane->GetSelection()); +} + +void DesignPanel::on_add_hole() +{ + if (m_doc.body.IsNull()) { + m_status->SetLabel(_L("Add a solid (sketch + extrude) first")); + return; + } + SketchPlane plane = hole_plane(); + double dia = m_hole_diameter->GetValue(); + double depth = m_hole_depth->GetValue(); + bool through = m_hole_through->GetValue(); + double px = m_hole_x->GetValue(); + double py = m_hole_y->GetValue(); + + m_feature_counter++; + const int hidx = m_doc.add_hole(dia, depth, through, px, py, plane, + "Hole" + std::to_string(m_feature_counter)); + // On-face holes drill the body the face belongs to (even after the pick was cleared). + if (m_hole_on_face && hidx >= 0 && hidx < int(m_doc.features.size())) + m_doc.features[hidx].target_body = m_hole_face_body; + + if (!m_doc.recompute()) + m_status->SetLabel(_L("Recompute error: ") + wxString::FromUTF8(m_doc.error)); + else + set_status_ok(); + + refresh_tree(); +} + +SketchPlane DesignPanel::thread_plane() const +{ + return m_thread_on_face ? m_thread_face_plane + : plane_from_index(m_thread_plane->GetSelection()); +} + +void DesignPanel::apply_thread_standard() +{ + if (!m_thread_std) return; + const int sel = m_thread_std->GetSelection(); + if (sel <= 0) return; // 0 = "Custom" → leave the manual spins untouched + + const ThreadSpec* s = find_thread_standard( + m_thread_std->GetString(sel).utf8_string()); + if (!s) return; + + // Pitch and depth are the defining "measures" of the standard — always apply. + if (m_thread_pitch) m_thread_pitch->SetValue(s->pitch_mm); + if (m_thread_depth) m_thread_depth->SetValue(s->thread_depth_mm()); + + // Nominal radius: external rod = major radius; internal tapped bore = minor + // (tap-drill) radius. On a picked cylindrical face the radius comes from the + // real geometry, so don't override it there. + if (!m_thread_on_face && m_thread_radius) { + const bool internal = m_thread_internal && m_thread_internal->GetValue(); + const double d = internal ? s->minor_diameter_mm() : s->major_diameter_mm; + m_thread_radius->SetValue(0.5 * d); + } + + if (m_status) + m_status->SetLabel(wxString::Format(_L("Thread standard: %s (pitch %.3g mm)"), + m_thread_std->GetString(sel), s->pitch_mm)); +} + +void DesignPanel::on_add_thread() +{ + bool internal = m_thread_internal->GetValue(); + if (internal && m_doc.body.IsNull()) { + m_status->SetLabel(_L("Internal thread needs a body — add a solid first")); + return; + } + SketchPlane plane = thread_plane(); + + m_feature_counter++; + const int tidx = m_doc.add_thread(m_thread_radius->GetValue(), m_thread_pitch->GetValue(), + m_thread_height->GetValue(), m_thread_depth->GetValue(), + internal, m_thread_x->GetValue(), m_thread_y->GetValue(), + plane, "Thread" + std::to_string(m_feature_counter)); + // On-surface internal thread taps the body the cylindrical face belongs to. + if (m_thread_on_face && tidx >= 0 && tidx < int(m_doc.features.size())) + m_doc.features[tidx].target_body = m_thread_face_body; + + if (!m_doc.recompute()) + m_status->SetLabel(_L("Recompute error: ") + wxString::FromUTF8(m_doc.error)); + else + set_status_ok(); + + refresh_tree(); +} + +void DesignPanel::on_add_revolve() +{ + if (m_revolve_sketch_ref < 0 || m_revolve_sketch_ref >= int(m_doc.features.size())) { + m_status->SetLabel(_L("Pick a sketch profile to revolve first")); + return; + } + const BooleanMode mode = static_cast(m_revolve_mode->GetSelection()); + if (mode != BooleanMode::New && m_doc.body.IsNull()) { + m_status->SetLabel(_L("Add/Cut/Intersect revolve needs an existing body")); + return; + } + m_feature_counter++; + m_doc.add_revolve(m_revolve_sketch_ref, m_revolve_angle->GetValue(), + m_revolve_axis->GetSelection(), m_revolve_flip->GetValue(), + mode, "Revolve" + std::to_string(m_feature_counter)); + + if (!m_doc.recompute()) + m_status->SetLabel(_L("Recompute error: ") + wxString::FromUTF8(m_doc.error)); + else + set_status_ok(); + + refresh_tree(); +} + +void DesignPanel::on_add_sweep() +{ + if (m_sweep_profile_ref < 0 || m_sweep_profile_ref >= int(m_doc.features.size())) { + m_status->SetLabel(_L("Pick a profile sketch to sweep first")); + return; + } + const int sel = m_sweep_path->GetSelection(); + const int path_ref = (sel != wxNOT_FOUND) + ? int(reinterpret_cast(m_sweep_path->GetClientData(sel))) : -1; + if (path_ref < 0) { + m_status->SetLabel(_L("Pick a path sketch for the sweep")); + return; + } + const BooleanMode mode = static_cast(m_sweep_mode->GetSelection()); + if (mode != BooleanMode::New && m_doc.body.IsNull()) { + m_status->SetLabel(_L("Add/Cut/Intersect sweep needs an existing body")); + return; + } + m_feature_counter++; + m_doc.add_sweep(m_sweep_profile_ref, path_ref, mode, + "Sweep" + std::to_string(m_feature_counter)); + + if (!m_doc.recompute()) + m_status->SetLabel(_L("Recompute error: ") + wxString::FromUTF8(m_doc.error)); + else + set_status_ok(); + + refresh_tree(); +} + +void DesignPanel::on_add_loft() +{ + // Collect the checked profile sketches in list (recipe) order. + std::vector refs; + for (unsigned i = 0; i < m_loft_list->GetCount(); ++i) + if (m_loft_list->IsChecked(i) && i < m_loft_sketch_idx.size()) + refs.push_back(m_loft_sketch_idx[i]); + if (refs.size() < 2) { + m_status->SetLabel(_L("Check at least two profile sketches to loft")); + return; + } + const BooleanMode mode = static_cast(m_loft_mode->GetSelection()); + if (mode != BooleanMode::New && m_doc.body.IsNull()) { + m_status->SetLabel(_L("Add/Cut/Intersect loft needs an existing body")); + return; + } + m_feature_counter++; + m_doc.add_loft(refs, m_loft_ruled->GetValue(), mode, + "Loft" + std::to_string(m_feature_counter)); + + if (!m_doc.recompute()) + m_status->SetLabel(_L("Recompute error: ") + wxString::FromUTF8(m_doc.error)); + else + set_status_ok(); + + refresh_tree(); +} + +void DesignPanel::on_add_pattern() +{ + if (m_doc.bodies.empty()) { + m_status->SetLabel(_L("Pattern needs a body — add a solid first")); + return; + } + const bool circular = (m_pattern_type->GetSelection() == 1); + const int target = (m_sel_solid_body >= 0 && m_sel_solid_body < int(m_doc.bodies.size())) + ? m_sel_solid_body : -1; + m_feature_counter++; + m_doc.add_pattern(circular, int(m_pattern_count->GetValue()), + m_pattern_spacing->GetValue(), m_pattern_dir->GetSelection(), + m_pattern_angle->GetValue(), target, + "Pattern" + std::to_string(m_feature_counter)); + + if (!m_doc.recompute()) + m_status->SetLabel(_L("Recompute error: ") + wxString::FromUTF8(m_doc.error)); + else + set_status_ok(); + + refresh_tree(); +} + +void DesignPanel::populate_body_choices() +{ + auto fill = [&](wxChoice* c, int def) { + if (!c) return; + c->Clear(); + for (size_t i = 0; i < m_doc.bodies.size(); ++i) { + const std::string& n = m_doc.bodies[i].name; + c->Append(n.empty() ? wxString::Format(_L("Body %zu"), i + 1) : wxString::FromUTF8(n)); + } + if (c->GetCount() > 0) + c->SetSelection(std::min(def, int(c->GetCount()) - 1)); // selection index == body index + }; + fill(m_bool_target, 0); + fill(m_bool_tool, 1); // default: combine body 0 (target) with body 1 (tool) + fill(m_cut_target, 0); // Cut tool: default to the first body +} + +void DesignPanel::on_add_boolean() +{ + if (m_doc.bodies.size() < 2) { + m_status->SetLabel(_L("Boolean needs two bodies")); + return; + } + const int sel = m_bool_op->GetSelection(); + const BooleanMode op = (sel == 1) ? BooleanMode::Cut + : (sel == 2) ? BooleanMode::Intersect + : BooleanMode::Add; // 0 = Union + m_feature_counter++; + m_doc.add_boolean(op, m_bool_target->GetSelection(), m_bool_tool->GetSelection(), + m_bool_keep->GetValue(), m_bool_tol->GetValue(), -1, -1, + "Boolean" + std::to_string(m_feature_counter)); + if (!m_doc.recompute()) + m_status->SetLabel(_L("Recompute error: ") + wxString::FromUTF8(m_doc.error)); + else + set_status_ok(); + refresh_tree(); +} + +void DesignPanel::on_add_cut() +{ + if (m_doc.bodies.empty()) { + m_status->SetLabel(_L("Cut needs a body")); + return; + } + m_feature_counter++; + m_doc.add_cut(plane_from_choice(m_cut_plane->GetSelection()), m_cut_offset->GetValue(), + /*flip*/ false, /*keep_upper*/ true, /*keep_lower*/ true, + m_cut_target->GetSelection(), "Cut" + std::to_string(m_feature_counter)); + if (!m_doc.recompute()) + m_status->SetLabel(_L("Recompute error: ") + wxString::FromUTF8(m_doc.error)); + else + set_status_ok(); + refresh_tree(); +} + +void DesignPanel::populate_plane_choices(wxChoice* c) const +{ + if (!c) return; + const int keep = c->GetSelection(); + c->Clear(); + c->Append("XY"); c->Append("XZ"); c->Append("YZ"); + for (const auto& dp : m_doc.resolve_datum_planes()) + c->Append(wxString::FromUTF8(dp.first)); + c->SetSelection((keep >= 0 && keep < int(c->GetCount())) ? keep : 0); +} + +SketchPlane DesignPanel::plane_from_choice(int row) const +{ + if (row < 3) return plane_from_index(row); // 0=XY,1=XZ,2=YZ + auto datums = m_doc.resolve_datum_planes(); + const int di = row - 3; + return (di >= 0 && di < int(datums.size())) ? datums[di].second : SketchPlane::XY(); +} + +void DesignPanel::on_add_plane() +{ + m_feature_counter++; + m_doc.add_plane(m_plane_base->GetSelection(), m_plane_offset->GetValue(), + m_plane_tilt->GetValue(), m_plane_tilt_axis->GetSelection(), + "Plane" + std::to_string(m_feature_counter)); + m_doc.recompute(); // datum-only docs yield no body; that is expected/benign + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(_L("Plane added — pick it as a sketch plane")); + refresh_tree(); +} + +void DesignPanel::on_add_shell() +{ + if (m_doc.body.IsNull()) { + m_status->SetLabel(_L("Shell needs a body — add a solid first")); + return; + } + const int face = (m_sel_solid_face >= 0) ? m_sel_solid_face : -1; + + m_feature_counter++; + m_doc.add_shell(m_shell_thickness->GetValue(), face, m_sel_solid_body, + "Shell" + std::to_string(m_feature_counter)); + + if (!m_doc.recompute()) + m_status->SetLabel(_L("Recompute error: ") + wxString::FromUTF8(m_doc.error)); + else + set_status_ok(); + + refresh_tree(); +} + +void DesignPanel::on_add_draft() +{ + if (m_doc.body.IsNull()) { + m_status->SetLabel(_L("Draft needs a body — add a solid first")); + return; + } + if (m_sel_solid_face < 0) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Draft needs a picked face — click a side face first")); + m_status->Refresh(); + return; + } + + m_feature_counter++; + m_doc.add_draft(m_draft_angle->GetValue(), m_sel_solid_face, m_sel_solid_body, + "Draft" + std::to_string(m_feature_counter)); + + if (!m_doc.recompute()) + m_status->SetLabel(_L("Recompute error: ") + wxString::FromUTF8(m_doc.error)); + else + set_status_ok(); + + refresh_tree(); +} + +int DesignPanel::tree_icon_for(CadFeatureType t) +{ + switch (t) { + case CadFeatureType::Sketch: return 0; + case CadFeatureType::Extrude: return 1; + case CadFeatureType::Fillet: + case CadFeatureType::Chamfer: return 2; + case CadFeatureType::Hole: return 3; + case CadFeatureType::Thread: return 4; + case CadFeatureType::Shell: return 5; + case CadFeatureType::Revolve: return 1; + case CadFeatureType::Sweep: return 1; + case CadFeatureType::Pattern: return 1; + case CadFeatureType::Plane: return 0; // datum plane: sketch-family icon + case CadFeatureType::Loft: return 1; + case CadFeatureType::Draft: return 5; // dressup-family icon + case CadFeatureType::Import: return 1; // imported solid: solid-family icon + case CadFeatureType::Boolean: return 1; // body-body combine: solid-family icon + case CadFeatureType::Cut: return 1; // plane split: solid-family icon + } + return 0; +} + +void DesignPanel::on_tab_shown() +{ + if (m_viewport) m_viewport->refresh_bed(); +} + +void DesignPanel::refresh_tree() +{ + // Preserve the selected row across the rebuild — wxTreeCtrl::DeleteAllItems + // drops the selection, which made every edit/add feel like it "lost" the + // selection (and broke Edit/Move/Delete on the just-touched feature). + const int keep = tree_selection(); + + m_tree->DeleteAllItems(); + m_tree_items.clear(); + m_tree_body_items.clear(); + wxTreeItemId root = m_tree->AddRoot("root"); + // Datum/reference planes carry no solid; feed them to the viewport so they render as + // translucent rectangles (otherwise a Plane feature is invisible in the canvas). + if (m_viewport) { + std::vector dplanes; + for (const auto& dp : m_doc.resolve_datum_planes()) dplanes.push_back(dp.second); + m_viewport->set_datum_planes(std::move(dplanes)); + } + for (const auto& f : m_doc.features) { + const int img = tree_icon_for(f.type); + wxTreeItemId id = m_tree->AppendItem(root, wxString::FromUTF8(f.name), img, img); + // Hidden (disabled) features are greyed so the show/hide state reads at a glance. + m_tree->SetItemTextColour(id, f.enabled ? dp_item_text() + : dp_item_dim()); + m_tree_items.push_back(id); + } + // Parts list: a Bodies group listing each independent solid. Shown only with >1 body + // (a single body is just "the solid"); selecting a row highlights it + targets it. + if (m_doc.bodies.size() > 1) { + sync_body_visible(); // keep flags parallel before reading them for the row colour + wxTreeItemId grp = m_tree->AppendItem(root, _L("Bodies")); + m_tree->SetItemTextColour(grp, dp_sec_text()); + for (size_t b = 0; b < m_doc.bodies.size(); ++b) { + // Label "Body N" (matches the viewport/status); the originating feature name is + // kept on the CadBody for tooltips/debug but isn't shown as the row label. + const bool vis = b >= m_body_visible.size() || m_body_visible[b]; + wxTreeItemId id = m_tree->AppendItem(grp, wxString::Format(_L("Body %zu"), b + 1)); + // Hidden bodies are greyed so the show/hide state reads at a glance (eye toggle). + m_tree->SetItemTextColour(id, vis ? dp_item_text() : dp_item_dim()); + m_tree_body_items.push_back(id); + } + m_tree->Expand(grp); + } + if (keep >= 0 && keep < int(m_tree_items.size())) + m_tree->SelectItem(m_tree_items[keep]); +} + +int DesignPanel::tree_body_selection() const +{ + const wxTreeItemId sel = m_tree->GetSelection(); + if (!sel.IsOk()) return -1; + for (size_t i = 0; i < m_tree_body_items.size(); ++i) + if (m_tree_body_items[i] == sel) return int(i); + return -1; +} + +void DesignPanel::sync_body_visible() +{ + // Keep the visibility vector parallel to bodies; newly-created bodies default visible. + // Bodies are appended in feature order, so existing indices keep their flag on resize. + m_body_visible.resize(m_doc.bodies.size(), true); +} + +void DesignPanel::sync_body_xform() +{ + // Parallel to bodies; new bodies default to identity (no move). Stable on resize. + m_body_xform.resize(m_doc.bodies.size(), Transform3d::Identity()); +} + +// Build the display + pick meshes with each body's Move transform applied. The pick mesh is +// re-merged from the transformed per-body meshes IN THE SAME body order as tessellate_bodies, +// so display_tri_face/display_tri_body stay aligned. m_disp_pick_mesh keeps a stable address — +// the tool holds a pointer to it, so an in-place rebuild updates picking without re-pointing. +void DesignPanel::rebuild_disp_meshes() +{ + sync_body_visible(); + sync_body_xform(); + const std::vector& src = m_doc.display_body_meshes; + + bool any = false; + for (const Transform3d& t : m_body_xform) + if (!t.isApprox(Transform3d::Identity())) { any = true; break; } + + if (!any) { // no body moved: identical to the untransformed meshes + m_disp_body_meshes = src; + m_disp_pick_mesh = m_doc.display_mesh; + return; + } + + m_disp_body_meshes.clear(); + m_disp_body_meshes.reserve(src.size()); + m_disp_pick_mesh = TriangleMesh{}; + for (size_t b = 0; b < src.size(); ++b) { + TriangleMesh m = src[b]; + if (b < m_body_xform.size()) m.transform(m_body_xform[b]); + m_disp_pick_mesh.merge(m); // same order as tessellate_bodies -> tri_* stay aligned + m_disp_body_meshes.push_back(std::move(m)); + } +} + +void DesignPanel::feed_bodies() +{ + // Rebuild the transformed meshes first so every display-refresh path (recompute, tint, + // visibility, live move) shows the bodies at their current Move offsets. The solid-pick + // keeps a STABLE pointer to m_disp_pick_mesh / m_body_visible / m_body_xform (rebuilt in + // place), so it needs no re-call here — the whole/face/edge selection survives a move drag. + if (m_viewport == nullptr) return; + rebuild_disp_meshes(); + m_viewport->set_bodies(m_disp_body_meshes, m_body_visible); +} + +void DesignPanel::on_move_body() +{ + const int b = m_sel_solid_body; + if (m_viewport == nullptr || b < 0 || b >= int(m_doc.display_body_meshes.size())) return; + sync_body_xform(); + // Delta gizmo: pivot at the body's CURRENT world centroid; the tool composes the drag deltas + // onto its current pose, so move + rotate both work (incl. on an already place-on-face'd body). + const Transform3d base = m_body_xform[b]; + const Vec3d pivot = base * m_doc.display_body_meshes[b].bounding_box().center(); + m_viewport->begin_move_body(b, pivot, base); + m_move_body = b; // for the action bar: Cancel reverts to this pose + m_move_prev = base; + update_action_bar(); // surface the unified ✓/✗ while moving + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(_L("Drag the arrows to move, the rings to rotate — then Confirm (Esc cancels)")); + m_status->Refresh(); +} + +// Color tool: open a colour picker on the selected body and store a per-body display-colour +// override on its CadBody. The override is carried across recompute() by body index and is +// read back by DesignCanvas::body_color()/reload(), so the body keeps its colour through edits. +void DesignPanel::on_set_body_color() +{ + // Same body-selection source Move / visibility use: the Parts-list row first, falling + // back to the in-canvas picked solid so either selection path works. + int b = tree_body_selection(); + if (b < 0) b = m_sel_solid_body; + if (b < 0 || b >= int(m_doc.bodies.size())) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Select a body first")); + m_status->Refresh(); + return; + } + + // Seed the picker with the body's current effective colour (override or auto palette). + const ColorRGBA cur = (m_viewport != nullptr) ? m_viewport->body_color(b) + : m_doc.bodies[b].color; + wxColourData data; + data.SetColour(wxColour(cur.r_uchar(), cur.g_uchar(), cur.b_uchar())); + wxColourDialog dlg(this, &data); + if (dlg.ShowModal() != wxID_OK) return; + + const wxColour picked = dlg.GetColourData().GetColour(); + m_doc.bodies[b].has_color = true; + m_doc.bodies[b].color = ColorRGBA((unsigned char)picked.Red(), (unsigned char)picked.Green(), + (unsigned char)picked.Blue(), (unsigned char)255); + feed_bodies(); // same refresh path the visibility toggle uses → viewport updates immediately + + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(wxString::Format(_L("Body %d colour set"), b + 1)); + m_status->Refresh(); +} + +// Prepare's "Place on Face" (F), ported to Design. Pick a body face, then this rotates the +// body so that face's outward normal points straight down (-Z) and drops it onto the bed — +// Orca's exact math (Selection::flattening_rotate). Writes the per-body display transform +// m_body_xform (baked into the mesh at Commit), like the Move gizmo; no shape mutation. +// Returns false (with a hint) when no body face is selected, so the F key can fall through. +bool DesignPanel::place_on_face() +{ + const int b = m_sel_solid_body; + if (b < 0 || b >= int(m_doc.bodies.size()) || m_sel_solid_face < 0 + || b >= int(m_doc.display_body_meshes.size())) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Pick a body face first (click a solid, then click again to a face), then press F")); + m_status->Refresh(); + return false; + } + const TopoDS_Face face = GeometryEngine::face_by_index(m_doc.bodies[b].shape, m_sel_solid_face); + if (face.IsNull()) return false; + sync_body_xform(); + const Transform3d old_x = m_body_xform[b]; + // Outward face normal in the body's CURRENT displayed orientation. + const Vec3d n = (old_x.linear() * GeometryEngine::face_normal_world(face)).normalized(); + if (!n.allFinite() || n.norm() < 0.5) return false; + // Align that normal with the down vector (-Z): the face ends up on the bed. + const Transform3d R(Eigen::Quaterniond().setFromTwoVectors(n, -Vec3d::UnitZ())); + // Rotate about the body's current world centroid so it spins in place, not about the origin. + const Vec3d c = old_x * m_doc.display_body_meshes[b].bounding_box().center(); + Transform3d x = Eigen::Translation3d(c) * R * Eigen::Translation3d(-c) * old_x; + // Drop the re-oriented body so its lowest point sits on the bed (min Z -> 0). + TriangleMesh probe = m_doc.display_body_meshes[b]; + probe.transform(x); + x = Transform3d(Eigen::Translation3d(0.0, 0.0, -probe.bounding_box().min.z())) * x; + m_body_xform[b] = x; + set_status_ok(); // rebuild display/pick meshes, re-point picking; resets face selection + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(_L("Placed on face — body laid flat on the bed")); + m_status->Refresh(); + return true; +} + +int DesignPanel::tree_selection() const +{ + const wxTreeItemId sel = m_tree->GetSelection(); + if (!sel.IsOk()) return wxNOT_FOUND; + for (size_t i = 0; i < m_tree_items.size(); ++i) + if (m_tree_items[i] == sel) return int(i); + return wxNOT_FOUND; +} + +void DesignPanel::set_tree_selection(int row) +{ + if (row >= 0 && row < int(m_tree_items.size())) + m_tree->SelectItem(m_tree_items[row]); +} + +void DesignPanel::after_tree_edit(bool ok) +{ + update_undo_redo_buttons(); // every commit/undo/redo funnels through here -> refresh greying + refresh_tree(); + if (!ok) { + // The edit was rolled back (recompute failed); the body is unchanged. + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Edit rejected: ") + wxString::FromUTF8(m_doc.error)); + m_status->Refresh(); + return; + } + m_status->SetForegroundColour(wxNullColour); + if (m_doc.display_mesh.its.indices.empty()) { + if (m_viewport != nullptr) m_viewport->clear_mesh(); + sync_sketch_display(); // empty body: show any un-consumed committed sketch + m_status->SetLabel(wxString()); + } else { + set_status_ok(); + } + m_status->Refresh(); +} + +void DesignPanel::on_delete_feature() +{ + int sel = tree_selection(); + if (sel == wxNOT_FOUND) { + m_status->SetLabel(_L("Select a feature in the tree first")); + m_status->Refresh(); + return; + } + m_doc.checkpoint(); // undo boundary: deleting a feature + after_tree_edit(m_doc.remove_feature(sel)); +} + +void DesignPanel::on_toggle_visibility() +{ + // A selected Body row toggles that body's visibility (per-body show/hide). The solid + // stays in the document; only its GLVolume + pickability flip. Falls through to the + // feature-level toggle below when a feature row (not a body row) is selected. + const int bsel = tree_body_selection(); + if (bsel >= 0) { + sync_body_visible(); + if (bsel < int(m_body_visible.size())) { + const bool now_visible = !m_body_visible[bsel]; + m_body_visible[bsel] = now_visible; + if (m_viewport != nullptr) { + feed_bodies(); // flips is_active; m_solid_visible is a stable pointer (live) + m_viewport->set_solid_pick(&m_doc.bodies, &m_disp_pick_mesh, + &m_doc.display_tri_face, &m_doc.display_tri_body, + &m_body_visible, &m_body_xform); + } + refresh_tree(); + if (bsel < int(m_tree_body_items.size())) // keep the row selected for repeat toggles + m_tree->SelectItem(m_tree_body_items[bsel]); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(wxString::Format(now_visible ? _L("Body %d shown") + : _L("Body %d hidden"), bsel + 1)); + m_status->Refresh(); + } + return; + } + + int sel = tree_selection(); + if (sel == wxNOT_FOUND || sel >= int(m_doc.features.size())) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Select a feature in the tree first")); + m_status->Refresh(); + return; + } + const bool shown = !m_doc.features[sel].enabled; + m_doc.features[sel].enabled = shown; + + // recompute() reports an all-hidden / sketch-only document as false (no + // solid to build), but that is a VALID state for hide — so clear the body + // explicitly instead of letting after_tree_edit treat it as a rejected edit + // (which would skip the overlay refresh, leaving hidden art on screen). + if (!m_doc.recompute()) { + m_doc.body = TopoDS_Shape(); + m_doc.display_mesh = TriangleMesh{}; + m_doc.error.clear(); + } + refresh_tree(); // greys the row + set_tree_selection(sel); // keep the toggled feature selected + if (m_viewport != nullptr) { + if (m_doc.display_mesh.its.indices.empty()) m_viewport->clear_mesh(); + else feed_bodies(); + } + sync_sketch_display(); // skips the hidden sketch + direct-renders + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(shown ? _L("Feature shown") : _L("Feature hidden")); + m_status->Refresh(); +} + +void DesignPanel::on_move_feature(int delta) +{ + int sel = tree_selection(); + if (sel == wxNOT_FOUND) { + m_status->SetLabel(_L("Select a feature in the tree first")); + m_status->Refresh(); + return; + } + int target = sel + delta; + if (target < 0 || target >= int(m_doc.features.size())) + return; // already at the end + m_doc.checkpoint(); // undo boundary: reordering a feature + if (m_doc.move_feature(sel, delta)) { + after_tree_edit(true); + set_tree_selection(target); // keep the moved feature selected + } else { + after_tree_edit(false); + } +} + +// Commit the live sketch in place, then enter Constrain mode on the just-committed sketch. +// One-click bridge from the SKETCH toolbar: removes the "Finish -> find in tree -> select -> +// Constrain" friction, so the constraint palette + Trim/Extend are reachable mid-sketch. +bool DesignPanel::enter_constrain_inline() +{ + if (m_viewport && m_viewport->is_sketching()) + m_viewport->finish_sketch(); // synchronous: packages live entities+constraints -> Sketch + const int sk = resolve_extrude_sketch(); // last/selected Sketch feature + if (sk < 0) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Draw a sketch first, then Constrain")); + m_status->Refresh(); + return false; + } + set_tree_selection(sk); // tree drives on_begin_constrain / the constraint manager + on_begin_constrain(sk); + const bool entered = m_viewport && + (m_viewport->is_constraining() || m_viewport->is_constraining_entities()); + if (entered) set_ui_mode(UiMode::Constrain); + return entered; +} + +void DesignPanel::on_begin_constrain(int sel_override) +{ + int sel = (sel_override >= 0) ? sel_override : tree_selection(); + if (sel == wxNOT_FOUND || sel >= int(m_doc.features.size())) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Select a sketch in the tree first")); + m_status->Refresh(); + return; + } + CadFeature& f = m_doc.features[sel]; + if (f.type != CadFeatureType::Sketch) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Selected feature is not a sketch")); + m_status->Refresh(); + return; + } + + // Entity sketches (Fase 4.2): pick Line entities; constraints solve against + // entity endpoints in the kernel. + if (!f.entities.empty()) { + m_constrain_feat = sel; + if (m_viewport) m_viewport->begin_constrain_entities(f.entities, f.plane); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(_L("Pick 1-2 lines, then a constraint; right-click exits")); + m_status->Refresh(); + return; + } + + // Legacy profile path (Fase 3). + if (f.profile.points.size() < 3) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Selected feature is not a sketch")); + m_status->Refresh(); + return; + } + m_constrain_feat = sel; + // Anchor the first profile point so H/V constraints don't let the sketch + // float freely; fix_point captures the point's current position in the solver. + if (f.constraints.empty()) + f.constraints.push_back(SketchConstraintDef{SketchConstraintType::Fix, 0, -1, -1, -1, 0.0}); + if (m_viewport) m_viewport->begin_constrain(f.profile, f.plane); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(_L("Pick 1-2 entities, then a constraint or dimension; right-click exits")); + m_status->Refresh(); +} + +void DesignPanel::apply_entity_constraint(SketchConstraintType type) +{ + using R = SketchPointRole; + using T = SketchConstraintType; + int e0 = -1, e1 = -1; + m_viewport->selected_constrain_entities(e0, e1); + + auto fail = [this](const wxString& msg) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(msg); + m_status->Refresh(); + }; + + CadFeature& feat = m_doc.features[m_constrain_feat]; + const bool needs_two = (type == T::Parallel || type == T::Perpendicular || + type == T::EqualLength || type == T::Coincident || + type == T::Concentric || type == T::Tangent || + type == T::Angle || type == T::Midpoint || + type == T::Symmetric); + if (e0 < 0 || e0 >= int(feat.entities.size()) || + (needs_two && (e1 < 0 || e1 >= int(feat.entities.size())))) { + fail(needs_two ? _L("Pick two entities first") : _L("Pick an entity first")); + return; + } + auto is_round = [](const SketchEntity& e) { + return e.type == SketchEntity::Type::Circle || e.type == SketchEntity::Type::Arc; }; + + SketchEntityConstraintDef def; + def.type = type; + def.value = 0.0; + switch (type) { + case T::Horizontal: + case T::Vertical: + // One line: level/plumb its own two endpoints. + def.ea = e0; def.ra = R::P0; + def.eb = e0; def.rb = R::P1; + break; + case T::Parallel: + case T::Perpendicular: + case T::EqualLength: + def.ea = e0; def.eb = e1; // two whole line segments (roles unused) + break; + case T::Coincident: { + // Join the closest endpoint pair of the two picked lines. + const SketchEntity& A = feat.entities[e0]; + const SketchEntity& B = feat.entities[e1]; + const std::pair aps[2] = {{R::P0, A.p0}, {R::P1, A.p1}}; + const std::pair bps[2] = {{R::P0, B.p0}, {R::P1, B.p1}}; + R ra = R::P1, rb = R::P0; + double best = 1e30; + for (const auto& ap : aps) + for (const auto& bp : bps) { + const double d = (ap.second - bp.second).squaredNorm(); + if (d < best) { best = d; ra = ap.first; rb = bp.first; } + } + def.ea = e0; def.ra = ra; def.eb = e1; def.rb = rb; + break; + } + case T::Concentric: { + // Two circles/arcs: make their centres coincide. + if (!is_round(feat.entities[e0]) || !is_round(feat.entities[e1])) { + fail(_L("Concentric needs two circles or arcs")); return; + } + def.ea = e0; def.ra = R::Center; def.eb = e1; def.rb = R::Center; + break; + } + case T::Tangent: { + // line+round or round+round; the kernel detects the entity types. + const bool ok = (is_round(feat.entities[e0]) && feat.entities[e1].type == SketchEntity::Type::Line) || + (is_round(feat.entities[e1]) && feat.entities[e0].type == SketchEntity::Type::Line) || + (is_round(feat.entities[e0]) && is_round(feat.entities[e1])); + if (!ok) { fail(_L("Tangent needs a line and a circle/arc, or two circles/arcs")); return; } + def.ea = e0; def.eb = e1; + break; + } + case T::Angle: { + // Angle between two line segments; typed in-canvas at the cursor (no card), + // pre-filled with the current angle between the picked lines. + const int a = e0, b = e1; + const Vec2d da = feat.entities[a].p1 - feat.entities[a].p0; + const Vec2d db = feat.entities[b].p1 - feat.entities[b].p0; + double cur = 90.0; + const double na = da.norm(), nb = db.norm(); + if (na > 1e-9 && nb > 1e-9) { + const double c = std::max(-1.0, std::min(1.0, da.dot(db) / (na * nb))); + cur = std::acos(c) * 180.0 / M_PI; + } + m_viewport->open_inline_value(cur, [this, a, b](double deg) { + SketchEntityConstraintDef d; + d.type = T::Angle; d.ea = a; d.eb = b; + d.value = deg * M_PI / 180.0; + commit_entity_constraint(d); + }); + return; // deferred: commit runs on the typed value + } + case T::Midpoint: { + // One pick is a Point, the other a Line: the point is the line's midpoint. + const SketchEntity& A = feat.entities[e0]; + const SketchEntity& B = feat.entities[e1]; + int pt = -1, ln = -1; + if (A.type == SketchEntity::Type::Point && B.type == SketchEntity::Type::Line) { pt = e0; ln = e1; } + else if (B.type == SketchEntity::Type::Point && A.type == SketchEntity::Type::Line) { pt = e1; ln = e0; } + else { fail(_L("Midpoint needs a point and a line")); return; } + def.ea = pt; def.ra = R::P0; def.eb = ln; + break; + } + case T::Symmetric: { + // Two entities made symmetric about a third (axis) line. Picks: slot0=A, + // slot1=B, slot2=axis. Two Points -> one pair; two Lines -> endpoint pairs. + using ET = SketchEntity::Type; + const int axis = m_viewport->selected_constrain_axis(); + if (axis < 0 || axis >= int(feat.entities.size()) || + feat.entities[axis].type != ET::Line) { + fail(_L("Symmetric: pick two entities, then an axis line")); return; + } + const ET ta = feat.entities[e0].type, tb = feat.entities[e1].type; + std::vector defs; + auto mk = [&](R ra, R rb) { + SketchEntityConstraintDef d; + d.type = T::Symmetric; + d.ea = e0; d.ra = ra; d.eb = e1; d.rb = rb; d.ec = axis; + defs.push_back(d); + }; + if (ta == ET::Point && tb == ET::Point) { mk(R::P0, R::P0); } + else if (ta == ET::Line && tb == ET::Line) { mk(R::P0, R::P0); mk(R::P1, R::P1); } + else { fail(_L("Symmetric needs two points or two lines + an axis")); return; } + commit_entity_constraints(defs); + return; // multi-def commit done here + } + case T::Fix: { + // Anchor the picked entity's reference point to its current coordinate (the + // kernel pins it to a fixed reference). A single point — not both endpoints — + // so it composes with any existing Horizontal/Vertical/length constraint + // instead of duplicating it (pinning both endpoints of an already-horizontal + // line is redundant → over-constrained). Removes 2 DoF (the entity's position); + // combine with H/V + a dimension to reach fully constrained. + using ET = SketchEntity::Type; + const ET et = feat.entities[e0].type; + def.ea = e0; + def.ra = (et == ET::Circle || et == ET::Ellipse || + et == ET::Arc || et == ET::EllipseArc) ? R::Center : R::P0; + break; + } + case T::Radius: + case T::Diameter: { + const SketchEntity& A = feat.entities[e0]; + if (!is_round(A)) { fail(_L("Radius/Diameter needs a circle or arc")); return; } + const double cur = (type == T::Diameter) ? 2.0 * A.radius : A.radius; + const int a = e0; const T tt = type; + // Typed in-canvas at the cursor (no docked card), pre-filled with the current value. + m_viewport->open_inline_value(cur, [this, a, tt](double v) { + SketchEntityConstraintDef d; + d.type = tt; d.ea = a; d.ra = R::Center; d.value = v; + commit_entity_constraint(d); + }); + return; // deferred: commit runs on the typed value + } + default: + fail(_L("Unsupported constraint")); + return; + } + + commit_entity_constraint(def); +} + +void DesignPanel::commit_entity_constraint(const SketchEntityConstraintDef& def) +{ + commit_entity_constraints({ def }); +} + +void DesignPanel::commit_entity_constraints(const std::vector& defs) +{ + if (m_constrain_feat < 0 || m_constrain_feat >= int(m_doc.features.size()) || + !m_viewport || defs.empty()) + return; + CadFeature& feat = m_doc.features[m_constrain_feat]; + // solve_sketch_feature rewrites entity coords even on failure, so snapshot + // to roll back a rejected (over-constrained) addition cleanly. Multiple defs + // (Symmetric on two lines) must solve together, so push all then resize back. + const std::vector saved = feat.entities; + const size_t before = feat.entity_constraints.size(); + for (const auto& d : defs) feat.entity_constraints.push_back(d); + if (!m_doc.solve_sketch_feature(m_constrain_feat)) { + feat.entity_constraints.resize(before); + feat.entities = saved; + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Constraint rejected (over-constrained)")); + m_status->Refresh(); + return; + } + m_doc.recompute(); + m_viewport->update_constrain_entities(m_doc.features[m_constrain_feat].entities); + if (!m_doc.display_mesh.its.indices.empty()) + feed_bodies(); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(_L("Applied constraint")); + m_status->Refresh(); + + refresh_constrain_dof(); // P3 DoF readout for the Constrain path + rebuild_constraint_list(); // C3.4 manager: a row appeared +} + +// Re-derive the solve state of the constrained feature and mirror it into the same +// DoF readout the in-session path uses, so "✓ Fully constrained" is reachable here. +void DesignPanel::refresh_constrain_dof() +{ + if (!m_dof_status || m_constrain_feat < 0 || m_constrain_feat >= int(m_doc.features.size())) + return; + const CadFeature& feat = m_doc.features[m_constrain_feat]; + std::vector ents = feat.entities; // already solved; re-solve is a cheap no-op + const SketchSolveResult r = sketch_solve(ents, feat.entity_constraints); + if (!r.ok) { + m_dof_status->SetForegroundColour(wxColour(235, 80, 80)); + m_dof_status->SetLabel(_L("✗ Conflicting constraints")); + } else if (r.dof == 0) { + m_dof_status->SetForegroundColour(wxColour(80, 200, 110)); + m_dof_status->SetLabel(_L("✓ Fully constrained")); + } else if (r.dof > 0) { + m_dof_status->SetForegroundColour(dp_ctl_text()); + m_dof_status->SetLabel(wxString::Format(_L("%d degrees of freedom"), r.dof)); + } else { + m_dof_status->SetLabel(wxString()); + } + m_dof_status->Refresh(); + m_form->Layout(); +} + +// Human-readable label for a constraint row, e.g. "Coincident L0·P1 — L1·P0", +// "Horizontal L2", "Radius C3 = 7.00". Entities are tagged by type letter + index. +wxString DesignPanel::constraint_label(const SketchEntityConstraintDef& d) const +{ + using T = SketchConstraintType; + const CadFeature* feat = (m_constrain_feat >= 0 && m_constrain_feat < int(m_doc.features.size())) + ? &m_doc.features[m_constrain_feat] : nullptr; + auto tag = [&](int ei, SketchPointRole r) -> wxString { + if (ei < 0) return wxString(); + char c = 'E'; + if (feat && ei < int(feat->entities.size())) { + switch (feat->entities[ei].type) { + case SketchEntity::Type::Line: c = 'L'; break; + case SketchEntity::Type::Circle: c = 'C'; break; + case SketchEntity::Type::Arc: c = 'A'; break; + case SketchEntity::Type::Point: c = 'P'; break; + case SketchEntity::Type::Ellipse: + case SketchEntity::Type::EllipseArc: c = 'E'; break; + case SketchEntity::Type::BSpline: c = 'B'; break; + } + } + wxString s; s << wxUniChar(c) << ei; // avoid %c assert in Unicode build + if (r == SketchPointRole::P1) s += "·P1"; + else if (r == SketchPointRole::Center) s += "·Ctr"; + else if (r == SketchPointRole::P0) s += "·P0"; + return s; + }; + auto two = [&](const wxString& name) { + return d.eb >= 0 ? wxString::Format("%s %s — %s", name, tag(d.ea, d.ra), tag(d.eb, d.rb)) + : wxString::Format("%s %s", name, tag(d.ea, d.ra)); + }; + switch (d.type) { + case T::Fix: return wxString::Format(_L("Fix %s"), tag(d.ea, d.ra)); + case T::Coincident: return two(_L("Coincident")); + case T::Horizontal: return two(_L("Horizontal")); + case T::Vertical: return two(_L("Vertical")); + case T::Distance: return wxString::Format("%s = %s", two(_L("Distance")), en_format(d.value)); + case T::LockX: return wxString::Format(_L("Lock X %s"), tag(d.ea, d.ra)); + case T::LockY: return wxString::Format(_L("Lock Y %s"), tag(d.ea, d.ra)); + case T::EqualLength: return two(_L("Equal")); + case T::Parallel: return two(_L("Parallel")); + case T::Perpendicular: return two(_L("Perpendicular")); + case T::Concentric: return two(_L("Concentric")); + case T::Tangent: return two(_L("Tangent")); + case T::Midpoint: return two(_L("Midpoint")); + case T::Symmetric: return wxString::Format(_L("Symmetric %s — %s / %s"), + tag(d.ea, d.ra), tag(d.eb, d.rb), tag(d.ec, d.rc)); + case T::Angle: return wxString::Format("%s = %s°", two(_L("Angle")), en_format(d.value * 180.0 / M_PI, 1)); + case T::Radius: return wxString::Format("%s %s = %s", _L("Radius"), tag(d.ea, d.ra), en_format(d.value)); + case T::Diameter: return wxString::Format("%s %s = %s", _L("Diameter"), tag(d.ea, d.ra), en_format(d.value)); + case T::PointOnLine: return two(_L("On line")); + case T::PointOnObject: return two(_L("On edge")); + } + return _L("Constraint"); +} + +// Rebuild the constraint-row list from the constrained feature's entity_constraints. +void DesignPanel::rebuild_constraint_list() +{ + if (m_constraint_rows == nullptr || m_form == nullptr) + return; + m_constraint_rows->Clear(true /* delete windows */); + m_constraint_sel = -1; + + const bool active = (m_constrain_feat >= 0 && m_constrain_feat < int(m_doc.features.size())); + const std::vector empty; + const std::vector& cons = + active ? m_doc.features[m_constrain_feat].entity_constraints : empty; + + if (m_hdr_constraints) + m_hdr_constraints->SetLabel(wxString::Format(_L("Constraints (%d)"), int(cons.size()))); + + if (cons.empty()) { + auto* none = new wxStaticText(m_form, wxID_ANY, _L("No constraints yet")); + none->SetForegroundColour(dp_sec_text()); + m_constraint_rows->Add(none, 0, wxTOP, 4); + } + for (int i = 0; i < int(cons.size()); ++i) { + auto* row = new wxBoxSizer(wxHORIZONTAL); + // Delete button first (fixed left position, always visible — long labels can + // horizontally scroll but ✗ stays put and clickable). BMP-safe ✗ glyph. + auto* del = new wxButton(m_form, wxID_ANY, wxString::FromUTF8("✗"), + wxDefaultPosition, wxSize(26, -1)); + del->SetToolTip(_L("Delete constraint")); + del->Bind(wxEVT_BUTTON, [this, i](wxCommandEvent&) { delete_constraint(i); }); + // Clickable label: selecting it highlights the referenced entities. + auto* lbl = new wxButton(m_form, wxID_ANY, constraint_label(cons[i]), + wxDefaultPosition, wxDefaultSize, wxBU_LEFT | wxBORDER_NONE); + lbl->Bind(wxEVT_BUTTON, [this, i](wxCommandEvent&) { highlight_constraint_entities(i); }); + row->Add(del, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6); + row->Add(lbl, 1, wxALIGN_CENTER_VERTICAL); + m_constraint_rows->Add(row, 0, wxEXPAND | wxTOP, 2); + } + + // Feed the same list to the viewport for the on-sketch glyph badges (C3.4b). + if (m_viewport) + m_viewport->set_constraint_glyphs(cons); + + m_form->GetSizer()->Show(m_box_constraints, m_ui_mode == UiMode::Constrain, true); + m_form->Layout(); + m_form->FitInside(); +} + +// Push the entities referenced by constraint `idx` to the viewport as a yellow +// highlight (toggle off if the same row is clicked again). +void DesignPanel::highlight_constraint_entities(int idx) +{ + if (!m_viewport || m_constrain_feat < 0 || m_constrain_feat >= int(m_doc.features.size())) + return; + const auto& cons = m_doc.features[m_constrain_feat].entity_constraints; + if (idx < 0 || idx >= int(cons.size())) + return; + if (m_constraint_sel == idx) { // second click clears + m_constraint_sel = -1; + m_viewport->set_constraint_highlight({}); + return; + } + m_constraint_sel = idx; + const SketchEntityConstraintDef& d = cons[idx]; + std::vector ents; + for (int e : { d.ea, d.eb, d.ec }) + if (e >= 0) ents.push_back(e); + m_viewport->set_constraint_highlight(std::move(ents)); +} + +// Drop constraint `idx`, re-solve the feature, and refresh viewport + list + DoF. +void DesignPanel::delete_constraint(int idx) +{ + if (m_constrain_feat < 0 || m_constrain_feat >= int(m_doc.features.size()) || !m_viewport) + return; + CadFeature& feat = m_doc.features[m_constrain_feat]; + if (idx < 0 || idx >= int(feat.entity_constraints.size())) + return; + feat.entity_constraints.erase(feat.entity_constraints.begin() + idx); + // Re-solve the remaining system (deleting a constraint can only free DoF, so it + // cannot fail for over-constraint; ignore the bool and refresh either way). + m_doc.solve_sketch_feature(m_constrain_feat); + m_doc.recompute(); + m_viewport->set_constraint_highlight({}); + m_viewport->update_constrain_entities(m_doc.features[m_constrain_feat].entities); + if (!m_doc.display_mesh.its.indices.empty()) + feed_bodies(); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(_L("Constraint deleted")); + m_status->Refresh(); + refresh_constrain_dof(); + rebuild_constraint_list(); +} + +void DesignPanel::apply_edit_op(EditOp op) +{ + if (m_constrain_feat < 0 || m_constrain_feat >= int(m_doc.features.size()) || !m_viewport || + !m_viewport->is_constraining_entities()) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Press Constrain on a sketch first")); + m_status->Refresh(); + return; + } + auto fail = [this](const wxString& msg) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(msg); + m_status->Refresh(); + }; + + int e0 = -1, e1 = -1; + m_viewport->selected_constrain_entities(e0, e1); + CadFeature& feat = m_doc.features[m_constrain_feat]; + const int n = int(feat.entities.size()); + if (e0 < 0 || e0 >= n) { fail(_L("Pick an entity first")); return; } + using Type = SketchEntity::Type; + + switch (op) { + case EditOp::Mirror: { + if (e1 < 0 || e1 >= n) { fail(_L("Pick the entity, then a mirror-axis line")); return; } + const SketchEntity& axis = feat.entities[e1]; + if (axis.type != Type::Line) { fail(_L("Mirror axis must be a line")); return; } + auto out = SketchEngine::mirror_entities({ feat.entities[e0] }, axis.p0, axis.p1); + if (out.empty()) { fail(_L("Mirror produced nothing")); return; } + const int mi = n; // index the single mirrored copy lands at (n entities before push) + for (auto& m : out) feat.entities.push_back(m); + + // C4a: bind the mirror to its source with Symmetric constraints about the + // axis, so the pair stays mirror-symmetric under later solves and drags. + // mirror_entities preserves P0/P1/Center ordering, so the constraints are + // satisfied by construction; if the solver still rejects them (degenerate + // axis, redundancy) keep the geometry and drop only the binding. + { + using R = SketchPointRole; + using CT = SketchConstraintType; + const std::vector saved_ents = feat.entities; + const size_t cons_before = feat.entity_constraints.size(); + SketchEntityConstraintDef d; d.type = CT::Symmetric; d.ea = e0; d.eb = mi; d.ec = e1; + const Type st = feat.entities[e0].type; + if (st == Type::Line) { + d.ra = R::P0; d.rb = R::P0; feat.entity_constraints.push_back(d); + d.ra = R::P1; d.rb = R::P1; feat.entity_constraints.push_back(d); + } else if (st == Type::Arc || st == Type::Circle) { + d.ra = R::Center; d.rb = R::Center; feat.entity_constraints.push_back(d); + } else if (st == Type::Point) { + d.ra = R::P0; d.rb = R::P0; feat.entity_constraints.push_back(d); + } + if (feat.entity_constraints.size() != cons_before && + !m_doc.solve_sketch_feature(m_constrain_feat)) { + feat.entity_constraints.resize(cons_before); + feat.entities = saved_ents; + } + } + break; + } + case EditOp::Offset: { + const int a = e0; + request_value(_L("Offset distance (+left / -right of direction)"), 1.0, -100000.0, 100000.0, + [this, a](double d) { + CadFeature& f = m_doc.features[m_constrain_feat]; + if (a >= int(f.entities.size())) return; + auto out = SketchEngine::offset_entities({ f.entities[a] }, d); + if (out.empty()) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Offset collapsed the entity")); m_status->Refresh(); return; + } + const int ni = int(f.entities.size()); // offset copy lands here + for (auto& o : out) f.entities.push_back(o); + + // C4c: bind the offset copy to its source. Offset only ADDS geometry + // (the source is untouched), so unlike trim/fillet there are no stale + // constraints to drop — just glue the pair. A line offset stays + // Parallel to its source; an arc/circle offset stays Concentric (same + // centre). Single constraint, so no degradation ladder; solve and roll + // the binding back if the solver rejects it (keep the geometry). + { + using CT = SketchConstraintType; + const Type st = f.entities[a].type; + SketchEntityConstraintDef d2; d2.ea = a; d2.eb = ni; + bool emit = true; + if (st == Type::Line) d2.type = CT::Parallel; + else if (st == Type::Arc || st == Type::Circle) d2.type = CT::Concentric; + else emit = false; + if (emit) { + const size_t cbefore = f.entity_constraints.size(); + f.entity_constraints.push_back(d2); + if (!m_doc.solve_sketch_feature(m_constrain_feat)) + f.entity_constraints.resize(cbefore); + } + } + after_edit_op(); + }); + return; // deferred: edit runs on Confirm + } + case EditOp::Fillet: { + if (e1 < 0 || e1 >= n) { fail(_L("Pick two lines to fillet")); return; } + if (feat.entities[e0].type != Type::Line || feat.entities[e1].type != Type::Line) { + fail(_L("Fillet needs two lines")); return; + } + const int a = e0, b = e1; + request_value(_L("Fillet radius"), 1.0, 0.001, 100000.0, [this, a, b](double r) { + CadFeature& f = m_doc.features[m_constrain_feat]; + if (a >= int(f.entities.size()) || b >= int(f.entities.size())) return; + SketchEntity a_out, b_out, arc_out; + if (!SketchEngine::fillet_lines(f.entities[a], f.entities[b], r, a_out, b_out, arc_out)) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Fillet failed (parallel lines or radius too large)")); + m_status->Refresh(); return; + } + f.entities[a] = a_out; + f.entities[b] = b_out; + const int arc = int(f.entities.size()); + f.entities.push_back(arc_out); + + // C4b: glue the fillet arc to the two trimmed lines so it survives a + // re-solve instead of floating free. arc.p0 sits on line a's moved + // endpoint, arc.p1 on line b's; recover the exact endpoint roles by + // nearest match, then emit Coincident (essential — keeps the corner + // joined) + Tangent (smoothness). The full set can be redundant for the + // arc, so try it first and drop tangents progressively until the solver + // accepts it; the Coincident pins survive even if tangency is rejected. + { + using R = SketchPointRole; + using CT = SketchConstraintType; + auto role_near = [](const SketchEntity& ln, const Vec2d& p) -> R { + return ((ln.p0 - p).squaredNorm() <= (ln.p1 - p).squaredNorm()) ? R::P0 : R::P1; + }; + const R ra = role_near(f.entities[a], arc_out.p0); + const R rb = role_near(f.entities[b], arc_out.p1); + + // Fillet trims both lines back from the shared corner, so any + // constraint anchored to a trimmed endpoint is now stale: the corner + // Coincident that joined (a,ra)·(b,rb), and each line's own length + // Distance (its length just changed). Drop them before re-binding — + // leaving them would fight the new arc geometry and reject every + // binding below. + auto refs = [](const SketchEntityConstraintDef& d, int e, R r) { + return (d.ea == e && d.ra == r) || (d.eb == e && d.rb == r); + }; + auto self_len = [](const SketchEntityConstraintDef& d, int e) { + return d.type == CT::Distance && d.ea == e && d.eb == e; + }; + auto& cs = f.entity_constraints; + cs.erase(std::remove_if(cs.begin(), cs.end(), + [&](const SketchEntityConstraintDef& d) { + return (d.type == CT::Coincident && refs(d, a, ra) && refs(d, b, rb)) + || self_len(d, a) || self_len(d, b); + }), cs.end()); + + auto coin = [&](R arc_role, int ln, R ln_role) { + SketchEntityConstraintDef d; d.type = CT::Coincident; + d.ea = arc; d.ra = arc_role; d.eb = ln; d.rb = ln_role; return d; + }; + auto tang = [&](int ln) { + SketchEntityConstraintDef d; d.type = CT::Tangent; d.ea = arc; d.eb = ln; return d; + }; + const std::vector> ladder = { + { coin(R::P0, a, ra), coin(R::P1, b, rb), tang(a), tang(b) }, + { coin(R::P0, a, ra), coin(R::P1, b, rb), tang(a) }, + { coin(R::P0, a, ra), coin(R::P1, b, rb) }, + }; + const std::vector saved = f.entities; + const size_t cbefore = f.entity_constraints.size(); + for (const auto& set : ladder) { + for (const auto& d : set) f.entity_constraints.push_back(d); + if (m_doc.solve_sketch_feature(m_constrain_feat)) break; // accepted + f.entity_constraints.resize(cbefore); + f.entities = saved; + } + } + after_edit_op(); + }); + return; // deferred: edit runs on Confirm + } + + case EditOp::Chamfer: { + if (e1 < 0 || e1 >= n) { fail(_L("Pick two lines to chamfer")); return; } + if (feat.entities[e0].type != Type::Line || feat.entities[e1].type != Type::Line) { + fail(_L("Chamfer needs two lines")); return; + } + const int a = e0, b = e1; + request_value(_L("Chamfer distance"), 1.0, 0.001, 100000.0, [this, a, b](double d) { + CadFeature& f = m_doc.features[m_constrain_feat]; + if (a >= int(f.entities.size()) || b >= int(f.entities.size())) return; + SketchEntity a_out, b_out, seg_out; + if (!SketchEngine::chamfer_lines(f.entities[a], f.entities[b], d, a_out, b_out, seg_out)) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Chamfer failed (parallel lines or distance too large)")); + m_status->Refresh(); return; + } + f.entities[a] = a_out; + f.entities[b] = b_out; + const int seg = int(f.entities.size()); + f.entities.push_back(seg_out); + + // C4.6: like fillet, chamfer trims both lines back from the shared corner + // and inserts a connecting segment. MUTATING op: drop the stale corner + // Coincident that joined the two trimmed endpoints and each line's own + // length Distance (lengths just changed), then pin the new segment's ends + // onto the trimmed line endpoints with Coincident so it survives re-solve. + { + using R = SketchPointRole; + using CT = SketchConstraintType; + auto role_near = [](const SketchEntity& ln, const Vec2d& p) -> R { + return ((ln.p0 - p).squaredNorm() <= (ln.p1 - p).squaredNorm()) ? R::P0 : R::P1; + }; + const R ra = role_near(f.entities[a], seg_out.p0); + const R rb = role_near(f.entities[b], seg_out.p1); + + auto refs = [](const SketchEntityConstraintDef& dd, int e, R r) { + return (dd.ea == e && dd.ra == r) || (dd.eb == e && dd.rb == r); + }; + auto self_len = [](const SketchEntityConstraintDef& dd, int e) { + return dd.type == CT::Distance && dd.ea == e && dd.eb == e; + }; + auto& cs = f.entity_constraints; + cs.erase(std::remove_if(cs.begin(), cs.end(), + [&](const SketchEntityConstraintDef& dd) { + return (dd.type == CT::Coincident && refs(dd, a, ra) && refs(dd, b, rb)) + || self_len(dd, a) || self_len(dd, b); + }), cs.end()); + + auto coin = [&](R seg_role, int ln, R ln_role) { + SketchEntityConstraintDef dd; dd.type = CT::Coincident; + dd.ea = seg; dd.ra = seg_role; dd.eb = ln; dd.rb = ln_role; return dd; + }; + const std::vector saved = f.entities; + const size_t cbefore = f.entity_constraints.size(); + f.entity_constraints.push_back(coin(R::P0, a, ra)); + f.entity_constraints.push_back(coin(R::P1, b, rb)); + if (!m_doc.solve_sketch_feature(m_constrain_feat)) { + f.entity_constraints.resize(cbefore); // keep geometry, drop pins + f.entities = saved; + } + } + after_edit_op(); + }); + return; // deferred: edit runs on Confirm + } + + case EditOp::Trim: + case EditOp::Extend: { + // Trim accepts Line/Arc/Circle subjects; Extend accepts Line/Arc (a Circle + // is already closed, so there is nothing to extend). + const Type st = feat.entities[e0].type; + const bool subject_ok = (op == EditOp::Trim) + ? (st == Type::Line || st == Type::Arc || st == Type::Circle) + : (st == Type::Line || st == Type::Arc); + if (!subject_ok) { + fail(op == EditOp::Trim ? _L("Trim works on lines, arcs and circles") + : _L("Extend works on lines and arcs")); + return; + } + Vec2d pick; + if (!m_viewport->pick0_point(pick)) { fail(_L("Pick the edge to trim/extend")); return; } + std::vector others; + others.reserve(n > 0 ? n - 1 : 0); + for (int i = 0; i < n; ++i) + if (i != e0) others.push_back(feat.entities[i]); + const SketchEntity before = feat.entities[e0]; // C4.1: detect the moved endpoint + const bool ok = (op == EditOp::Trim) + ? SketchEngine::trim_entity(feat.entities[e0], others, pick) + : SketchEngine::extend_entity(feat.entities[e0], others, pick); + if (!ok) { fail(op == EditOp::Trim ? _L("Nothing to trim at the pick") + : _L("No edge to extend to")); return; } + + // C4.1: trim/extend slides ONE endpoint of the subject along its own + // direction (line) or sweep (arc). That (a) kills the subject's + // self-length Distance dim and (b) detaches the moved endpoint from any + // corner Coincident/PointOn* it used to hold. Drop both stale classes, + // then re-anchor the moved endpoint onto the entity it now lands on with a + // PointOnObject (the bridge picks PT_ON_LINE / PT_ON_CIRCLE). A Circle + // subject restructures into an Arc (both endpoints new) — skip the + // re-anchor there; its self constraints (Radius/Concentric) survive, so + // there is nothing stale to drop either. + if (st == Type::Line || st == Type::Arc) { + using R = SketchPointRole; + using CT = SketchConstraintType; + const SketchEntity& aft = feat.entities[e0]; + R moved = R::P0; + Vec2d P; + if (st == Type::Line) { + const bool p0_moved = (before.p0 - aft.p0).squaredNorm() + > (before.p1 - aft.p1).squaredNorm(); + moved = p0_moved ? R::P0 : R::P1; + P = p0_moved ? aft.p0 : aft.p1; + } else { + const bool start_moved = std::abs(before.start_angle - aft.start_angle) + > std::abs(before.end_angle - aft.end_angle); + moved = start_moved ? R::P0 : R::P1; + const double ang = start_moved ? aft.start_angle : aft.end_angle; + P = aft.center + aft.radius * Vec2d(std::cos(ang), std::sin(ang)); + } + + // Drop stale: subject self-length Distance + any Coincident/PointOn* + // pinning the moved endpoint to its old corner. + auto refs = [&](const SketchEntityConstraintDef& d, R r) { + return (d.ea == e0 && d.ra == r) || (d.eb == e0 && d.rb == r); + }; + auto& cs = feat.entity_constraints; + cs.erase(std::remove_if(cs.begin(), cs.end(), + [&](const SketchEntityConstraintDef& d) { + if (d.type == CT::Distance && d.ea == e0 && d.eb == e0) return true; + return (d.type == CT::Coincident || d.type == CT::PointOnLine + || d.type == CT::PointOnObject) && refs(d, moved); + }), cs.end()); + + // Find which other entity the moved endpoint now lies on (Line/Circle + // cutters only — PT_ON_* needs a line or circle primitive). + int cutter = -1; + const double tol = 1e-5; + for (int i = 0; i < int(feat.entities.size()); ++i) { + if (i == e0) continue; + const SketchEntity& o = feat.entities[i]; + if (o.type == Type::Line) { + Vec2d dv = o.p1 - o.p0; + const double L2 = dv.dot(dv); + if (L2 < 1e-18) continue; + const double t = (P - o.p0).dot(dv) / L2; + if (t < -1e-6 || t > 1.0 + 1e-6) continue; + if ((o.p0 + t * dv - P).norm() < tol) { cutter = i; break; } + } else if (o.type == Type::Circle) { + if (std::abs((P - o.center).norm() - o.radius) < tol) { cutter = i; break; } + } + } + + // Re-anchor with PointOnObject; keep geometry + the stale-drop even if + // the solver rejects the new (possibly redundant) binding. + if (cutter >= 0) { + const size_t cbefore = feat.entity_constraints.size(); + SketchEntityConstraintDef d; d.type = CT::PointOnObject; + d.ea = e0; d.ra = moved; d.eb = cutter; + feat.entity_constraints.push_back(d); + if (!m_doc.solve_sketch_feature(m_constrain_feat)) + feat.entity_constraints.resize(cbefore); + } + } + break; + } + case EditOp::Array: { + // C4.4 linear array. Additive op (originals untouched, copies appended) -> + // no stale constraints to drop. Collect count, then spacing; the array runs + // along the subject line's own direction (or +X for non-lines). Copies are + // pure translates, so for lines they are Parallel + EqualLength to the + // source by construction -> bind each copy to the source in a star web; for + // arc/circle subjects the translate preserves radius (but not the centre), + // so the web is a per-copy Radius dimension instead (see below). + const int a = e0; + request_value(_L("Array count (incl. original)"), 3.0, 2.0, 200.0, + [this, a](double cnt_d) { + const int count = std::max(2, int(cnt_d + 0.5)); + request_value(_L("Spacing (mm)"), 20.0, -100000.0, 100000.0, + [this, a, count](double sp) { + CadFeature& f = m_doc.features[m_constrain_feat]; + if (a >= int(f.entities.size())) return; + using Type = SketchEntity::Type; + // Snapshot everything we need from the source BEFORE pushing the + // copies: push_back can reallocate f.entities and dangle any + // reference into it. Copy the subject by value. + const SketchEntity src = f.entities[a]; + const Type src_type = src.type; + // Default direction: perpendicular to a line (so copies stack + // into a visible, non-overlapping parallel pattern rather than + // extending collinearly); +X for non-line subjects. + Vec2d dir(1.0, 0.0); + if (src_type == Type::Line) { + const Vec2d t = src.p1 - src.p0; + if (t.norm() > 1e-9) { + const Vec2d u = t.normalized(); + dir = Vec2d(-u.y(), u.x()); + } + } + auto copies = SketchEngine::array_entities( + { src }, count, sp * dir, 0.0, Vec2d(0, 0)); + if (copies.empty()) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Array produced nothing")); m_status->Refresh(); return; + } + const int base = int(f.entities.size()); // first copy index + for (auto& c : copies) f.entities.push_back(c); + + if (src_type == Type::Line) { + using CT = SketchConstraintType; + // Bind every copy to the source in a star web. Emit the + // WHOLE web before solving (a per-constraint solve would run + // while later copies are still unconstrained, which the + // solver rejects), then degrade as a set: try Parallel + + // EqualLength, fall back to Parallel only (EqualLength can be + // rank-deficient on exact congruent copies), then to bare + // geometry. Keep the geometry regardless. + const size_t cb = f.entity_constraints.size(); + auto build_web = [&](bool with_equal) { + f.entity_constraints.resize(cb); + for (int k = 0; k < int(copies.size()); ++k) { + SketchEntityConstraintDef dp; dp.type = CT::Parallel; + dp.ea = a; dp.eb = base + k; + f.entity_constraints.push_back(dp); + if (with_equal) { + SketchEntityConstraintDef de; de.type = CT::EqualLength; + de.ea = a; de.eb = base + k; + f.entity_constraints.push_back(de); + } + } + }; + build_web(true); + if (!m_doc.solve_sketch_feature(m_constrain_feat)) { + build_web(false); + if (!m_doc.solve_sketch_feature(m_constrain_feat)) + f.entity_constraints.resize(cb); + } + } else if (src_type == Type::Arc || src_type == Type::Circle) { + // Curved subject: translation preserves the radius but + // marches the centres apart, so the copies are NOT + // concentric. There is no EQUAL_RADIUS in the constraint + // enum, so pin each copy's radius to the source value with + // a per-copy Radius dimension (keeps the array equal-radius + // and documents intent, mirroring the line web). Solve and + // roll the whole web back if the solver rejects it. + using CT = SketchConstraintType; + const size_t cb = f.entity_constraints.size(); + for (int k = 0; k < int(copies.size()); ++k) { + SketchEntityConstraintDef dr; dr.type = CT::Radius; + dr.ea = base + k; dr.value = src.radius; + f.entity_constraints.push_back(dr); + } + if (!m_doc.solve_sketch_feature(m_constrain_feat)) + f.entity_constraints.resize(cb); + } + after_edit_op(); + }); + }); + return; // deferred: edit runs on the two Confirms + } + case EditOp::Move: { + // C4.5 Transform (move). MUTATING op: the subject is translated in place + // (kernel transform_entities with angle=0, scale=1). Pure translation + // PRESERVES orientation and length, so intrinsic + orientation constraints + // survive (Horizontal/Vertical/Parallel/Perpendicular/EqualLength/Angle, + // self-length Distance, Radius/Diameter); it BREAKS position-coupling ones + // (Coincident/PointOn*/Concentric/Symmetric/Midpoint/Fix/LockX/LockY, and + // any Distance tying the subject to a *different* entity). Per the governing + // P4 insight, drop those before re-solving — otherwise the solver drags the + // subject straight back to satisfy them and the move never sticks. + const int a = e0; + request_value(_L("Move dX (mm)"), 20.0, -100000.0, 100000.0, + [this, a](double dx) { + request_value(_L("Move dY (mm)"), 0.0, -100000.0, 100000.0, + [this, a, dx](double dy) { + CadFeature& f = m_doc.features[m_constrain_feat]; + if (a >= int(f.entities.size())) return; + auto out = SketchEngine::transform_entities( + { f.entities[a] }, Vec2d(dx, dy), 0.0, 1.0, Vec2d(0, 0)); + if (out.empty()) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Move produced nothing")); m_status->Refresh(); return; + } + f.entities[a] = out[0]; + + using CT = SketchConstraintType; + auto refs_a = [&](const SketchEntityConstraintDef& d) { + return d.ea == a || d.eb == a || d.ec == a; + }; + auto& cs = f.entity_constraints; + cs.erase(std::remove_if(cs.begin(), cs.end(), + [&](const SketchEntityConstraintDef& d) { + if (!refs_a(d)) return false; + switch (d.type) { + case CT::Coincident: case CT::PointOnLine: case CT::PointOnObject: + case CT::Concentric: case CT::Symmetric: case CT::Midpoint: + case CT::Fix: case CT::LockX: case CT::LockY: + return true; // position-coupling: broken by translation + case CT::Distance: + // self-length (ea==eb==a) survives translation; a + // distance to a *different* entity does not. + return !(d.ea == a && d.eb == a); + default: + return false; // orientation/length: preserved + } + }), cs.end()); + + // The surviving constraints are satisfied by construction + // (translation preserves them); re-solve to fold the new + // position in, keep the geometry even if the solver balks. + m_doc.solve_sketch_feature(m_constrain_feat); + after_edit_op(); + }); + }); + return; // deferred: edit runs on the two Confirms + } + case EditOp::Rotate: { + // C4.5b Transform (rotate-in-place about the subject centroid). MUTATING op. + // Rotation PRESERVES intrinsic size (length/radius) but changes the subject's + // ORIENTATION and the POSITION of its points. So only the size constraints + // survive (EqualLength/Radius/Diameter + self-length Distance); every + // position- or orientation-coupling constraint is broken and must be dropped + // before re-solving, otherwise the solver spins the subject back to satisfy + // them and the rotation never sticks (governing P4 insight). + const int a = e0; + request_value(_L("Rotate angle (deg)"), 45.0, -360.0, 360.0, + [this, a](double deg) { + CadFeature& f = m_doc.features[m_constrain_feat]; + if (a >= int(f.entities.size())) return; + auto centroid_of = [](const SketchEntity& e) -> Vec2d { + using T = SketchEntity::Type; + switch (e.type) { + case T::Line: return 0.5 * (e.p0 + e.p1); + case T::Arc: case T::Circle: case T::Ellipse: case T::EllipseArc: + return e.center; + case T::BSpline: + if (!e.ctrl.empty()) { + Vec2d s(0, 0); for (auto& p : e.ctrl) s += p; + return s / double(e.ctrl.size()); + } + return 0.5 * (e.p0 + e.p1); + default: return e.p0; // Point + } + }; + const Vec2d piv = centroid_of(f.entities[a]); + auto out = SketchEngine::transform_entities( + { f.entities[a] }, Vec2d(0, 0), deg * M_PI / 180.0, 1.0, piv); + if (out.empty()) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Rotate produced nothing")); m_status->Refresh(); return; + } + f.entities[a] = out[0]; + + using CT = SketchConstraintType; + auto refs_a = [&](const SketchEntityConstraintDef& d) { + return d.ea == a || d.eb == a || d.ec == a; + }; + auto& cs = f.entity_constraints; + cs.erase(std::remove_if(cs.begin(), cs.end(), + [&](const SketchEntityConstraintDef& d) { + if (!refs_a(d)) return false; + switch (d.type) { + case CT::EqualLength: case CT::Radius: case CT::Diameter: + return false; // intrinsic size: preserved by rotation + case CT::Distance: + return !(d.ea == a && d.eb == a); // self-length survives + default: + return true; // position/orientation-coupling: broken + } + }), cs.end()); + + m_doc.solve_sketch_feature(m_constrain_feat); + after_edit_op(); + }); + return; // deferred: edit runs on Confirm + } + case EditOp::Scale: { + // C4.5c Transform (uniform scale-in-place about the subject centroid). MUTATING + // op. Uniform scaling PRESERVES orientation and angles (Horizontal/Vertical/ + // Parallel/Perpendicular/Angle survive) but changes SIZE and point POSITIONS: + // drop every size constraint (Radius/Diameter/EqualLength/any Distance) and + // every position-coupling constraint before re-solving, else the solver + // rescales the subject back to satisfy them. + const int a = e0; + request_value(_L("Scale factor"), 2.0, 0.01, 1000.0, + [this, a](double sf) { + CadFeature& f = m_doc.features[m_constrain_feat]; + if (a >= int(f.entities.size())) return; + auto centroid_of = [](const SketchEntity& e) -> Vec2d { + using T = SketchEntity::Type; + switch (e.type) { + case T::Line: return 0.5 * (e.p0 + e.p1); + case T::Arc: case T::Circle: case T::Ellipse: case T::EllipseArc: + return e.center; + case T::BSpline: + if (!e.ctrl.empty()) { + Vec2d s(0, 0); for (auto& p : e.ctrl) s += p; + return s / double(e.ctrl.size()); + } + return 0.5 * (e.p0 + e.p1); + default: return e.p0; // Point + } + }; + const Vec2d piv = centroid_of(f.entities[a]); + auto out = SketchEngine::transform_entities( + { f.entities[a] }, Vec2d(0, 0), 0.0, sf, piv); + if (out.empty()) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Scale produced nothing")); m_status->Refresh(); return; + } + f.entities[a] = out[0]; + + using CT = SketchConstraintType; + auto refs_a = [&](const SketchEntityConstraintDef& d) { + return d.ea == a || d.eb == a || d.ec == a; + }; + auto& cs = f.entity_constraints; + cs.erase(std::remove_if(cs.begin(), cs.end(), + [&](const SketchEntityConstraintDef& d) { + if (!refs_a(d)) return false; + switch (d.type) { + case CT::Horizontal: case CT::Vertical: case CT::Parallel: + case CT::Perpendicular: case CT::Angle: + return false; // orientation/angle: preserved by uniform scale + default: + return true; // size + position-coupling: broken + } + }), cs.end()); + + m_doc.solve_sketch_feature(m_constrain_feat); + after_edit_op(); + }); + return; // deferred: edit runs on Confirm + } + case EditOp::PolarArray: { + // Polar array about the subject centroid. ADDITIVE op (originals untouched, + // count-1 rotated copies appended) -> no stale constraints to drop. Copies + // are rigid rotations of the source, so they preserve LENGTH but NOT + // orientation: bind each copy to the source with EqualLength only (Parallel + // does NOT hold under rotation, unlike the linear-array web). Arc/circle + // subjects rotate about their own centre, so their copies stay Concentric + + // equal-radius instead (see below). Dialogs: count + // then total sweep; angle_step = sweep/count spreads them evenly (last copy + // lands just shy of the original on a full 360). + const int a = e0; + request_value(_L("Polar count (incl. original)"), 6.0, 2.0, 200.0, + [this, a](double cnt_d) { + const int count = std::max(2, int(cnt_d + 0.5)); + request_value(_L("Total sweep (deg)"), 360.0, -360.0, 360.0, + [this, a, count](double sweep_deg) { + CadFeature& f = m_doc.features[m_constrain_feat]; + if (a >= int(f.entities.size())) return; + using Type = SketchEntity::Type; + // Snapshot the subject by value BEFORE pushing copies: push_back + // can reallocate f.entities and dangle a reference into it. + const SketchEntity src = f.entities[a]; + const Type src_type = src.type; + auto centroid_of = [](const SketchEntity& e) -> Vec2d { + using T = SketchEntity::Type; + switch (e.type) { + case T::Line: return 0.5 * (e.p0 + e.p1); + case T::Arc: case T::Circle: case T::Ellipse: case T::EllipseArc: + return e.center; + case T::BSpline: + if (!e.ctrl.empty()) { + Vec2d s(0, 0); for (auto& p : e.ctrl) s += p; + return s / double(e.ctrl.size()); + } + return 0.5 * (e.p0 + e.p1); + default: return e.p0; // Point + } + }; + const Vec2d piv = centroid_of(src); + const double angle_step = (sweep_deg * M_PI / 180.0) / double(count); + auto copies = SketchEngine::array_entities( + { src }, count, Vec2d(0, 0), angle_step, piv); + if (copies.empty()) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Polar array produced nothing")); m_status->Refresh(); return; + } + const int base = int(f.entities.size()); // first copy index + for (auto& c : copies) f.entities.push_back(c); + + if (src_type == Type::Line) { + using CT = SketchConstraintType; + // Rotational web: each copy is EqualLength to the source + // (rotation preserves length; orientation differs so NO + // Parallel). Emit the whole web before solving, then fall + // back to bare geometry if it is rank-deficient. + const size_t cb = f.entity_constraints.size(); + for (int k = 0; k < int(copies.size()); ++k) { + SketchEntityConstraintDef de; de.type = CT::EqualLength; + de.ea = a; de.eb = base + k; + f.entity_constraints.push_back(de); + } + if (!m_doc.solve_sketch_feature(m_constrain_feat)) + f.entity_constraints.resize(cb); + } else if (src_type == Type::Arc || src_type == Type::Circle) { + // Curved subject: the polar pivot is the subject centroid, + // which for an arc/circle IS its own centre. Rotating about + // that centre keeps every copy CONCENTRIC with the source and + // at the same radius (only the angular position shifts). Bind + // each copy with Concentric + a per-copy Radius dimension; + // degrade to Radius-only, then to bare geometry, keeping the + // geometry regardless. + using CT = SketchConstraintType; + const size_t cb = f.entity_constraints.size(); + auto build_web = [&](bool with_concentric) { + f.entity_constraints.resize(cb); + for (int k = 0; k < int(copies.size()); ++k) { + if (with_concentric) { + SketchEntityConstraintDef dc; dc.type = CT::Concentric; + dc.ea = a; dc.eb = base + k; + f.entity_constraints.push_back(dc); + } + SketchEntityConstraintDef dr; dr.type = CT::Radius; + dr.ea = base + k; dr.value = src.radius; + f.entity_constraints.push_back(dr); + } + }; + build_web(true); + if (!m_doc.solve_sketch_feature(m_constrain_feat)) { + build_web(false); + if (!m_doc.solve_sketch_feature(m_constrain_feat)) + f.entity_constraints.resize(cb); + } + } + after_edit_op(); + }); + }); + return; // deferred: edit runs on the two Confirms + } + } + + after_edit_op(); +} + +void DesignPanel::after_edit_op() +{ + if (m_constrain_feat < 0 || m_constrain_feat >= int(m_doc.features.size()) || !m_viewport) + return; + m_doc.recompute(); + m_viewport->set_constraint_highlight({}); // entity indices may have shifted + m_viewport->update_constrain_entities(m_doc.features[m_constrain_feat].entities); + // Refresh the committed-sketch overlay too: it caches the entity list at + // constrain-entry, so a relocating edit (Move/Trim/Extend) would otherwise + // leave a stale ghost of the pre-edit geometry beside the new position. + sync_sketch_display(); + if (!m_doc.display_mesh.its.indices.empty()) + feed_bodies(); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(_L("Applied edit")); + m_status->Refresh(); + refresh_constrain_dof(); + rebuild_constraint_list(); +} + +void DesignPanel::request_value(const wxString& label, double def, double mn, double mx, + std::function cont, + std::function on_cancel) +{ + m_value_cont = std::move(cont); + m_value_cancel = std::move(on_cancel); + m_value_min = mn; + m_value_max = mx; + m_value_label->SetLabel(label); + m_value_input->ChangeValue(en_format(def)); // '.' decimals, no EVT_TEXT feedback + m_form->GetSizer()->Show(m_box_value, true, true); + m_form->Layout(); + m_form->FitInside(); + m_value_input->SetFocus(); + m_value_input->SetSelection(-1, -1); // select all so typing replaces the value + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(label + _L(" — type a value, press Enter (or Confirm)")); + m_status->Refresh(); +} + +void DesignPanel::confirm_value() +{ + if (!m_value_cont) { cancel_value(); return; } + double v = 0.0; + if (!en_parse(m_value_input->GetValue(), v)) { m_value_input->SetFocus(); return; } + v = std::min(std::max(v, m_value_min), m_value_max); // clamp to range + auto cont = m_value_cont; // copy, then clear before running so a + m_value_cont = nullptr; // re-entrant request_value can re-arm cleanly + m_value_cancel = nullptr; // confirmed: drop the cancel action + m_form->GetSizer()->Show(m_box_value, false, true); + m_form->Layout(); + m_form->FitInside(); + cont(v); // run the deferred constraint / edit-op apply +} + +void DesignPanel::cancel_value() +{ + const bool was_open = (m_value_cont != nullptr); + m_value_cont = nullptr; + auto on_cancel = m_value_cancel; // copy, clear, then run (re-entrancy safe) + m_value_cancel = nullptr; + if (m_box_value) + m_form->GetSizer()->Show(m_box_value, false, true); + m_form->Layout(); + m_form->FitInside(); + if (was_open) { + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(wxString()); + m_status->Refresh(); + } + if (on_cancel) + on_cancel(); // e.g. keep a pending line segment as drawn +} + +void DesignPanel::apply_constraint(SketchConstraintType type) +{ + if (m_constrain_feat < 0 || m_constrain_feat >= int(m_doc.features.size()) || + m_viewport == nullptr) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Press Constrain on a sketch first")); + m_status->Refresh(); + return; + } + + // Entity sketches (Fase 4.2) route through the entity-constraint path. + if (m_viewport->is_constraining_entities()) { + apply_entity_constraint(type); + return; + } + + if (!m_viewport->is_constraining()) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Press Constrain on a sketch first")); + m_status->Refresh(); + return; + } + int a = -1, b = -1; + if (!m_viewport->selected_segment(a, b)) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Pick a segment in the viewport first")); + m_status->Refresh(); + return; + } + CadFeature& feat = m_doc.features[m_constrain_feat]; + // solve_sketch_feature rewrites profile.points even on failure, so snapshot + // the geometry to roll back a rejected constraint cleanly. + const std::vector saved_pts = feat.profile.points; + feat.constraints.push_back(SketchConstraintDef{type, a, b, -1, -1, 0.0}); + if (!m_doc.solve_sketch_feature(m_constrain_feat)) { + feat.constraints.pop_back(); // reject the non-converging addition + feat.profile.points = saved_pts; // and restore the pre-solve geometry + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Constraint rejected (over-constrained)")); + m_status->Refresh(); + return; + } + m_doc.recompute(); + m_viewport->update_constrain_profile(m_doc.features[m_constrain_feat].profile.points); + if (!m_doc.display_mesh.its.indices.empty()) + feed_bodies(); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(type == SketchConstraintType::Horizontal ? _L("Applied Horizontal") + : _L("Applied Vertical")); + m_status->Refresh(); +} + +void DesignPanel::reset_edit_state() +{ + m_edit_index = -1; +} + +int DesignPanel::resolve_extrude_sketch() const +{ + int sel = tree_selection(); + if (sel != wxNOT_FOUND && sel < int(m_doc.features.size()) && + m_doc.features[sel].type == CadFeatureType::Sketch) + return sel; + for (int i = int(m_doc.features.size()) - 1; i >= 0; --i) + if (m_doc.features[i].type == CadFeatureType::Sketch) return i; + return -1; +} + +void DesignPanel::load_feature_into_dialog(const CadFeature& f) +{ + switch (f.type) { + case CadFeatureType::Sketch: + m_shape->SetSelection(f.shape == SketchShape::Circle ? 1 : 0); + m_plane->SetSelection(index_from_plane(f.plane)); + m_width->SetValue(f.width); + m_height->SetValue(f.height); + m_radius->SetValue(f.radius); + break; + case CadFeatureType::Extrude: + m_distance->SetValue(f.distance); + m_mode->SetSelection(static_cast(f.mode)); // New=0,Add=1,Cut=2,Intersect=3 + m_extrude_end->SetSelection(static_cast(f.extrude_end)); + m_distance2->SetValue(f.distance2); + m_taper->SetValue(f.taper_deg); + m_flip->SetValue(f.flip); + m_extrude_sketch_ref = f.sketch_ref; + m_sel_solid_body = f.target_body; // preserve which body on re-edit + if (m_extrude_sketch_ref >= 0 && m_extrude_sketch_ref < int(m_doc.features.size())) + m_extrude_sketch_label->SetLabel(_L("Sketch: ") + + wxString::FromUTF8(m_doc.features[m_extrude_sketch_ref].name)); + break; + case CadFeatureType::Fillet: + case CadFeatureType::Chamfer: + m_dressup_type->SetSelection(f.type == CadFeatureType::Fillet ? 0 : 1); + m_dressup_size->SetValue(f.dressup_size); + m_face_group->SetSelection(static_cast(f.face_group)); + m_sel_solid_edge = f.dressup_edge; // preserve edge-targeting on re-edit + m_sel_solid_body = f.target_body; // preserve which body on re-edit + break; + case CadFeatureType::Hole: + m_hole_plane->SetSelection(index_from_plane(f.plane)); + m_hole_diameter->SetValue(f.hole_diameter); + m_hole_depth->SetValue(f.hole_depth); + m_hole_through->SetValue(f.hole_through); + m_hole_x->SetValue(f.hole_x); + m_hole_y->SetValue(f.hole_y); + break; + case CadFeatureType::Thread: + m_thread_plane->SetSelection(index_from_plane(f.plane)); + m_thread_radius->SetValue(f.thread_radius); + m_thread_pitch->SetValue(f.thread_pitch); + m_thread_height->SetValue(f.thread_height); + m_thread_depth->SetValue(f.thread_depth); + m_thread_internal->SetValue(f.thread_internal); + m_thread_x->SetValue(f.thread_x); + m_thread_y->SetValue(f.thread_y); + if (m_thread_std) m_thread_std->SetSelection(0); // Custom: spins reflect the stored feature + break; + case CadFeatureType::Shell: + m_shell_thickness->SetValue(f.shell_thickness); + m_sel_solid_face = f.shell_face; + m_shell_face_label->SetLabel(f.shell_face >= 0 + ? wxString::Format(_L("Face %d"), f.shell_face) + : _L("(all faces — closed hollow)")); + break; + case CadFeatureType::Revolve: + m_revolve_angle->SetValue(f.revolve_angle); + m_revolve_axis->SetSelection(f.revolve_axis); + m_revolve_mode->SetSelection(static_cast(f.mode)); + m_revolve_flip->SetValue(f.flip); + m_revolve_sketch_ref = f.sketch_ref; + break; + case CadFeatureType::Sweep: + m_sweep_profile_ref = f.sketch_ref; + m_sweep_path_ref = f.sweep_path_ref; // show_tool pre-selects this in the picker + m_sweep_mode->SetSelection(static_cast(f.mode)); + break; + case CadFeatureType::Pattern: + m_pattern_type->SetSelection(f.pattern_circular ? 1 : 0); + m_pattern_count->SetValue(f.pattern_count); + m_pattern_spacing->SetValue(f.pattern_spacing); + m_pattern_dir->SetSelection(f.pattern_dir); + m_pattern_angle->SetValue(f.pattern_angle); + break; + case CadFeatureType::Plane: + populate_plane_choices(m_plane_base); + m_plane_base->SetSelection(f.plane_base); + m_plane_offset->SetValue(f.plane_offset); + m_plane_tilt->SetValue(f.plane_angle_tilt); + m_plane_tilt_axis->SetSelection(f.plane_axis); + break; + case CadFeatureType::Loft: + m_loft_refs = f.loft_profile_refs; // show_tool re-checks these in the list + m_loft_ruled->SetValue(f.loft_ruled); + m_loft_mode->SetSelection(static_cast(f.mode)); + break; + case CadFeatureType::Draft: + m_draft_angle->SetValue(f.draft_angle); + m_sel_solid_face = f.draft_face; + m_draft_face_label->SetLabel(f.draft_face >= 0 + ? wxString::Format(_L("Face %d"), f.draft_face) + : _L("(pick a side face)")); + break; + default: break; + } +} + +void DesignPanel::on_edit_feature() +{ + int sel = tree_selection(); + if (sel == wxNOT_FOUND) { + m_status->SetLabel(_L("Select a feature in the tree first")); + m_status->Refresh(); + return; + } + const CadFeature& f = m_doc.features[sel]; + reset_edit_state(); + + switch (f.type) { + case CadFeatureType::Sketch: + // Imported Text/SVG art has no editable sketch dialog — edit means + // move / scale its placement instead, behind the same Confirm/Cancel gate as + // the initial insert (Cancel = undo restores the prior placement). + if (!f.imported_regions.empty()) { + m_doc.checkpoint(); // undo boundary: re-placing imported art + on_transform_imported(sel); + m_insert_feat = sel; + open_insert_card(wxString::FromUTF8(f.name)); + break; + } + m_edit_index = sel; + if (!f.entities.empty()) { + // Entity sketcher: re-open the geometry for full in-canvas editing (handles, + // live quotes, regular-polygon drag) in the ENTITY sketch UI (the top sketch + // toolbar + session card), NOT the legacy parametric card. The commit handler + // replaces this feature in place (see m_edit_index). Hide its display overlay + // so the live tool is the only copy drawn. + set_ui_mode(UiMode::Sketch); + if (m_viewport) { + m_viewport->set_display_sketches({}); + m_viewport->edit_sketch(f.entities, f.entity_constraints, f.plane); + } + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(_L("Editing sketch — drag a handle or click a quote to edit")); + m_status->Refresh(); + } else { + load_feature_into_dialog(f); + open_tool(Tool::Sketch); + } + break; + case CadFeatureType::Extrude: + m_edit_index = sel; + load_feature_into_dialog(f); + open_tool(Tool::Extrude); + break; + case CadFeatureType::Fillet: + case CadFeatureType::Chamfer: + m_edit_index = sel; + load_feature_into_dialog(f); + open_tool(Tool::Dressup); + break; + case CadFeatureType::Hole: + m_edit_index = sel; + load_feature_into_dialog(f); + open_tool(Tool::Hole); + break; + case CadFeatureType::Thread: + m_edit_index = sel; + load_feature_into_dialog(f); + open_tool(Tool::Thread); + break; + case CadFeatureType::Shell: + m_edit_index = sel; + load_feature_into_dialog(f); + open_tool(Tool::Shell); + break; + case CadFeatureType::Revolve: + m_edit_index = sel; + load_feature_into_dialog(f); + open_tool(Tool::Revolve); + break; + case CadFeatureType::Sweep: + m_edit_index = sel; + load_feature_into_dialog(f); + open_tool(Tool::Sweep); + break; + case CadFeatureType::Pattern: + m_edit_index = sel; + load_feature_into_dialog(f); + open_tool(Tool::Pattern); + break; + case CadFeatureType::Plane: + m_edit_index = sel; + load_feature_into_dialog(f); + open_tool(Tool::Plane); + break; + case CadFeatureType::Loft: + m_edit_index = sel; + load_feature_into_dialog(f); + open_tool(Tool::Loft); + break; + case CadFeatureType::Draft: + m_edit_index = sel; + load_feature_into_dialog(f); + open_tool(Tool::Draft); + break; + default: break; + } +} + +void DesignPanel::on_commit() +{ + // A feature tool open with a live preview ghost (e.g. a fillet being previewed) is + // NOT yet part of the body. Apply it first so "Commit to Plate" ships exactly what + // is shown on screen, not the pre-feature solid. (confirm_tool() applies + closes.) + if (m_active != Tool::None) + confirm_tool(); + + if (m_doc.display_mesh.its.indices.empty()) { + m_status->SetLabel(_L("Nothing to commit — add a feature first")); + return; + } + ObjectList* obj_list = wxGetApp().obj_list(); + if (obj_list == nullptr) + return; + + // Multi-body: ship each (visible) body as its own plate object so they arrive on the + // slicer plate as independent, separately-arrangeable parts (Onshape "Commit all parts"). + // Hidden bodies are skipped — what you see on the Design plate is what gets committed. + sync_body_visible(); + rebuild_disp_meshes(); // ship moved bodies at their Move-gizmo positions + if (m_disp_body_meshes.size() > 1) { + int committed = 0; + for (size_t b = 0; b < m_disp_body_meshes.size(); ++b) { + if (b < m_body_visible.size() && !m_body_visible[b]) continue; // skip hidden + if (m_disp_body_meshes[b].its.indices.empty()) continue; + obj_list->load_mesh_object(m_disp_body_meshes[b], + "Design Body " + std::to_string(b + 1)); + ++committed; + } + if (committed == 0) { // every body hidden — nothing to ship + m_status->SetLabel(_L("All bodies hidden — show one before committing")); + return; + } + } else { + obj_list->load_mesh_object(m_disp_pick_mesh, "Design Body"); + } + + if (wxGetApp().mainframe != nullptr) + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); +} + +CadFeature DesignPanel::build_candidate(Tool t) const +{ + CadFeature f; + switch (t) { + case Tool::Sketch: + f.type = CadFeatureType::Sketch; + f.shape = (m_shape->GetSelection() == 0) ? SketchShape::Rectangle : SketchShape::Circle; + f.plane = plane_from_choice(m_plane->GetSelection()); + f.width = m_width->GetValue(); + f.height = m_height->GetValue(); + f.radius = m_radius->GetValue(); + break; + case Tool::Extrude: + f.type = CadFeatureType::Extrude; + f.distance = m_distance->GetValue(); + f.symmetric = false; + f.extrude_end = static_cast(m_extrude_end->GetSelection()); + f.distance2 = m_distance2->GetValue(); + f.taper_deg = m_taper->GetValue(); + f.flip = m_flip->GetValue(); + f.up_to_face = (f.extrude_end == ExtrudeEnd::UpToFace) ? m_sel_solid_face : -1; + f.mode = (m_mode->GetSelection() == 0) ? BooleanMode::New + : (m_mode->GetSelection() == 1) ? BooleanMode::Add + : (m_mode->GetSelection() == 2) ? BooleanMode::Cut + : BooleanMode::Intersect; + if (m_extrude_face_src >= 0) { + // Preview the face-as-profile extrude: the kernel grabs the body face by id. + f.extrude_src_face = m_extrude_face_src; + f.sketch_ref = -1; + } else if (extrude_uses_loop()) { + // Preview just the click-selected loop: carry its entity subset on the + // feature (sketch_ref = -1 -> build_sketch_wire uses f.entities). + f.sketch_ref = -1; + f.entities = m_viewport->selected_loop_entities(); + f.plane = m_doc.features[m_extrude_sketch_ref].plane; + } else { + f.sketch_ref = m_extrude_sketch_ref; + // Preview an on-face engraving Cut against the host body (matches the commit). + if (m_extrude_sketch_ref >= 0 && m_extrude_sketch_ref < int(m_doc.features.size()) + && m_doc.features[m_extrude_sketch_ref].import_on_face) + f.target_body = m_doc.features[m_extrude_sketch_ref].import_face_body; + } + break; + case Tool::Dressup: + f.type = (m_dressup_type->GetSelection() == 0) ? CadFeatureType::Fillet + : CadFeatureType::Chamfer; + f.dressup_size = m_dressup_size->GetValue(); + f.face_group = static_cast(m_face_group->GetSelection()); + // A click-selected solid edge overrides the face-group: dress THAT edge. + f.dressup_edge = m_sel_solid_edge; // -1 when no edge picked + break; + case Tool::Hole: + f.type = CadFeatureType::Hole; + f.plane = hole_plane(); + f.hole_diameter = m_hole_diameter->GetValue(); + f.hole_depth = m_hole_depth->GetValue(); + f.hole_through = m_hole_through->GetValue(); + f.hole_x = m_hole_x->GetValue(); + f.hole_y = m_hole_y->GetValue(); + if (m_hole_on_face) f.target_body = m_hole_face_body; // preview the right body + break; + case Tool::Thread: + f.type = CadFeatureType::Thread; + f.plane = thread_plane(); + f.thread_radius = m_thread_radius->GetValue(); + f.thread_pitch = m_thread_pitch->GetValue(); + f.thread_height = m_thread_height->GetValue(); + f.thread_depth = m_thread_depth->GetValue(); + f.thread_internal = m_thread_internal->GetValue(); + f.thread_x = m_thread_x->GetValue(); + f.thread_y = m_thread_y->GetValue(); + if (m_thread_on_face) f.target_body = m_thread_face_body; // tap the right body + break; + case Tool::Shell: + f.type = CadFeatureType::Shell; + f.shell_thickness = m_shell_thickness->GetValue(); + // A picked solid face opens the shell there; -1 = closed hollow. + f.shell_face = (m_sel_solid_face >= 0) ? m_sel_solid_face : -1; + break; + case Tool::Draft: + f.type = CadFeatureType::Draft; + f.draft_angle = m_draft_angle->GetValue(); + f.draft_face = (m_sel_solid_face >= 0) ? m_sel_solid_face : -1; + break; + case Tool::Revolve: + f.type = CadFeatureType::Revolve; + f.sketch_ref = m_revolve_sketch_ref; + f.revolve_angle = m_revolve_angle->GetValue(); + f.revolve_axis = m_revolve_axis->GetSelection(); + f.flip = m_revolve_flip->GetValue(); + f.mode = static_cast(m_revolve_mode->GetSelection()); + break; + case Tool::Sweep: { + f.type = CadFeatureType::Sweep; + f.sketch_ref = m_sweep_profile_ref; + const int sel = m_sweep_path ? m_sweep_path->GetSelection() : wxNOT_FOUND; + f.sweep_path_ref = (sel != wxNOT_FOUND) + ? int(reinterpret_cast(m_sweep_path->GetClientData(sel))) : -1; + f.mode = static_cast(m_sweep_mode->GetSelection()); + break; + } + case Tool::Pattern: + f.type = CadFeatureType::Pattern; + f.pattern_circular = (m_pattern_type->GetSelection() == 1); + f.pattern_count = int(m_pattern_count->GetValue()); + f.pattern_spacing = m_pattern_spacing->GetValue(); + f.pattern_dir = m_pattern_dir->GetSelection(); + f.pattern_angle = m_pattern_angle->GetValue(); + break; + case Tool::Plane: + f.type = CadFeatureType::Plane; + f.plane_base = m_plane_base->GetSelection(); + f.plane_offset = m_plane_offset->GetValue(); + f.plane_angle_tilt = m_plane_tilt->GetValue(); + f.plane_axis = m_plane_tilt_axis->GetSelection(); + break; + case Tool::Loft: { + f.type = CadFeatureType::Loft; + f.loft_ruled = m_loft_ruled->GetValue(); + f.mode = static_cast(m_loft_mode->GetSelection()); + f.loft_profile_refs.clear(); + for (unsigned i = 0; i < m_loft_list->GetCount(); ++i) + if (m_loft_list->IsChecked(i) && i < m_loft_sketch_idx.size()) + f.loft_profile_refs.push_back(m_loft_sketch_idx[i]); + break; + } + case Tool::Boolean: { + f.type = CadFeatureType::Boolean; + const int sel = m_bool_op->GetSelection(); + f.mode = (sel == 1) ? BooleanMode::Cut + : (sel == 2) ? BooleanMode::Intersect + : BooleanMode::Add; // 0 = Union + f.target_body = m_bool_target->GetSelection(); + f.bool_tool_body = m_bool_tool->GetSelection(); + f.bool_keep_tool = m_bool_keep->GetValue(); + f.bool_tolerance = m_bool_tol->GetValue(); // OCCT fuzzy: robust cut on near-coincident faces + break; + } + case Tool::Cut: + f.type = CadFeatureType::Cut; + f.plane = plane_from_choice(m_cut_plane->GetSelection()); + f.cut_offset = m_cut_offset->GetValue(); + f.cut_flip = false; + f.cut_keep_upper = true; // always split: keep both pieces as separate bodies + f.cut_keep_lower = true; + f.target_body = m_cut_target->GetSelection(); + break; + case Tool::Insert: // imported art is committed by add_imported_sketch, not build_candidate + case Tool::None: + break; + } + // Boolean drives its own target/tool body from the card; every other tool targets the + // picked body (face-extrude reads its source face there, dress-up / hole / boolean-mode + // extrude mutate it). -1 when nothing is picked => auto (last body). + if (m_active != Tool::Boolean && m_active != Tool::Cut) + f.target_body = m_sel_solid_body; + return f; +} + +// Resolve the active Extrude's profile plane + a representative 2D centroid (arrow anchor) +// and push them to the viewport gizmo. Self-gates: clears the gizmo unless Extrude is open. +void DesignPanel::update_fillet_gizmo() +{ + if (!m_viewport) return; + // Only while the Fillet/Chamfer card is open AND a solid EDGE is the target. Face-group + // dress-up (no picked edge) keeps the docked card with no in-canvas handle. The body centroid + // comes from the transformed display mesh so it matches the (transformed) edge sample points. + const bool ok = (m_active == Tool::Dressup) && m_sel_solid_edge >= 0 + && m_sel_solid_body >= 0 && m_sel_solid_body < int(m_disp_body_meshes.size()); + if (!ok) { m_viewport->clear_fillet_gizmo(); return; } + const Vec3d centroid = m_disp_body_meshes[m_sel_solid_body].bounding_box().center(); + m_viewport->begin_fillet_gizmo(centroid, m_dressup_size->GetValue()); +} + +// Push the active Hole card's plane + position + diameter/depth to the viewport gizmo. +// Self-gates: clears the gizmo unless the Hole card is open. +void DesignPanel::update_hole_gizmo() +{ + if (!m_viewport) return; + if (m_active != Tool::Hole) { m_viewport->clear_hole_gizmo(); return; } + const SketchPlane plane = hole_plane(); + m_viewport->set_hole_face_bounds(m_hole_has_bounds, m_hole_umin, m_hole_umax, + m_hole_vmin, m_hole_vmax); + m_viewport->begin_hole_gizmo(plane, + m_hole_x->GetValue(), m_hole_y->GetValue(), + m_hole_diameter->GetValue(), m_hole_depth->GetValue(), + m_hole_through->GetValue()); +} + +// Push the active Thread card's plane + position + radius/length to the viewport gizmo. +// Self-gates: clears the gizmo unless the Thread card is open. +void DesignPanel::update_thread_gizmo() +{ + if (!m_viewport) return; + if (m_active != Tool::Thread) { m_viewport->clear_thread_gizmo(); return; } + const SketchPlane plane = thread_plane(); + m_viewport->begin_thread_gizmo(plane, + m_thread_x->GetValue(), m_thread_y->GetValue(), + m_thread_radius->GetValue(), m_thread_height->GetValue()); +} + +// Anchor an inward thickness arrow at the picked open face's centroid (along -outward-normal). +// Self-gates: clears unless the Shell card is open AND a face is picked. The face centroid/normal +// come from the kernel shape, then carry the body's display-only Move transform. +void DesignPanel::update_shell_gizmo() +{ + if (!m_viewport) return; + const int b = m_sel_solid_body; + const bool ok = (m_active == Tool::Shell) && m_sel_solid_face >= 0 + && b >= 0 && b < int(m_doc.bodies.size()); + if (!ok) { m_viewport->clear_shell_gizmo(); return; } + const TopoDS_Face fc = GeometryEngine::face_by_index(m_doc.bodies[b].shape, m_sel_solid_face); + if (fc.IsNull()) { m_viewport->clear_shell_gizmo(); return; } + Vec3d c = GeometryEngine::face_centroid_world(fc); + Vec3d n = GeometryEngine::face_normal_world(fc); + sync_body_xform(); + if (b < int(m_body_xform.size())) { + c = m_body_xform[b] * c; + n = m_body_xform[b].linear() * n; + } + if (n.norm() < 1e-9) { m_viewport->clear_shell_gizmo(); return; } + // Arrow points inward (into the wall): -outward normal. + m_viewport->begin_shell_gizmo(c, (-n).normalized(), m_shell_thickness->GetValue()); +} + +void DesignPanel::update_revolve_gizmo() +{ + if (!m_viewport) return; + if (m_active != Tool::Revolve + || m_revolve_sketch_ref < 0 || m_revolve_sketch_ref >= int(m_doc.features.size())) { + m_viewport->clear_revolve_gizmo(); + return; + } + const CadFeature& sk = m_doc.features[m_revolve_sketch_ref]; + // Profile centroid in sketch coords (same rule as the Extrude gizmo: average entity centres, + // else profile points, else the plane origin for primitive shapes). + Vec2d centroid(0, 0); + if (!sk.entities.empty()) { + Vec2d acc(0, 0); int n = 0; + for (const SketchEntity& e : sk.entities) { + switch (e.type) { + case SketchEntity::Type::Line: acc += 0.5 * (e.p0 + e.p1); ++n; break; + case SketchEntity::Type::Arc: + case SketchEntity::Type::EllipseArc: + case SketchEntity::Type::Circle: + case SketchEntity::Type::Ellipse: acc += e.center; ++n; break; + case SketchEntity::Type::Point: acc += e.p0; ++n; break; + case SketchEntity::Type::BSpline: + if (!e.ctrl.empty()) { + Vec2d s(0, 0); for (const Vec2d& q : e.ctrl) s += q; + acc += s / double(e.ctrl.size()); ++n; + } + break; + } + } + if (n > 0) centroid = acc / double(n); + } else if (!sk.profile.points.empty()) { + for (const Vec2d& p : sk.profile.points) centroid += p; + centroid /= double(sk.profile.points.size()); + } + m_viewport->begin_revolve_gizmo(sk.plane, centroid, m_revolve_axis->GetSelection(), + m_revolve_angle->GetValue(), m_revolve_flip->GetValue()); +} + +void DesignPanel::update_pattern_gizmo() +{ + if (!m_viewport) return; + if (m_active != Tool::Pattern || m_doc.display_body_meshes.empty()) { + m_viewport->clear_pattern_gizmo(); + return; + } + // Pattern operates in the default world XY plane (matches the kernel: linear along world X/Y, + // circular about world Z through the origin). Anchor on the target body's bbox centre; if that + // body carries a display-only Move transform, shift the plane origin + anchor by it so the + // gizmo sits on the body where the ghost copies actually appear. + const int b = (m_sel_solid_body >= 0 && m_sel_solid_body < int(m_doc.display_body_meshes.size())) + ? m_sel_solid_body : int(m_doc.display_body_meshes.size()) - 1; + SketchPlane plane; // world XY axes by default + Vec3d base = m_doc.display_body_meshes[b].bounding_box().center(); + if (b < int(m_body_xform.size())) { + plane.origin = m_body_xform[b].translation(); + base = m_body_xform[b] * base; + } + m_viewport->begin_pattern_gizmo(plane, base, m_pattern_type->GetSelection() == 1, + int(m_pattern_count->GetValue()), m_pattern_dir->GetSelection(), + m_pattern_spacing->GetValue(), m_pattern_angle->GetValue()); +} + +void DesignPanel::update_extrude_gizmo() +{ + if (!m_viewport) return; + if (m_active != Tool::Extrude) { m_viewport->clear_extrude_gizmo(); return; } + + SketchPlane plane; + std::vector ents; + Vec2d centroid(0, 0); + bool have = false, have_centroid = false; + if (extrude_uses_loop()) { + plane = m_doc.features[m_extrude_sketch_ref].plane; + ents = m_viewport->selected_loop_entities(); + have = true; + } else if (m_extrude_sketch_ref >= 0 && m_extrude_sketch_ref < int(m_doc.features.size())) { + const CadFeature& sk = m_doc.features[m_extrude_sketch_ref]; + plane = sk.plane; + ents = sk.entities; + have = true; + if (ents.empty() && !sk.profile.points.empty()) { + for (const Vec2d& p : sk.profile.points) centroid += p; + centroid /= double(sk.profile.points.size()); + have_centroid = true; + } + // Primitive shape sketches (no entities/profile) are centred at the plane origin -> (0,0). + } else if (m_extrude_face_src >= 0 && !m_doc.bodies.empty()) { + // Face-as-profile (push/pull): anchor the arrow at the picked face's CENTROID, pointing + // along its outward normal. The face id is LOCAL to the owner body — route_feature reads + // it from `context` (the target, else the last body) — so look it up on that SAME body, + // never the whole-document compound (m_doc.body). from_face's plane origin sits at the + // plane's canonical point near the world origin, NOT on the face, which is why the arrow + // used to land on the bed. Carry the body's display Move transform so the arrow sits where + // the body is actually shown. + const int b = int(m_doc.bodies.size()) - 1; // matches route_feature's default context + TopoDS_Face srcf = GeometryEngine::face_by_index(m_doc.bodies[b].shape, m_extrude_face_src); + if (!srcf.IsNull()) { + plane = SketchPlane::from_face(srcf); + Vec3d c = GeometryEngine::face_centroid_world(srcf); + Vec3d n = GeometryEngine::face_normal_world(srcf); + if (srcf.Orientation() == TopAbs_REVERSED) n = -n; // outward, matches the kernel push + sync_body_xform(); + if (b < int(m_body_xform.size())) { c = m_body_xform[b] * c; n = m_body_xform[b].linear() * n; } + plane.origin = c; + if (n.norm() > 1e-9) plane.normal = n.normalized(); + have = true; + } + } + if (!have) { m_viewport->clear_extrude_gizmo(); return; } + + if (!ents.empty()) { + Vec2d acc(0, 0); int n = 0; + for (const SketchEntity& e : ents) { + switch (e.type) { + case SketchEntity::Type::Line: acc += 0.5 * (e.p0 + e.p1); ++n; break; + case SketchEntity::Type::Arc: + case SketchEntity::Type::EllipseArc: + case SketchEntity::Type::Circle: + case SketchEntity::Type::Ellipse: acc += e.center; ++n; break; + case SketchEntity::Type::Point: acc += e.p0; ++n; break; + case SketchEntity::Type::BSpline: + if (!e.ctrl.empty()) { + Vec2d s(0, 0); for (const Vec2d& q : e.ctrl) s += q; + acc += s / double(e.ctrl.size()); ++n; + } + break; + } + } + if (n > 0) { centroid = acc / double(n); have_centroid = true; } + } + (void)have_centroid; // centroid defaults to (0,0) for primitive sketches + + const ExtrudeEnd end = static_cast(m_extrude_end->GetSelection()); + m_viewport->set_extrude_gizmo(plane, centroid, + m_distance->GetValue(), m_distance2->GetValue(), + end == ExtrudeEnd::TwoSided, m_flip->GetValue()); +} + +void DesignPanel::refresh_preview() +{ + if (m_active == Tool::None) { m_viewport->clear_preview(); return; } + + if (m_active == Tool::Sketch || m_active == Tool::Plane) { + // A sketch / datum plane carries no 3D solid; there is no ghost to show. Always + // valid, so just enable Confirm and clear any stale ghost. + m_viewport->clear_preview(); + m_status->SetForegroundColour(wxColour(120, 210, 120)); + m_status->SetLabel(m_active == Tool::Plane ? _L("Plane ready") : _L("Sketch ready")); + for (wxButton* b : m_confirm_btns) if (b) b->Enable(true); + m_status->Refresh(); + return; + } + + // Trim m_body_xform to the LIVE committed bodies before building the ghost. A move + // writes a per-body display transform keyed by index; if a moved body is later deleted + // or consumed, its stale transform must not survive and get re-applied to whatever new + // body lands at that index — that was painting fresh extrudes as a moved+rotated ghost + // far from the sketch. resize() drops entries beyond the current body count. + sync_body_xform(); + + CadFeature cand = build_candidate(m_active); + TriangleMesh mesh; + std::string err; + bool ok = false; + + // The body is displayed through its per-body Move transform (m_body_xform); the ghost is + // built from the untransformed kernel, so without this it floats back at the origin once a + // body has been moved. Re-merge the per-body ghost meshes with the same transforms applied. + auto ghost_from = [this](const std::vector& pbm) -> TriangleMesh { + TriangleMesh out; + for (size_t b = 0; b < pbm.size(); ++b) { + TriangleMesh m = pbm[b]; + if (b < m_body_xform.size()) m.transform(m_body_xform[b]); + out.merge(m); + } + return out; + }; + + const bool editing_single = (m_edit_index >= 0); + if (editing_single) { + // Edit-mode preview: stacking the candidate on top of the live body would + // re-apply the feature being edited (fillet-on-fillet) — wrong, and a + // source of OCCT failures. Instead evaluate the *replace* on a throwaway + // copy so the ghost is the true post-edit body. + CadDocument tmp = m_doc; + ok = tmp.replace_feature(m_edit_index, cand); + if (ok) mesh = ghost_from(tmp.display_body_meshes); else err = tmp.error; + } else { + std::vector pbm; + ok = m_doc.preview(cand, mesh, pbm, err); + if (ok) mesh = ghost_from(pbm); + } + + if (ok) { + m_viewport->set_preview_mesh(mesh); + m_status->SetForegroundColour(wxColour(120, 210, 120)); // ok = green + m_status->SetLabel(wxString::Format(_L("Preview — %zu triangles"), mesh.its.indices.size())); + } else { + m_viewport->clear_preview(); + m_status->SetForegroundColour(wxColour(235, 110, 110)); // invalid = red + m_status->SetLabel(_L("Invalid: ") + wxString::FromUTF8(err)); + } + // Onshape parity: a broken candidate cannot be committed. Grey the active dialog's + // Confirm so the user sees the gate before clicking; the red status says why. + for (wxButton* b : m_confirm_btns) + if (b != nullptr) b->Enable(ok); + // Fillet/Chamfer/Draft: once the target edge/face yields a valid result, show ONLY the + // preview (hide the base bodies) so the user sees the finished shape, not the old solid + // doubled with the ghost. Before a valid pick the body stays visible so it can be picked. + m_viewport->set_body_hidden((m_active == Tool::Dressup || m_active == Tool::Draft) && ok); + m_status->Refresh(); + + // Refresh the in-canvas Extrude depth arrow (self-gates: only while the Extrude card is open). + update_extrude_gizmo(); + // Same for the Fillet/Chamfer radius arrow (self-gates: Dressup card + a picked edge). + update_fillet_gizmo(); + // Same for the Hole footprint circle + diameter/depth arrows (self-gates: Hole card). + update_hole_gizmo(); + // Same for the Thread footprint circle + radius/length arrows (self-gates: Thread card). + update_thread_gizmo(); + // Same for the Shell thickness arrow on the picked face (self-gates: Shell card + a face). + update_shell_gizmo(); + // Same for the Revolve angle-arc around the axis (self-gates: only while the Revolve card is open). + update_revolve_gizmo(); + // Same for the Pattern spacing arrow / angle-arc (self-gates: only while the Pattern card is open). + update_pattern_gizmo(); +} + +void DesignPanel::open_tool(Tool t) +{ + m_active = t; + // Fillet/Chamfer/Draft no longer fade the body see-through; instead, once a valid target + // is picked, refresh_preview hides the base bodies entirely (preview-only). Keep it opaque + // here so the body is fully visible for picking the edge/face. + if (m_viewport) { m_viewport->set_body_translucent(false); m_viewport->set_body_hidden(false); } + wxSizer* s = m_form->GetSizer(); + s->Show(m_box_sketch, t == Tool::Sketch, true); + s->Show(m_box_extrude, t == Tool::Extrude, true); + s->Show(m_box_dressup, t == Tool::Dressup, true); + s->Show(m_box_hole, t == Tool::Hole, true); + s->Show(m_box_thread, t == Tool::Thread, true); + s->Show(m_box_shell, t == Tool::Shell, true); + s->Show(m_box_revolve, t == Tool::Revolve, true); + s->Show(m_box_sweep, t == Tool::Sweep, true); + s->Show(m_box_pattern, t == Tool::Pattern, true); + s->Show(m_box_plane, t == Tool::Plane, true); + s->Show(m_box_loft, t == Tool::Loft, true); + s->Show(m_box_boolean, t == Tool::Boolean, true); + s->Show(m_box_cut, t == Tool::Cut, true); + s->Show(m_box_draft, t == Tool::Draft, true); + s->Show(m_box_insert, t == Tool::Insert, true); + + if (t == Tool::Revolve && m_revolve_sketch_ref >= 0 + && m_revolve_sketch_ref < int(m_doc.features.size())) + m_revolve_sketch_label->SetLabel(_L("Sketch: ") + + wxString::FromUTF8(m_doc.features[m_revolve_sketch_ref].name)); + + if (t == Tool::Sweep) { + if (m_sweep_profile_ref >= 0 && m_sweep_profile_ref < int(m_doc.features.size())) + m_sweep_profile_label->SetLabel(_L("Profile: ") + + wxString::FromUTF8(m_doc.features[m_sweep_profile_ref].name)); + // Populate the path picker with every Sketch feature except the profile itself; + // the feature index rides in the entry's client data. Pre-select the stored path + // (re-edit), else the first available sketch. + m_sweep_path->Clear(); + int sel_idx = wxNOT_FOUND; + for (int i = 0; i < int(m_doc.features.size()); ++i) { + const CadFeature& sf = m_doc.features[i]; + if (sf.type != CadFeatureType::Sketch || i == m_sweep_profile_ref) continue; + const int pos = m_sweep_path->Append(wxString::FromUTF8(sf.name), + reinterpret_cast(intptr_t(i))); + if (i == m_sweep_path_ref) sel_idx = pos; + } + if (sel_idx != wxNOT_FOUND) m_sweep_path->SetSelection(sel_idx); + else if (m_sweep_path->GetCount() > 0) m_sweep_path->SetSelection(0); + } + + if (t == Tool::Loft) { + // List every Sketch feature; the feature index for each row rides in + // m_loft_sketch_idx. Re-check the stored profile refs (re-edit). + m_loft_list->Clear(); + m_loft_sketch_idx.clear(); + for (int i = 0; i < int(m_doc.features.size()); ++i) { + const CadFeature& sf = m_doc.features[i]; + if (sf.type != CadFeatureType::Sketch) continue; + const int row = m_loft_list->Append(wxString::FromUTF8(sf.name)); + m_loft_sketch_idx.push_back(i); + if (std::find(m_loft_refs.begin(), m_loft_refs.end(), i) != m_loft_refs.end()) + m_loft_list->Check(row, true); + } + } + + if (t == Tool::Extrude) { + if (m_extrude_face_src >= 0) + m_extrude_sketch_label->SetLabel( + wxString::Format(_L("Face %d (push/pull)"), m_extrude_face_src)); + else if (m_extrude_sketch_ref >= 0 && m_extrude_sketch_ref < int(m_doc.features.size())) + m_extrude_sketch_label->SetLabel(_L("Sketch: ") + + wxString::FromUTF8(m_doc.features[m_extrude_sketch_ref].name)); + // A fresh extrude defaults to New body — even when other bodies exist — so + // overlapping extrudes stay SEPARATE solids instead of silently fusing. Joining + // is opt-in (pick "Join"). Engraving art onto a face still defaults to Cut. + // (Edit-mode keeps the feature's stored mode, set below.) + if (m_edit_index < 0) { + const bool on_face_import = + m_extrude_sketch_ref >= 0 && m_extrude_sketch_ref < int(m_doc.features.size()) + && m_doc.features[m_extrude_sketch_ref].import_on_face; + if (on_face_import) { + m_mode->SetSelection(2); // Cut — engrave into the face + m_flip->SetValue(true); // extrude inward (the face normal points out) + } else { + m_mode->SetSelection(0); // New body (was: Add when a body already existed) + } + } + } + + // Retitle the active card's header: edit-mode shows the feature's real name, + // add-mode previews the type + next feature number (Onshape "Extrude 1"). + const bool editing = (m_edit_index >= 0 && m_edit_index < int(m_doc.features.size())); + auto title = [&](const wxString& base) -> wxString { + return editing ? wxString::FromUTF8(m_doc.features[m_edit_index].name) + : base + wxString::Format(" %d", m_feature_counter + 1); + }; + switch (t) { + case Tool::Sketch: m_hdr_sketch->SetLabel(title(_L("Sketch"))); break; + case Tool::Extrude: m_hdr_extrude->SetLabel(title(_L("Extrude"))); break; + case Tool::Dressup: m_hdr_dressup->SetLabel(title( + m_dressup_type->GetSelection() == 0 ? _L("Fillet") : _L("Chamfer"))); break; + case Tool::Hole: m_hdr_hole->SetLabel(title(_L("Hole"))); break; + case Tool::Thread: m_hdr_thread->SetLabel(title(_L("Thread"))); break; + case Tool::Shell: m_hdr_shell->SetLabel(title(_L("Shell"))); break; + case Tool::Revolve: m_hdr_revolve->SetLabel(title(_L("Revolve"))); break; + case Tool::Sweep: m_hdr_sweep->SetLabel(title(_L("Sweep"))); break; + case Tool::Pattern: m_hdr_pattern->SetLabel(title(_L("Pattern"))); break; + case Tool::Plane: m_hdr_plane->SetLabel(title(_L("Plane"))); break; + case Tool::Loft: m_hdr_loft->SetLabel(title(_L("Loft"))); break; + case Tool::Draft: m_hdr_draft->SetLabel(title(_L("Draft"))); break; + case Tool::Boolean: m_hdr_boolean->SetLabel(title(_L("Boolean"))); break; + case Tool::Cut: m_hdr_cut->SetLabel(title(_L("Cut"))); break; + case Tool::Insert: break; // header set by open_insert_card() + case Tool::None: break; + } + + m_form->Layout(); + m_form->FitInside(); + update_action_bar(); // a tool is now active -> show the unified ✓/✗ + refresh_preview(); +} + +void DesignPanel::close_tool() +{ + m_active = Tool::None; + set_active_tool_btn(nullptr); // clear the active-tool teal highlight + if (m_viewport) { m_viewport->set_body_translucent(false); m_viewport->set_body_hidden(false); } // restore the opaque solid + wxSizer* s = m_form->GetSizer(); + s->Show(m_box_sketch, false, true); + s->Show(m_box_extrude, false, true); + s->Show(m_box_dressup, false, true); + s->Show(m_box_hole, false, true); + s->Show(m_box_thread, false, true); + s->Show(m_box_shell, false, true); + s->Show(m_box_revolve, false, true); + s->Show(m_box_sweep, false, true); + s->Show(m_box_pattern, false, true); + s->Show(m_box_plane, false, true); + s->Show(m_box_loft, false, true); + s->Show(m_box_boolean, false, true); + s->Show(m_box_cut, false, true); + s->Show(m_box_draft, false, true); + s->Show(m_box_insert, false, true); + m_viewport->clear_preview(); + m_viewport->clear_extrude_gizmo(); + m_viewport->clear_fillet_gizmo(); + m_viewport->clear_hole_gizmo(); + m_viewport->clear_thread_gizmo(); + m_viewport->clear_shell_gizmo(); + m_viewport->clear_revolve_gizmo(); + m_viewport->clear_pattern_gizmo(); + m_form->Layout(); + m_form->FitInside(); + update_action_bar(); // no feature tool active -> hide the bar (unless a mode keeps it) +} + +void DesignPanel::confirm_tool() +{ + // One undo boundary per committed feature (Extrude/Dressup/Hole/Thread/Shell, the + // legacy Sketch card via on_add_sketch, and edit-mode replace all funnel here). + m_doc.checkpoint(); + const bool editing_single = (m_edit_index >= 0); + + if (editing_single) { + // Edit mode: overwrite the existing feature instead of appending. + CadFeature cand = build_candidate(m_active); + bool ok = m_doc.replace_feature(m_edit_index, cand); + reset_edit_state(); + close_tool(); // clears the preview ghost + after_tree_edit(ok); // refresh tree/viewport/status (or "Edit rejected") + return; + } + + switch (m_active) { + case Tool::Sketch: on_add_sketch(); break; + case Tool::Extrude: on_add_extrude(); break; + case Tool::Dressup: on_add_dressup(); break; + case Tool::Hole: on_add_hole(); break; + case Tool::Thread: on_add_thread(); break; + case Tool::Shell: on_add_shell(); break; + case Tool::Revolve: on_add_revolve(); break; + case Tool::Sweep: on_add_sweep(); break; + case Tool::Pattern: on_add_pattern(); break; + case Tool::Plane: on_add_plane(); break; + case Tool::Loft: on_add_loft(); break; + case Tool::Draft: on_add_draft(); break; + case Tool::Boolean: on_add_boolean(); break; + case Tool::Cut: on_add_cut(); break; + case Tool::Insert: return; // committed via finalize_insert(), never here + case Tool::None: return; + } + close_tool(); // also clears the preview ghost; the committed body is now shown +} + +void DesignPanel::cancel_tool() +{ + reset_edit_state(); // abort an in-progress edit: back to add-mode + close_tool(); + // Cancel discards the candidate: clear the stale "Preview …"/"Invalid …" + // label and restore the neutral idle colour (Confirm keeps its "OK" status). + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(wxString()); + m_status->Refresh(); +} + +// One Confirm surface for the whole tab. Routes to the right commit by current context: +// a feature card, the Insert placement, the Sketch session, or the Constrain session. +void DesignPanel::tool_confirm() +{ + if (m_viewport && m_viewport->moving_body()) { // keep the placement, drop the gizmo + m_viewport->clear_move_gizmo(); + m_move_body = -1; + update_action_bar(); + set_status_ok(); + return; + } + if (m_active == Tool::Insert) { finalize_insert(); return; } + if (m_active != Tool::None) { confirm_tool(); return; } + if (m_ui_mode == UiMode::Sketch) { + if (m_viewport && m_viewport->is_sketching()) m_viewport->finish_sketch(); + set_ui_mode(UiMode::Feature); + return; + } + if (m_ui_mode == UiMode::Constrain) { + cancel_value(); + if (m_viewport) m_viewport->end_constrain(); + m_constrain_feat = -1; + set_ui_mode(UiMode::Feature); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(wxString()); + m_status->Refresh(); + } +} + +// One Cancel/exit surface (also bound to Esc). Discards the active feature/insert, or a +// drawn-but-uncommitted Sketch, or exits Constrain. +void DesignPanel::tool_cancel() +{ + if (m_viewport && m_viewport->moving_body()) { // revert to the pose at move-start + sync_body_xform(); + if (m_move_body >= 0 && m_move_body < int(m_body_xform.size())) + m_body_xform[m_move_body] = m_move_prev; + m_viewport->clear_move_gizmo(); + m_move_body = -1; + feed_bodies(); // re-render the reverted placement + update_action_bar(); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(_L("Move cancelled")); + m_status->Refresh(); + return; + } + if (m_active == Tool::Insert) { cancel_insert(); return; } + if (m_active != Tool::None) { cancel_tool(); return; } + if (m_ui_mode == UiMode::Sketch) { + if (m_viewport) m_viewport->cancel_sketch(); // drop the live session (committed art stays) + m_edit_index = -1; + set_ui_mode(UiMode::Feature); + sync_sketch_display(); + refresh_tree(); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(wxString()); + m_status->Refresh(); + return; + } + if (m_ui_mode == UiMode::Constrain) { + cancel_value(); + if (m_viewport) m_viewport->end_constrain(); + m_constrain_feat = -1; + set_ui_mode(UiMode::Feature); + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(wxString()); + m_status->Refresh(); + } +} + +void DesignPanel::update_undo_redo_buttons() +{ + // Grey Undo/Redo to mirror exactly what do_undo_redo will do: it acts only in Feature + // mode with no tool/dialog open (otherwise Esc is the way out), so reflect that gate here + // as well as the document's available history. + if (m_btn_undo == nullptr || m_btn_redo == nullptr) return; + const bool gated = (m_ui_mode != UiMode::Feature) || (m_active != Tool::None); + m_btn_undo->Enable(!gated && m_doc.can_undo()); + m_btn_redo->Enable(!gated && m_doc.can_redo()); +} + +void DesignPanel::update_action_bar() +{ + update_undo_redo_buttons(); // mode/tool changes flip the do_undo_redo gate -> refresh greying + if (m_tb_action == nullptr || m_toolbar == nullptr) return; + wxSizer* s = m_toolbar->GetSizer(); + if (s == nullptr) return; + const bool active = (m_active != Tool::None) + || m_ui_mode == UiMode::Sketch + || m_ui_mode == UiMode::Constrain + || (m_viewport && m_viewport->moving_body()); + s->Show(m_tb_action, active, true); + m_toolbar->Layout(); + m_toolbar->FitInside(); // refresh scroll range when the action bar shows/hides +} + +void DesignPanel::do_undo_redo(bool redo) +{ + // v1: act only in Feature mode. While authoring/constraining a sketch (m_ui_mode) or + // with a feature dialog open (m_active), Esc/Cancel is the way out — popping committed + // history mid-tool would be ambiguous (and could orphan the tool's referenced feature). + if (m_ui_mode != UiMode::Feature || m_active != Tool::None) { + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(_L("Finish or cancel the current tool first (Esc)")); + m_status->Refresh(); + return; + } + const bool ok = redo ? m_doc.redo() : m_doc.undo(); + if (!ok) { + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(redo ? _L("Nothing to redo") : _L("Nothing to undo")); + m_status->Refresh(); + return; + } + // The solid whole/face/edge pick and any in-place edit reference ids that recompute() + // invalidates — drop them before refreshing from the restored document. + m_sel_solid_body = m_sel_solid_face = m_sel_solid_edge = -1; + reset_edit_state(); + after_tree_edit(true); // refresh tree + viewport meshes + status from the restored doc + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(wxString::Format(redo ? _L("Redo (%zu more)") : _L("Undo (%zu more)"), + redo ? m_doc.redo_depth() : m_doc.undo_depth())); + m_status->Refresh(); +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/DesignPanel.hpp b/src/slic3r/GUI/DesignPanel.hpp new file mode 100644 index 0000000000..b7af57c97d --- /dev/null +++ b/src/slic3r/GUI/DesignPanel.hpp @@ -0,0 +1,420 @@ +#ifndef slic3r_DesignPanel_hpp_ +#define slic3r_DesignPanel_hpp_ + +#include +#include +#include // wxTreeItemId + +#include +#include +#include + +#include "libslic3r/CadDocument.hpp" + +class wxChoice; +class wxCheckBox; +class wxCheckListBox; +class wxSpinCtrl; +class wxSpinCtrlDouble; +class wxTreeCtrl; +class wxImageList; +class wxStaticText; +class wxSizer; +class wxButton; +class wxPanel; +class ScalableButton; + +namespace Slic3r { namespace GUI { + +class DesignCanvas; + +// Design (CAD) tab: a sketch-first, Onshape-style form-driven CAD panel. +// Sketch and Extrude are independent tools: the user creates a Sketch first, +// then selects it and Extrudes to produce a solid. +class DesignPanel : public wxPanel +{ +public: + explicit DesignPanel(wxWindow* parent); + void on_tab_shown(); // re-sync bed to the active printer when the Design tab is activated + +private: + enum class Tool { None, Sketch, Extrude, Dressup, Hole, Thread, Shell, Revolve, Sweep, Pattern, Plane, Loft, Draft, Boolean, Cut, Insert }; + + // Onshape-style contextual top toolbar: only the active mode's tool group is + // shown (Feature = sketch/extrude/dress/hole/thread; Sketch = entity tools; + // Constrain = constraints + edit ops). Replaces the old always-visible wall. + enum class UiMode { Feature, Sketch, Constrain }; + void set_ui_mode(UiMode m); + // Unified action-bar dispatch: one Confirm / one Cancel for every tool and mode. + void tool_confirm(); // ✓ : commit the active feature / sketch / constrain session + void tool_cancel(); // ✗ / Esc : cancel the active feature / discard / exit + void update_action_bar(); // show the ✓/✗ bar iff a tool or mode is active + + void on_shape_changed(); + void on_add_sketch(); + void on_add_extrude(); + void on_add_dressup(); + void on_add_hole(); + void on_add_thread(); + void apply_thread_standard(); // fill pitch/depth/radius from m_thread_std selection + void on_add_revolve(); + void on_add_sweep(); + void on_add_loft(); + void on_add_pattern(); + void on_add_plane(); + void on_add_shell(); + void on_add_draft(); + void on_add_boolean(); + void on_add_cut(); // commit a plane Cut (split-by-plane) + void populate_body_choices(); // fill m_bool_target / m_bool_tool / m_cut_target from m_doc.bodies + // Import rigid 2D art (Text / SVG) as a new Sketch feature carrying + // imported_regions (no solver entities). on_add_text/on_import_svg gather + // input; add_imported_sketch builds the feature, refreshes tree + display. + void on_add_text(); + void on_import_svg(); + void on_import_step(); // STEP -> editable B-rep body (keeps the OCCT solid, not a mesh) + bool place_on_face(); // Prepare's Place on Face (F): lay the selected body face on the bed + void add_imported_sketch(const std::vector>>& regions, + const wxString& base_name); + // Imported Text/SVG art is placed/sized in-canvas then explicitly committed via a + // small Confirm/Cancel card (Onshape Button->Dialog->Preview->Confirm). The feature + // is added provisionally by add_imported_sketch; Confirm keeps it, Cancel undoes it. + void open_insert_card(const wxString& base_name); + void finalize_insert(); // Confirm: keep the placed art, leave the placement gizmo + void cancel_insert(); // Cancel: undo the provisional insert + // Move / enlarge / stretch (independent X/Y) an imported Text/SVG sketch: + // a modal dialog editing the feature's placement transform in place. + void on_transform_imported(int feat_idx); + void on_commit(); + void refresh_tree(); + void set_status_ok(); + + // Feature-tree editing (Onshape-style): act on the selected tree row. + void on_delete_feature(); + void on_move_feature(int delta); // -1 = up, +1 = down + void on_toggle_visibility(); // show/hide the selected feature (CadFeature::enabled) + + // Constrain mode: enter on the tree-selected sketch, then apply a geometric + // constraint to the in-canvas picked segment and re-solve in the kernel. + void on_begin_constrain(int sel_override = -1); + // Sketch-toolbar Constrain entry: commit the live sketch in place, then enter Constrain + // mode on it (so the constraint palette + Trim/Extend are reachable without leaving the + // sketch flow). Returns true if constrain mode was entered. + bool enter_constrain_inline(); + void apply_constraint(SketchConstraintType type); + void apply_entity_constraint(SketchConstraintType type); // Fase 4.2 entity path + enum class EditOp { Mirror, Offset, Fillet, Trim, Extend, Array, Move, Chamfer, Rotate, Scale, PolarArray }; // Fase 4.4/4.5/4.6 sketch edit ops + void apply_edit_op(EditOp op); // mutate selected sketch entities + // Onshape-style docked value entry (replaces wxGetTextFromUser popups for + // Angle/Radius/Diameter constraints + Offset/Fillet edit ops). request_value + // shows the card and stows a continuation run by confirm_value(). + void request_value(const wxString& label, double def, double mn, double mx, + std::function cont, + std::function on_cancel = nullptr); + void confirm_value(); + void cancel_value(); + void commit_entity_constraint(const SketchEntityConstraintDef& def); // shared solve/refresh tail + void commit_entity_constraints(const std::vector& defs); // multi-def (Symmetric) + + // Constraint manager (C3.4): a docked list of the constrained sketch's + // entity-constraints with per-row select (highlight the referenced entities in + // the viewport) and delete (drop the constraint + re-solve). Shown in Constrain + // mode only; operates on m_doc.features[m_constrain_feat].entity_constraints. + void rebuild_constraint_list(); // refill m_constraint_rows + void delete_constraint(int idx); // erase + re-solve + refresh + void highlight_constraint_entities(int idx); // push referenced entities to viewport + void refresh_constrain_dof(); // re-solve feature, mirror DoF readout + wxString constraint_label(const SketchEntityConstraintDef& d) const; // human-readable row text + void after_edit_op(); // shared edit-op refresh tail + void on_edit_feature(); // reopen the selected feature's dialog populated + void after_tree_edit(bool ok); // shared post-op refresh of tree/viewport/status + void load_feature_into_dialog(const CadFeature& f); + void reset_edit_state(); // back to add-mode (m_edit_index = -1) + + // Onshape loop: Button -> open_tool (show dialog) -> refresh_preview (ghost) -> + // confirm_tool (commit) / cancel_tool (abort). + void open_tool(Tool t); + void close_tool(); + void refresh_preview(); + void confirm_tool(); + void cancel_tool(); + // Ctrl+Z / Ctrl+Shift+Z (Ctrl+Y) from the viewport. With a tool/dialog open it + // cancels that (Esc-like); otherwise it undoes/redoes the committed feature history. + void do_undo_redo(bool redo); + // The plane the Hole tool drills on: a picked face (inward, centred) or the dropdown. + SketchPlane hole_plane() const; + // The plane the Thread tool builds on: a picked cylindrical face (axis) or the dropdown. + SketchPlane thread_plane() const; + CadFeature build_candidate(Tool t) const; + int resolve_extrude_sketch() const; + // Plane pickers: fill a choice with XY/XZ/YZ + the document's datum planes, and + // map a choice row back to the actual SketchPlane (rows 0-2 base, 3+ datum). + void populate_plane_choices(wxChoice* c) const; + SketchPlane plane_from_choice(int row) const; + // True when Extrude should build only the click-selected loop (a region of the + // resolved sketch is selected and it carries entities). + bool extrude_uses_loop() const; + void sync_sketch_display(); // push un-consumed committed sketches to the viewport + // Feed the viewport's visual Extrude depth-arrow gizmo (C5b) with the current profile + // plane + centroid + live depths while the Extrude card is open (self-gates on m_active). + void update_extrude_gizmo(); + void update_fillet_gizmo(); // edge-anchored radius arrow (Dressup card) + void update_hole_gizmo(); // footprint circle + diameter/depth arrows (Hole card) + void update_thread_gizmo(); // footprint circle + radius/length arrows (Thread card) + void update_shell_gizmo(); // inward thickness arrow on the picked face (Shell card) + void update_revolve_gizmo(); // angle-arc around the axis (Revolve card) + void update_pattern_gizmo(); // linear spacing arrow / circular angle-arc (Pattern card) + + CadDocument m_doc; + + Tool m_active{Tool::None}; + wxSizer* m_box_sketch{nullptr}; + wxSizer* m_box_extrude{nullptr}; + wxSizer* m_box_dressup{nullptr}; + wxSizer* m_box_hole{nullptr}; + wxSizer* m_box_thread{nullptr}; + wxSizer* m_box_shell{nullptr}; + wxSizer* m_box_revolve{nullptr}; + wxSizer* m_box_sweep{nullptr}; + wxSizer* m_box_pattern{nullptr}; + wxSizer* m_box_plane{nullptr}; + wxSizer* m_box_loft{nullptr}; + wxSizer* m_box_draft{nullptr}; + wxSizer* m_box_boolean{nullptr}; + wxSizer* m_box_cut{nullptr}; + wxSizer* m_box_insert{nullptr}; // Confirm/Cancel card for placing Text/SVG art + int m_insert_feat{-1}; // provisional imported-art feature awaiting Confirm + // Move-body gizmo runs through the unified action bar too: Confirm keeps the placement, + // Cancel reverts to the pose captured when the move started. + int m_move_body{-1}; + Transform3d m_move_prev{Transform3d::Identity()}; + + // Onshape-style dialog-card title rows (icon + bold feature name), retitled + // per tool in open_tool() (edit-mode shows the feature's actual name). + wxStaticText* m_hdr_sketch{nullptr}; + // Onshape sketch-entry card (plane/orientation) that opens on "New sketch" and + // persists until Finish (Phase 3). + wxSizer* m_box_sketch_session{nullptr}; + wxStaticText* m_hdr_sketch_session{nullptr}; + wxStaticText* m_hdr_extrude{nullptr}; + wxStaticText* m_hdr_dressup{nullptr}; + wxStaticText* m_hdr_hole{nullptr}; + wxStaticText* m_hdr_thread{nullptr}; + wxStaticText* m_hdr_shell{nullptr}; + wxStaticText* m_hdr_revolve{nullptr}; + wxStaticText* m_hdr_sweep{nullptr}; + wxStaticText* m_hdr_pattern{nullptr}; + wxStaticText* m_hdr_plane{nullptr}; + wxStaticText* m_hdr_loft{nullptr}; + wxStaticText* m_hdr_draft{nullptr}; + wxStaticText* m_hdr_boolean{nullptr}; + wxStaticText* m_hdr_cut{nullptr}; + wxStaticText* m_hdr_insert{nullptr}; + + wxScrolledWindow* m_form{nullptr}; + DesignCanvas* m_viewport{nullptr}; + + // Top contextual toolbar (parented to the panel, above the form/viewport row). + UiMode m_ui_mode{UiMode::Feature}; + wxScrolledWindow* m_toolbar{nullptr}; // horizontally scrollable so the action bar stays reachable on narrow windows + wxSizer* m_tb_feature{nullptr}; + wxSizer* m_tb_sketch{nullptr}; + wxSizer* m_tb_constrain{nullptr}; + // Unified Confirm/Cancel action bar (right end of the ribbon). Shown whenever any + // tool or mode is active; the single confirm/cancel surface for the whole tab. + wxSizer* m_tb_action{nullptr}; + // Persistent Undo/Redo group at the left of the ribbon — always visible, independent + // of the mode-gated tool groups. The buttons are greyed per the document history and + // the do_undo_redo gate (see update_undo_redo_buttons). + wxSizer* m_tb_history{nullptr}; + ScalableButton* m_btn_undo{nullptr}; + ScalableButton* m_btn_redo{nullptr}; + void update_undo_redo_buttons(); // enable/disable Undo/Redo from can_undo/can_redo + gate + // All tool buttons, for the active-tool teal highlight (Onshape-style). + std::vector m_tool_btns; + ScalableButton* m_active_tool_btn{nullptr}; + void set_active_tool_btn(ScalableButton* b); // nullptr clears the highlight + // Owns the themed DropDown flyouts (and the item vectors they hold by ref). + std::vector> m_flyout_keepalive; + wxCheckBox* m_construction{nullptr}; // sketch-mode construction toggle + wxSpinCtrl* m_sides{nullptr}; // polygon sides + wxCheckBox* m_poly_circ{nullptr}; // polygon circumscribed toggle + + wxChoice* m_draw_plane{nullptr}; + wxChoice* m_shape{nullptr}; + wxChoice* m_plane{nullptr}; + wxChoice* m_mode{nullptr}; + wxSpinCtrlDouble* m_width{nullptr}; + wxSpinCtrlDouble* m_height{nullptr}; + wxSpinCtrlDouble* m_radius{nullptr}; + wxSpinCtrlDouble* m_distance{nullptr}; + wxChoice* m_extrude_end{nullptr}; // Blind/Symmetric/TwoSided/ThroughAll/UpTo* + wxSpinCtrlDouble* m_distance2{nullptr}; // second-side depth (Two-sided) + wxSpinCtrlDouble* m_taper{nullptr}; // draft angle (deg) + wxCheckBox* m_flip{nullptr}; // reverse extrude direction + + wxStaticText* m_extrude_sketch_label{nullptr}; + int m_extrude_sketch_ref{-1}; + + // Revolve controls (sweep a sketch profile about an in-plane axis). + wxStaticText* m_revolve_sketch_label{nullptr}; + wxSpinCtrlDouble* m_revolve_angle{nullptr}; + wxChoice* m_revolve_axis{nullptr}; // 0 = plane X, 1 = plane Y + wxChoice* m_revolve_mode{nullptr}; // New/Add/Cut/Intersect + wxCheckBox* m_revolve_flip{nullptr}; + int m_revolve_sketch_ref{-1}; + + // Sweep controls (sweep a profile sketch along a path sketch). + wxStaticText* m_sweep_profile_label{nullptr}; + wxChoice* m_sweep_path{nullptr}; // path Sketch picker (feature index in client data) + wxChoice* m_sweep_mode{nullptr}; // New/Add/Cut/Intersect + int m_sweep_profile_ref{-1}; + int m_sweep_path_ref{-1}; // path Sketch feature index (for re-edit pre-select) + + // Loft controls (skin a solid through 2+ ordered profile Sketches). + wxCheckListBox* m_loft_list{nullptr}; // every Sketch; check 2+ in list order = profiles + wxCheckBox* m_loft_ruled{nullptr}; // ruled (straight) vs smooth sections + wxChoice* m_loft_mode{nullptr}; // New/Add/Cut/Intersect + std::vector m_loft_sketch_idx; // feature index for each row in m_loft_list + std::vector m_loft_refs; // chosen profile refs (for re-edit pre-check) + + // Pattern controls (replicate the target body: linear or circular). + wxChoice* m_pattern_type{nullptr}; // 0 = Linear, 1 = Circular + wxSpinCtrlDouble* m_pattern_count{nullptr}; // total instances incl. seed + wxSpinCtrlDouble* m_pattern_spacing{nullptr}; // linear step (mm) + wxChoice* m_pattern_dir{nullptr}; // linear direction: 0 = plane X, 1 = plane Y + wxSpinCtrlDouble* m_pattern_angle{nullptr}; // circular total angle (deg) + // Boolean controls (combine two existing bodies). + wxChoice* m_bool_op{nullptr}; // 0 = Union, 1 = Subtract, 2 = Intersect + wxChoice* m_bool_target{nullptr}; // body that survives (selection == body index) + wxChoice* m_bool_tool{nullptr}; // body consumed (selection == body index) + wxCheckBox* m_bool_keep{nullptr}; // keep the tool body after the op + wxSpinCtrlDouble* m_bool_tol{nullptr}; // OCCT fuzzy tolerance (mm); robust cut on near-coincident faces + + // Plane Cut (split-by-plane): a reference plane + offset splits the target body into + // two separate bodies (both pieces kept). + wxChoice* m_cut_plane{nullptr}; // XY/XZ/YZ + datum planes (cut plane) + wxChoice* m_cut_target{nullptr}; // body to cut (selection == body index) + wxSpinCtrlDouble* m_cut_offset{nullptr}; // offset along the plane normal (mm) + // Datum plane controls (derive a selectable sketch plane: offset + tilt from a base). + wxChoice* m_plane_base{nullptr}; // 0=XY,1=XZ,2=YZ, 3+N = Nth datum plane + wxSpinCtrlDouble* m_plane_offset{nullptr}; // offset along base normal (mm) + wxSpinCtrlDouble* m_plane_tilt{nullptr}; // tilt about a base axis (deg) + wxChoice* m_plane_tilt_axis{nullptr}; // 0 = base X, 1 = base Y + // Plate loop selection (click a committed sketch loop): the Sketch feature + the + // clicked closed-region index, so Extrude builds just that one loop. -1 = none. + int m_sel_sketch_feat{-1}; + int m_sel_sketch_region{-1}; + // Click-selected solid topology (whole/face/edge cycle): face id for up-to-face / dress-up. + int m_sel_solid_body{-1}; // which body the face/edge selection is on + int m_sel_solid_face{-1}; + int m_sel_solid_edge{-1}; + // Face-as-profile extrude (Onshape): when Extrude is opened on a picked solid face with + // no sketch source, this carries that global face id so the kernel extrudes the face. + // -1 = ordinary sketch/loop extrude. Set when opening the Extrude card, consumed on add. + int m_extrude_face_src{-1}; + + wxChoice* m_dressup_type{nullptr}; + wxChoice* m_face_group{nullptr}; + wxSpinCtrlDouble* m_dressup_size{nullptr}; + + wxChoice* m_hole_plane{nullptr}; + wxSpinCtrlDouble* m_hole_diameter{nullptr}; + wxSpinCtrlDouble* m_hole_depth{nullptr}; + wxCheckBox* m_hole_through{nullptr}; + wxSpinCtrlDouble* m_hole_x{nullptr}; + wxSpinCtrlDouble* m_hole_y{nullptr}; + // #2: when the Hole tool is opened on a picked solid face, drill on that face centred + // on it (origin = face centroid, normal = inward). m_hole_x/y then read as the offset + // from the face centre. Falls back to the m_hole_plane dropdown when no face is picked. + bool m_hole_on_face{false}; + SketchPlane m_hole_face_plane; + int m_hole_face_body{-1}; + // #2 Part B: the picked face's (u,v) bounds in m_hole_face_plane, so the hole's construction + // dims read as distance from the face sides (umin/vmin edges) rather than from the centre. + bool m_hole_has_bounds{false}; + double m_hole_umin{0}, m_hole_umax{0}, m_hole_vmin{0}, m_hole_vmax{0}; + + wxChoice* m_thread_plane{nullptr}; + wxChoice* m_thread_std{nullptr}; // standard designation (M6, 1/4-20 UNC, ...) + wxSpinCtrlDouble* m_thread_radius{nullptr}; + wxSpinCtrlDouble* m_thread_pitch{nullptr}; + wxSpinCtrlDouble* m_thread_height{nullptr}; + wxSpinCtrlDouble* m_thread_depth{nullptr}; + wxCheckBox* m_thread_internal{nullptr}; + wxSpinCtrlDouble* m_thread_x{nullptr}; + wxSpinCtrlDouble* m_thread_y{nullptr}; + // #3: when the Thread tool is opened on a picked cylindrical face (a hole bore or a + // cylinder), thread that surface — plane on its axis, radius/internal derived from it. + bool m_thread_on_face{false}; + SketchPlane m_thread_face_plane; + int m_thread_face_body{-1}; + + wxSpinCtrlDouble* m_shell_thickness{nullptr}; + wxStaticText* m_shell_face_label{nullptr}; // shows the picked face to remove + + // Draft controls (taper a single picked solid face about the body bottom). + wxSpinCtrlDouble* m_draft_angle{nullptr}; + wxStaticText* m_draft_face_label{nullptr}; // shows the picked face to draft + + // Onshape-style docked value-entry card (Angle/Radius/Diameter/Offset/Fillet). + wxSizer* m_box_value{nullptr}; + wxStaticText* m_value_label{nullptr}; + wxTextCtrl* m_value_input{nullptr}; // plain text field: forces en ('.') decimals + double m_value_min{0.0}; // range for confirm-time clamping + double m_value_max{0.0}; + std::function m_value_cont; // deferred apply, run on Confirm + std::function m_value_cancel; // optional action when the card is cancelled + + // Feature tree: a wxTreeCtrl with per-feature-type icons. Callers keep using + // integer row indices via tree_selection()/set_tree_selection(); m_tree_items + // maps feature order -> tree node, rebuilt by refresh_tree(). + wxTreeCtrl* m_tree{nullptr}; + wxImageList* m_tree_images{nullptr}; + std::vector m_tree_items; + // Parts list: tree rows for each body (parallel to m_doc.bodies). Selecting one + // highlights that body and makes it the target for the next op. + std::vector m_tree_body_items; + // Per-body visibility (parallel to m_doc.bodies; index stable across recompute since + // bodies are appended in feature order). Empty/grown to all-visible by sync_body_visible(). + std::vector m_body_visible; + void sync_body_visible(); // grow/shrink m_body_visible to bodies.size() + // Per-body display translation (Move-body, M5). Parallel to m_doc.bodies; default + // identity. Applied to the display/pick meshes only — the OCCT shape (and face/edge + // global ids) is never touched, so dress-up targeting stays stable across a move. + std::vector m_body_xform; + std::vector m_disp_body_meshes; // display_body_meshes with m_body_xform applied + TriangleMesh m_disp_pick_mesh; // combined pick mesh with m_body_xform applied + void sync_body_xform(); // grow m_body_xform to bodies.size() (identity) + void rebuild_disp_meshes(); // recompute m_disp_* from m_doc + m_body_xform + void feed_bodies(); // push m_disp_* + visibility/xform to the viewport + void on_move_body(); // start the move gizmo on the selected body + void on_set_body_color(); // Color tool: pick a per-body display colour override + int tree_selection() const; // selected feature row, or wxNOT_FOUND + int tree_body_selection() const; // selected Parts-list body index, or -1 + void set_tree_selection(int row); + static int tree_icon_for(CadFeatureType t); + + wxStaticText* m_status{nullptr}; + wxStaticText* m_dof_status{nullptr}; // DoF / constraint-state readout (P3) + int m_feature_counter{0}; + + std::vector m_confirm_btns; + + // Edit-in-place state: add-mode is m_edit_index == -1. Single-feature edit + // (Sketch or Extrude independently) uses only m_edit_index as the row to replace. + int m_edit_index{-1}; + + // Tree row of the sketch currently being constrained (-1 = not constraining). + int m_constrain_feat{-1}; + + // Constraint-manager card (C3.4): header + a rebuildable list of constraint rows. + wxSizer* m_box_constraints{nullptr}; + wxStaticText* m_hdr_constraints{nullptr}; + wxSizer* m_constraint_rows{nullptr}; + int m_constraint_sel{-1}; // highlighted constraint row, or -1 +}; + +}} // namespace Slic3r::GUI + +#endif // slic3r_DesignPanel_hpp_ diff --git a/src/slic3r/GUI/DesignSketchTool.cpp b/src/slic3r/GUI/DesignSketchTool.cpp new file mode 100644 index 0000000000..f4784ab139 --- /dev/null +++ b/src/slic3r/GUI/DesignSketchTool.cpp @@ -0,0 +1,7906 @@ +#include "DesignSketchTool.hpp" +#include "GLCanvas3D.hpp" +#include "GUI_App.hpp" +#include "Plater.hpp" +#include "Camera.hpp" +#include "3DScene.hpp" +#include "GLShader.hpp" +#include "libslic3r/GeometryEngine.hpp" +#include "libslic3r/TriangleMesh.hpp" + +#include +#include +#include +#include +#include +#include + +namespace Slic3r { +namespace GUI { + +// Positioning helpers (defined lower down, used by the dimension methods above them). +static bool entity_ref_point(const SketchEntity& e, Vec2d& out); +static void translate_entity(SketchEntity& e, const Vec2d& d); +// Pick-distance helpers (defined lower down; used earlier by the edit-op gizmo). +static double point_segment_dist(const Vec2d& p, const Vec2d& a, const Vec2d& b); +static double entity_pick_dist(const Vec2d& p, const SketchEntity& e); +static bool ray_triangle(const Vec3d& ro, const Vec3d& rd, const Vec3d& v0, const Vec3d& v1, + const Vec3d& v2, double& t); +static double ray_segment_dist3(const Vec3d& ro, const Vec3d& rd, const Vec3d& a, const Vec3d& b); + +// Project a world-space point to canvas screen pixels (device px, GL viewport units; +// origin top-left after the GL y-flip). Mirrors GLCanvas3D's world->screen pattern: +// projection * view, perspective divide, NDC -> viewport. Returns (-1,-1) if behind. +// (Retained for auto-emitted dimensions in Phase B, which have no click to anchor to; +// needs the design canvas's own camera/viewport, not the plater's.) +[[maybe_unused]] static wxPoint world_to_screen_px(const Camera& cam, const Vec3d& world) +{ + const Eigen::Matrix4d m = (cam.get_projection_matrix() * cam.get_view_matrix()).matrix(); + const Eigen::Vector4d clip = m * world.homogeneous(); + if (std::abs(clip.w()) < 1e-9) return wxPoint(-1, -1); + const Vec3d ndc = clip.head<3>() / clip.w(); + const std::array& vp = cam.get_viewport(); + const double sx = vp[0] + (ndc.x() * 0.5 + 0.5) * vp[2]; + const double sy = vp[1] + (1.0 - (ndc.y() * 0.5 + 0.5)) * vp[3]; // GL y-up -> wx y-down + return wxPoint(int(sx + 0.5), int(sy + 0.5)); +} + +void DesignSketchTool::begin(const SketchPlane& plane, Mode mode) +{ + m_plane = plane; + m_mode = mode; + m_points.clear(); + m_entities.clear(); + m_construction = false; + m_has_cursor = false; + m_sel_a = m_sel_b = -1; + m_constrain_entities = false; + m_pick0 = m_pick1 = m_pick2 = -1; + m_constraint_hl.clear(); + m_constrain_cons.clear(); + m_awaiting_length = false; + m_autoedit_seen = 0; // baseline: no entities yet; first commit triggers edit + m_autoedit_pending = false; + m_selection.clear(); + m_point_sel.clear(); + m_constraints.clear(); + m_dimensions.clear(); + m_dim_has0 = false; + m_pending_dim = -1; + m_dof = -1; m_solve_ok = true; m_entity_conflict.clear(); + m_features.clear(); + m_open_feature = -1; + m_active = true; +} + +// Re-open a committed entity sketch for editing: mirror begin() (fresh Select session), +// then load the geometry + driving constraints, re-detect feature groups, and live-solve +// so handles/quotes/regular-drag work exactly as in the original draw session. +void DesignSketchTool::begin_edit(const std::vector& entities, + const std::vector& constraints, + const SketchPlane& plane) +{ + begin(plane, Mode::Select); + m_entities = entities; + m_constraints = constraints; + rebuild_features_from_entities(); + resolve_live(); +} + +// Walk the entity list grouping each consecutive CLOSED chain (entities are stored in +// gesture order, so a polygon/rect/slot's members are contiguous and end-to-end linked) +// and classify it: L,A,L,A -> Slot; 4 arcs (2 concentric + 2 caps) -> ArcSlot; +// L,A×4 (4 equal-radius corner arcs) -> RoundedRect; 4 right-angled lines -> Rect; +// N equal-length lines -> Polygon. Single/irregular entities stay ungrouped (they fall +// back to per-entity quotes). This reconstructs m_features for a re-opened sketch. +void DesignSketchTool::rebuild_features_from_entities() +{ + m_features.clear(); + m_open_feature = -1; + const int n = int(m_entities.size()); + using T = SketchEntity::Type; + auto start = [&](int i) { return m_entities[i].p0; }; + auto end = [&](int i) { const SketchEntity& e = m_entities[i]; + return (e.type == T::Line || e.type == T::Arc) ? e.p1 : e.p0; }; + auto near = [](const Vec2d& a, const Vec2d& b) { + return (a - b).norm() <= 0.05 + 1e-3 * std::max(a.norm(), b.norm()); }; + + int i = 0; + while (i < n) { + int j = i; bool closed = false; // greedily extend a consecutive chain + while (j + 1 < n && near(end(j), start(j + 1))) { + ++j; + if ((j - i) >= 2 && near(end(j), start(i))) { closed = true; break; } + } + const int cnt = j - i + 1; + if (!closed || cnt < 3) { ++i; continue; } + + int arcs = 0; bool all_line = true; + for (int k = i; k <= j; ++k) { + if (m_entities[k].type == T::Arc) ++arcs; + if (m_entities[k].type != T::Line) all_line = false; + } + Vec2d c(0, 0); for (int k = i; k <= j; ++k) c += start(k); c /= double(cnt); + Feature f; f.begin = i; f.end = j + 1; f.c0 = c; + + if (cnt == 4 && arcs == 2 && + m_entities[i].type == T::Line && m_entities[i + 1].type == T::Arc && + m_entities[i + 2].type == T::Line && m_entities[i + 3].type == T::Arc) { + f.kind = FeatureKind::Slot; // make_slot: top, cap@c1, bottom, cap@c0 + f.c0 = m_entities[i + 3].center; + f.c1 = m_entities[i + 1].center; + f.param = m_entities[i + 1].radius; + m_features.push_back(f); + } else if (cnt == 4 && arcs == 4 && + near(m_entities[i].center, m_entities[i + 2].center) && + std::abs(m_entities[i + 1].radius - m_entities[i + 3].radius) <= + 0.02 * std::max(m_entities[i + 1].radius, 1e-6) && + m_entities[i].radius > m_entities[i + 2].radius) { + // make_arc_slot: [0]outer(Rc+w), [1]cap@E(w), [2]inner(Rc-w), [3]cap@S(w). + // c0=main centre, c1=centreline start (cap@S centre = Sc), param=half-width. + f.kind = FeatureKind::ArcSlot; + f.c0 = m_entities[i].center; + f.c1 = m_entities[i + 3].center; + f.param = m_entities[i + 3].radius; + m_features.push_back(f); + } else if (cnt == 8 && arcs == 4 && + m_entities[i].type == T::Line && m_entities[i + 1].type == T::Arc && + m_entities[i + 2].type == T::Line && m_entities[i + 3].type == T::Arc && + m_entities[i + 4].type == T::Line && m_entities[i + 5].type == T::Arc && + m_entities[i + 6].type == T::Line && m_entities[i + 7].type == T::Arc) { + // rounded_rect_entities: 4 corner arcs (equal radius r) at the inset corners. + // Recover the axis-aligned bounds from the arc centres ± r. + const double r = m_entities[i + 1].radius; + bool equal_r = true; + for (int k : {3, 5, 7}) + if (std::abs(m_entities[i + k].radius - r) > 0.02 * std::max(r, 1e-6)) + equal_r = false; + if (equal_r && r > 1e-6) { + double cxmin = 1e18, cxmax = -1e18, cymin = 1e18, cymax = -1e18; + for (int k : {1, 3, 5, 7}) { + const Vec2d& o = m_entities[i + k].center; + cxmin = std::min(cxmin, o.x()); cxmax = std::max(cxmax, o.x()); + cymin = std::min(cymin, o.y()); cymax = std::max(cymax, o.y()); + } + f.kind = FeatureKind::RoundedRect; + f.c0 = Vec2d(cxmin - r, cymin - r); // (xmin,ymin) + f.c1 = Vec2d(cxmax + r, cymax + r); // (xmax,ymax) + f.param = r; + m_features.push_back(f); + } + } else if (all_line) { + std::vector sidelen(cnt); + for (int k = 0; k < cnt; ++k) sidelen[k] = (m_entities[i + k].p1 - m_entities[i + k].p0).norm(); + const double lmin = *std::min_element(sidelen.begin(), sidelen.end()); + const double lmax = *std::max_element(sidelen.begin(), sidelen.end()); + const bool equal_sides = lmin > 1e-6 && (lmax - lmin) / lmax < 0.02; + bool all_right = true; + for (int k = 0; k < cnt && all_right; ++k) { + Vec2d u = m_entities[i + k].p1 - m_entities[i + k].p0; + Vec2d v = m_entities[i + (k + 1) % cnt].p1 - m_entities[i + (k + 1) % cnt].p0; + if (u.norm() < 1e-9 || v.norm() < 1e-9) { all_right = false; break; } + if (std::abs(u.normalized().dot(v.normalized())) > 0.06) all_right = false; // ~3.4° + } + if (cnt == 4 && all_right) { + f.kind = FeatureKind::CornerRect; f.c0 = start(i); f.c1 = start(i + 2); + m_features.push_back(f); + } else if (equal_sides) { + f.kind = FeatureKind::Polygon; f.c0 = c; f.c1 = start(i); + f.sides = cnt; f.param = (start(i) - c).norm(); + m_features.push_back(f); + } + } + i = j + 1; + } +} + +void DesignSketchTool::set_tool(Mode mode) +{ + // Switch the active drawing tool without dropping accumulated entities. + m_mode = mode; + m_points.clear(); + m_has_cursor = false; + m_awaiting_length = false; + m_autoedit_seen = int(m_entities.size()); // resync baseline so a switch never fires + m_autoedit_pending = false; + reset_op(); // drop any in-progress edit-op gizmo + reset_tf(); // drop any in-progress transform gizmo + m_selection.clear(); + if (on_selection_changed) on_selection_changed(0); +} + +void DesignSketchTool::cancel() +{ + m_active = false; + m_points.clear(); + m_entities.clear(); + m_construction = false; + m_has_cursor = false; + m_sel_a = m_sel_b = -1; + m_constrain_entities = false; + m_pick0 = m_pick1 = m_pick2 = -1; + m_constraint_hl.clear(); + m_constrain_cons.clear(); + m_awaiting_length = false; + m_selection.clear(); + m_point_sel.clear(); + m_constraints.clear(); + m_dimensions.clear(); + m_dim_has0 = false; + m_pending_dim = -1; + m_dof = -1; m_solve_ok = true; m_entity_conflict.clear(); + m_features.clear(); + m_open_feature = -1; + reset_op(); + reset_xform(); + reset_tf(); +} + +// Esc while active: layered exit (Onshape-like). Abort an in-progress entity first, then +// drop a draw tool back to Select; only an idle Select session exits to Feature mode. +void DesignSketchTool::request_exit() +{ + if (!m_points.empty()) { m_points.clear(); m_has_cursor = false; return; } + if (m_mode != Mode::Select) { set_tool(Mode::Select); return; } + if (on_exit) on_exit(); else cancel(); +} + +void DesignSketchTool::request_undo_redo(bool redo) +{ + if (on_undo_redo) on_undo_redo(redo); +} + +void DesignSketchTool::clear_selection() +{ + if (m_selection.empty() && m_point_sel.empty()) return; + m_selection.clear(); + m_point_sel.clear(); + if (on_selection_changed) on_selection_changed(0); +} + +void DesignSketchTool::delete_selected() +{ + if (m_selection.empty()) return; + const int n = int(m_entities.size()); + std::vector del(n, false); + for (int i : m_selection) + if (i >= 0 && i < n) del[i] = true; + // old index -> new index (or -1 if deleted), to fix up constraint references. + std::vector remap(n, -1); + int next = 0; + for (int i = 0; i < n; ++i) + if (!del[i]) remap[i] = next++; + for (int i = n - 1; i >= 0; --i) + if (del[i]) m_entities.erase(m_entities.begin() + i); + // Drop constraints touching a deleted entity; remap the survivors. + std::vector kept; + auto live = [&](int e) { return e < 0 || (e < n && remap[e] >= 0); }; + auto map = [&](int e) { return e < 0 ? -1 : remap[e]; }; + for (SketchEntityConstraintDef c : m_constraints) { + if (!live(c.ea) || !live(c.eb) || !live(c.ec)) continue; + c.ea = map(c.ea); c.eb = map(c.eb); c.ec = map(c.ec); + kept.push_back(c); + } + m_constraints.swap(kept); + m_selection.clear(); + m_point_sel.clear(); + // v1: placed quotes reference entity indices that have shifted; drop them rather + // than risk a dangling reference (the driving constraints survive, reindexed). + m_dimensions.clear(); + m_dim_has0 = false; + m_pending_dim = -1; + if (on_selection_changed) on_selection_changed(0); +} + +bool DesignSketchTool::selection_valid() const +{ + for (int i : m_selection) + if (i < 0 || i >= int(m_entities.size())) return false; + return true; +} + +DesignSketchTool::DimType DesignSketchTool::dimension_kind() const +{ + if (!selection_valid()) return DimType::None; + if (m_selection.size() == 1) { + switch (m_entities[m_selection[0]].type) { + case SketchEntity::Type::Line: return DimType::Length; + case SketchEntity::Type::Circle: return DimType::Diameter; + case SketchEntity::Type::Arc: return DimType::Radius; + default: return DimType::None; + } + } + if (m_selection.size() == 2) { + const SketchEntity& a = m_entities[m_selection[0]]; + const SketchEntity& b = m_entities[m_selection[1]]; + const bool aLine = (a.type == SketchEntity::Type::Line); + const bool bLine = (b.type == SketchEntity::Type::Line); + Vec2d tmp(0, 0); + if (aLine && bLine) return DimType::Angle; + // one line + one point-like (point / circle-centre / arc-centre) + if (aLine && entity_ref_point(b, tmp)) return DimType::DistanceToLine; + if (bLine && entity_ref_point(a, tmp)) return DimType::DistanceToLine; + // two point-likes -> centre/point distance (0 = coincident/concentric) + if (entity_ref_point(a, tmp) && entity_ref_point(b, tmp)) return DimType::Distance; + } + return DimType::None; +} + +double DesignSketchTool::dimension_current() const +{ + switch (dimension_kind()) { + case DimType::Length: { const auto& e = m_entities[m_selection[0]]; return (e.p1 - e.p0).norm(); } + case DimType::Diameter: return 2.0 * m_entities[m_selection[0]].radius; + case DimType::Radius: return m_entities[m_selection[0]].radius; + case DimType::Angle: { + const auto& a = m_entities[m_selection[0]]; + const auto& b = m_entities[m_selection[1]]; + const Vec2d da = a.p1 - a.p0, db = b.p1 - b.p0; + const double na = da.norm(), nb = db.norm(); + if (na < 1e-9 || nb < 1e-9) return 0.0; + const double c = std::max(-1.0, std::min(1.0, da.dot(db) / (na * nb))); + return std::acos(c) * 180.0 / M_PI; + } + case DimType::Distance: { + Vec2d ra(0, 0), rb(0, 0); + entity_ref_point(m_entities[m_selection[0]], ra); + entity_ref_point(m_entities[m_selection[1]], rb); + return (rb - ra).norm(); + } + case DimType::DistanceToLine: { + const SketchEntity& a = m_entities[m_selection[0]]; + const SketchEntity& b = m_entities[m_selection[1]]; + const bool aLine = (a.type == SketchEntity::Type::Line); + const SketchEntity& L = aLine ? a : b; + const SketchEntity& P = aLine ? b : a; + Vec2d rp(0, 0); entity_ref_point(P, rp); + Vec2d dir = L.p1 - L.p0; + const double n = dir.norm(); + if (n < 1e-9) return 0.0; + const Vec2d nrm(-dir.y() / n, dir.x() / n); // unit normal to the line + return std::abs((rp - L.p0).dot(nrm)); + } + default: return 0.0; + } +} + +void DesignSketchTool::apply_angle_between(int ia, int ib, double deg) +{ + SketchEntity& A = m_entities[ia]; + SketchEntity& B = m_entities[ib]; + const Vec2d aE[2] = { A.p0, A.p1 }; + const Vec2d bE[2] = { B.p0, B.p1 }; + int si = -1, sj = -1; + double best = 1e-6; + for (int i = 0; i < 2; ++i) + for (int j = 0; j < 2; ++j) { + const double d = (aE[i] - bE[j]).squaredNorm(); + if (d < best) { best = d; si = i; sj = j; } + } + Vec2d pivot, refDir, bMoving; + bool moveP1; + if (si >= 0) { // shared vertex: pivot there, A's arm is the reference + pivot = aE[si]; + refDir = aE[1 - si] - pivot; + moveP1 = (sj == 0); // move the B end that is NOT at the pivot + bMoving = moveP1 ? B.p1 : B.p0; + } else { // no shared vertex: pivot B.p0, A's direction is reference + pivot = B.p0; + refDir = A.p1 - A.p0; + moveP1 = true; + bMoving = B.p1; + } + double nr = refDir.norm(); + if (nr < 1e-9) return; + refDir /= nr; + const Vec2d db = bMoving - pivot; + const double Lb = db.norm(); + if (Lb < 1e-9) return; + const double cross = refDir.x() * db.y() - refDir.y() * db.x(); + const double sign = (cross >= 0.0) ? 1.0 : -1.0; // keep B on its current side + const double rad = sign * deg * M_PI / 180.0; + const Vec2d ndir(refDir.x() * std::cos(rad) - refDir.y() * std::sin(rad), + refDir.x() * std::sin(rad) + refDir.y() * std::cos(rad)); + const Vec2d nb = pivot + Lb * ndir; + if (moveP1) B.p1 = nb; else B.p0 = nb; +} + +void DesignSketchTool::apply_dimension(double v) +{ + switch (dimension_kind()) { + case DimType::Length: { + SketchEntity& e = m_entities[m_selection[0]]; + const Vec2d d = e.p1 - e.p0; + const double r = d.norm(); + if (r > 1e-9 && v > 1e-9) e.p1 = e.p0 + (v / r) * d; + break; + } + case DimType::Diameter: if (v > 1e-9) m_entities[m_selection[0]].radius = 0.5 * v; break; + case DimType::Radius: if (v > 1e-9) m_entities[m_selection[0]].radius = v; break; + case DimType::Angle: apply_angle_between(m_selection[0], m_selection[1], v); break; + case DimType::Distance: { + // Move the 2nd selection so its reference point sits at distance v from the + // 1st (v == 0 -> coincident / concentric). Translate the whole entity. + if (v < 0.0) break; + Vec2d ra(0, 0), rb(0, 0); + entity_ref_point(m_entities[m_selection[0]], ra); + SketchEntity& b = m_entities[m_selection[1]]; + entity_ref_point(b, rb); + const Vec2d d = rb - ra; + const double r = d.norm(); + Vec2d target = ra; + if (r > 1e-9) target = ra + (v / r) * d; + else target = ra + Vec2d(v, 0.0); + translate_entity(b, target - rb); + break; + } + case DimType::DistanceToLine: { + // Move the point-like selection perpendicular to the line so its reference + // point is at distance v (v == 0 -> on the line / on the axis). + if (v < 0.0) break; + const bool aLine = (m_entities[m_selection[0]].type == SketchEntity::Type::Line); + const SketchEntity& L = m_entities[m_selection[aLine ? 0 : 1]]; + SketchEntity& P = m_entities[m_selection[aLine ? 1 : 0]]; + Vec2d rp(0, 0); entity_ref_point(P, rp); + Vec2d dir = L.p1 - L.p0; + const double n = dir.norm(); + if (n < 1e-9) break; + const Vec2d nrm(-dir.y() / n, dir.x() / n); + const double d0 = (rp - L.p0).dot(nrm); // current signed distance + const double sign = (d0 >= 0.0) ? 1.0 : -1.0; // keep the point on its side + translate_entity(P, (sign * v - d0) * nrm); + break; + } + default: break; + } + record_dimension_constraint(v); // store a driving constraint for this dimension + resolve_live(); // live-solve so the viewport shows the solved sketch + m_selection.clear(); + if (on_selection_changed) on_selection_changed(0); +} + +// Append the SketchEntityConstraintDef that makes the just-applied dimension a +// driving constraint (enforced by the kernel live and at commit). DistanceToLine +// records a PointOnLine constraint so "centre onto axis" persists through re-solve. +void DesignSketchTool::record_dimension_constraint(double v) +{ + const DimType k = dimension_kind(); + auto role = [&](int i) { + return (m_entities[i].type == SketchEntity::Type::Point) ? SketchPointRole::P0 + : SketchPointRole::Center; + }; + SketchEntityConstraintDef c; + switch (k) { + case DimType::Length: + c.type = SketchConstraintType::Distance; + c.ea = m_selection[0]; c.ra = SketchPointRole::P0; + c.eb = m_selection[0]; c.rb = SketchPointRole::P1; + c.value = v; m_constraints.push_back(c); break; + case DimType::Diameter: + c.type = SketchConstraintType::Diameter; c.ea = m_selection[0]; c.value = v; + m_constraints.push_back(c); break; + case DimType::Radius: + c.type = SketchConstraintType::Radius; c.ea = m_selection[0]; c.value = v; + m_constraints.push_back(c); break; + case DimType::Angle: + c.type = SketchConstraintType::Angle; + c.ea = m_selection[0]; c.eb = m_selection[1]; c.value = v; + m_constraints.push_back(c); break; + case DimType::Distance: { + const int ia = m_selection[0], ib = m_selection[1]; + if (v < 1e-9) c.type = SketchConstraintType::Coincident; + else { c.type = SketchConstraintType::Distance; c.value = v; } + c.ea = ia; c.ra = role(ia); c.eb = ib; c.rb = role(ib); + m_constraints.push_back(c); break; + } + case DimType::DistanceToLine: { + // Point-on-line driving constraint: hold the point-like entity at unsigned + // perpendicular distance v from the line (v == 0 -> on the axis). + const bool aLine = (m_entities[m_selection[0]].type == SketchEntity::Type::Line); + const int ip = m_selection[aLine ? 1 : 0]; // point-like (Point/Circle/Arc) + const int il = m_selection[aLine ? 0 : 1]; // line + c.type = SketchConstraintType::PointOnLine; + c.ea = ip; c.ra = role(ip); c.eb = il; c.value = v; + m_constraints.push_back(c); break; + } + default: break; // None: no driving constraint recorded + } +} + +// Onshape-style live solve: enforce all accumulated driving constraints on the +// in-session entities immediately, so the viewport reflects the solved sketch as +// each dimension/constraint is added (not only at commit). The pre-edit geometry +// is the solver's initial guess, keeping convergence local and side-preserving. +void DesignSketchTool::resolve_live() +{ + resolve_live_drag(-1, SketchPointRole::P0); +} + +void DesignSketchTool::resolve_live_drag(int dragged_ei, SketchPointRole dragged_role) +{ + const bool has = !m_constraints.empty(); + m_entity_conflict.assign(m_entities.size(), 0); + if (has) { + // Dragging a line endpoint must move ONLY that endpoint (changing the line's angle + + // length), anchoring the other end — Onshape behaviour. Without this, a constraint on + // the line (e.g. a length dimension or an inferred H/V) lets the solver relocate the + // un-dragged endpoint too, so the whole line appears to shift. Temporarily Fix the + // opposite endpoint for this drag-solve only (set_point already moved just the grabbed + // point, so the other end's current coord is its anchor). + std::vector cons = m_constraints; + const bool line_end = dragged_ei >= 0 && dragged_ei < int(m_entities.size()) && + m_entities[dragged_ei].type == SketchEntity::Type::Line && + (dragged_role == SketchPointRole::P0 || dragged_role == SketchPointRole::P1); + if (line_end) { + SketchEntityConstraintDef fix; + fix.type = SketchConstraintType::Fix; + fix.ea = dragged_ei; + fix.ra = (dragged_role == SketchPointRole::P0) ? SketchPointRole::P1 + : SketchPointRole::P0; + cons.push_back(fix); + } + const SketchSolveResult r = (dragged_ei >= 0) + ? sketch_solve_drag(m_entities, cons, dragged_ei, dragged_role) + : sketch_solve(m_entities, cons); + m_dof = r.dof; + m_solve_ok = r.ok; + // Flag every entity referenced by a conflicting constraint so render() can + // tint it red (Onshape/SolveSpace over-constrained feedback). + for (int bi : r.bad) { + if (bi < 0 || bi >= int(m_constraints.size())) continue; + const SketchEntityConstraintDef& c = m_constraints[bi]; + for (int e : {c.ea, c.eb, c.ec}) + if (e >= 0 && e < int(m_entity_conflict.size())) m_entity_conflict[e] = 1; + } + } else { + m_dof = -1; m_solve_ok = true; + } + if (on_solve_state) on_solve_state(m_dof, m_solve_ok, has); +} + +// ---- Onshape-style visual editing: feature grouping + handles ----------------- + +// Index of the Feature whose entity span contains ei, or -1 (last match wins so a +// later, tighter gesture shadows an earlier one if they ever overlap). +int DesignSketchTool::feature_of(int ei) const +{ + for (int i = int(m_features.size()) - 1; i >= 0; --i) + if (ei >= m_features[i].begin && ei < m_features[i].end) return i; + return -1; +} + +// Open a Feature spanning the entities a single gesture is about to append. The +// [begin,end) range is closed in end_feature() once the gesture's entities are in. +void DesignSketchTool::begin_feature(FeatureKind kind) +{ + Feature f; + f.kind = kind; + f.begin = int(m_entities.size()); + f.end = f.begin; + m_features.push_back(f); + m_open_feature = int(m_features.size()) - 1; +} + +// Close the open Feature: record its entity span end + the gesture's parametric +// anchors (centres / corners / half-width / sides) for later handle + dim rebuild. +void DesignSketchTool::end_feature(const Vec2d& c0, const Vec2d& c1, double param, int sides) +{ + if (m_open_feature < 0 || m_open_feature >= int(m_features.size())) return; + Feature& f = m_features[m_open_feature]; + f.end = int(m_entities.size()); + f.c0 = c0; + f.c1 = c1; + f.param = param; + f.sides = sides; + // Drop a degenerate feature (gesture appended nothing). + if (f.end <= f.begin) m_features.pop_back(); + m_open_feature = -1; +} + +// Forward decls: these ellipse helpers are defined further down but used by the +// handle/drag code above their definition. +static Vec2d ellipse_point(const Vec2d& c, double a, double b, double phi, double t); +static double ellipse_param_of(const Vec2d& center, double a, double b, double phi, const Vec2d& q); + +// Live handle set (A3: Line + Circle). Recomputed from solved geometry every frame, +// never persisted — so handles always track the current solve. Derived roles (here +// the circle RadiusHandle, which is NOT a serialized SketchPointRole) are what let a +// tool expose a parametric control its raw entity points don't carry. A4 routes a +// Select-mode drag through hit_test_handle + set_handle; later phases add the +// slot/rect/polygon/ellipse roles off the Feature groups. +std::vector DesignSketchTool::build_handles() const +{ + std::vector hs; + hs.reserve(m_entities.size() * 2); + for (size_t i = 0; i < m_entities.size(); ++i) { + const SketchEntity& e = m_entities[i]; + switch (e.type) { + case SketchEntity::Type::Line: { + Handle a; a.role = HandleRole::P0; a.ei = int(i); a.pos = e.p0; hs.push_back(a); + Handle b; b.role = HandleRole::P1; b.ei = int(i); b.pos = e.p1; hs.push_back(b); + break; + } + case SketchEntity::Type::Circle: { + Handle c; c.role = HandleRole::Center; c.ei = int(i); c.pos = e.center; hs.push_back(c); + // RadiusHandle sits on the circle to the +x side of its centre; dragging it + // (A4) edits the radius. Pure geometry, no constraint of its own. + Handle r; r.role = HandleRole::RadiusHandle; r.ei = int(i); + r.pos = e.center + Vec2d(e.radius, 0.0); hs.push_back(r); + break; + } + case SketchEntity::Type::Ellipse: + case SketchEntity::Type::EllipseArc: { + // 3 grips: centre (translate), major-axis end (semi-major a + orientation phi), + // minor-axis end (semi-minor b). a=e.radius, b=e.rminor, phi=e.rotation. + // (EllipseArc also exposes its two endpoints via hit_test_point for sweep.) + const Vec2d um(std::cos(e.rotation), std::sin(e.rotation)); // major dir + const Vec2d un(-um.y(), um.x()); // minor dir + Handle c; c.role = HandleRole::Center; c.ei = int(i); c.pos = e.center; hs.push_back(c); + Handle ma; ma.role = HandleRole::MajorAxis; ma.ei = int(i); + ma.pos = e.center + um * e.radius; hs.push_back(ma); + Handle mi; mi.role = HandleRole::MinorAxis; mi.ei = int(i); + mi.pos = e.center + un * e.rminor; hs.push_back(mi); + break; + } + case SketchEntity::Type::BSpline: { + // One draggable grip per control pole; dragging a pole reshapes the curve. + for (size_t k = 0; k < e.ctrl.size(); ++k) { + Handle h; h.role = HandleRole::BSplineCtrl; h.ei = int(i); + h.ctrl_index = int(k); h.pos = e.ctrl[k]; hs.push_back(h); + } + break; + } + default: break; // Arc derived handles land in later chunks + } + } + return hs; +} + +// Nearest handle to plane-point p within tol. Ties broken by smallest distance. +bool DesignSketchTool::hit_test_handle(const Vec2d& p, double tol, Handle& out) const +{ + bool found = false; + double best = tol; + for (const Handle& h : build_handles()) { + const double d = (h.pos - p).norm(); + if (d <= best) { best = d; out = h; out.hovered = true; found = true; } + } + return found; +} + +// Recompute the hovered handle on a plain (no-button) move. Returns true ONLY when the +// hovered handle changes, so on_mouse forces a single repaint per transition rather than +// re-rendering on every motion event. A no-op (false) for non-Moving events. +bool DesignSketchTool::update_hover(GLCanvas3D& canvas, wxMouseEvent& evt) +{ + if (!evt.Moving()) return false; + Vec2d p; + screen_to_plane(canvas, evt, p); + // Zoom-aware pick tolerance: project a point a few px away and measure in plane units. + const Linef3 r2 = canvas.mouse_ray(Point(evt.GetX() + 6, evt.GetY())); + const double tol = std::max(1e-3, (m_plane.project(r2.a, r2.vector()) - p).norm()); + const bool had = m_has_hover_handle; + const Handle prev = m_hover_handle; + Handle h; + m_has_hover_handle = hit_test_handle(p, tol, h); + if (m_has_hover_handle) m_hover_handle = h; + return (had != m_has_hover_handle) || + (m_has_hover_handle && (prev.ei != h.ei || prev.role != h.role)); +} + +// Apply a handle drag (A4). Point handles (P0/P1/Center) move the entity point and +// pin it in the drag-solve so constraints settle around the cursor. The derived +// RadiusHandle isn't a solver point — it edits the circle radius directly, then a +// full re-solve lets any driving Radius/Diameter constraint reassert (an unconstrained +// radius is a free DoF, so the solver keeps the new value). Slot/rect/polygon/ellipse +// roles land in later phases (they rebuild their Feature group). +void DesignSketchTool::set_handle(const Handle& h, const Vec2d& target) +{ + if (h.ei < 0 || h.ei >= int(m_entities.size())) return; + SketchEntity& e = m_entities[h.ei]; + switch (h.role) { + case HandleRole::P0: + set_point(h.ei, SketchPointRole::P0, target); resolve_live_drag(h.ei, SketchPointRole::P0); break; + case HandleRole::P1: + set_point(h.ei, SketchPointRole::P1, target); resolve_live_drag(h.ei, SketchPointRole::P1); break; + case HandleRole::Center: + set_point(h.ei, SketchPointRole::Center, target); resolve_live_drag(h.ei, SketchPointRole::Center); break; + case HandleRole::RadiusHandle: { + const double r = (target - e.center).norm(); + if (r > 1e-6) e.radius = r; + resolve_live(); + break; + } + case HandleRole::MajorAxis: { + // The major grip defines the major-axis vector: sets semi-major a + orientation phi. + const Vec2d d = target - e.center; + const double a = d.norm(); + if (a > 1e-6) { + e.rotation = std::atan2(d.y(), d.x()); + e.radius = std::max(a, e.rminor); // keep OCCT invariant a >= b + } + if (e.type == SketchEntity::Type::EllipseArc) { // endpoints ride the reshaped frame + e.p0 = ellipse_point(e.center, e.radius, e.rminor, e.rotation, e.start_angle); + e.p1 = ellipse_point(e.center, e.radius, e.rminor, e.rotation, e.end_angle); + } + resolve_live(); + break; + } + case HandleRole::MinorAxis: { + // The minor grip sets semi-minor b = perpendicular distance to the major axis. + const Vec2d um(std::cos(e.rotation), std::sin(e.rotation)); + const Vec2d un(-um.y(), um.x()); + const double b = std::abs((target - e.center).dot(un)); + if (b > 1e-6) e.rminor = std::min(b, e.radius); + if (e.type == SketchEntity::Type::EllipseArc) { + e.p0 = ellipse_point(e.center, e.radius, e.rminor, e.rotation, e.start_angle); + e.p1 = ellipse_point(e.center, e.radius, e.rminor, e.rotation, e.end_angle); + } + resolve_live(); + break; + } + case HandleRole::BSplineCtrl: { + // Move one control pole; the end poles mirror p0/p1 (kept in sync for picking). + const int k = h.ctrl_index; + if (k >= 0 && k < int(e.ctrl.size())) { + e.ctrl[k] = target; + if (k == 0) e.p0 = target; + if (k == int(e.ctrl.size()) - 1) e.p1 = target; + } + resolve_live(); + break; + } + default: break; + } +} + +// ---- Dimension tool (Mode::Dimension): click-to-place driving quotes ---------- + +bool DesignSketchTool::point_at(int ei, SketchPointRole role, Vec2d& out) const +{ + if (ei < 0 || ei >= int(m_entities.size())) return false; + const SketchEntity& e = m_entities[ei]; + switch (role) { + case SketchPointRole::P0: out = e.p0; return true; + case SketchPointRole::P1: out = e.p1; return true; + case SketchPointRole::Center: out = e.center; return true; + } + return false; +} + +// Move an entity point to v. Lines/Points set the coordinate directly; a circle/ +// arc centre translates the whole entity (arc endpoints ride along). Dragging an +// arc's endpoint is intentionally a no-op (it would redefine radius + angles). +void DesignSketchTool::set_point(int ei, SketchPointRole role, const Vec2d& v) +{ + if (ei < 0 || ei >= int(m_entities.size())) return; + SketchEntity& e = m_entities[ei]; + switch (e.type) { + case SketchEntity::Type::Line: + if (role == SketchPointRole::P0) e.p0 = v; + else if (role == SketchPointRole::P1) e.p1 = v; + break; + case SketchEntity::Type::Point: + e.p0 = v; + break; + case SketchEntity::Type::Circle: + if (role == SketchPointRole::Center) e.center = v; + break; + case SketchEntity::Type::Arc: + case SketchEntity::Type::EllipseArc: + if (role == SketchPointRole::Center) { + const Vec2d d = v - e.center; // rigid translate, keep radius/angles + e.center = v; e.p0 += d; e.p1 += d; + } + break; + case SketchEntity::Type::Ellipse: + if (role == SketchPointRole::Center) { e.center = v; e.p0 = v; } + break; + case SketchEntity::Type::BSpline: + // P0/P1 drag the end poles; Center rigidly translates the whole curve. + if (role == SketchPointRole::P0 && !e.ctrl.empty()) { e.ctrl.front() = v; e.p0 = v; } + else if (role == SketchPointRole::P1 && !e.ctrl.empty()) { e.ctrl.back() = v; e.p1 = v; } + else if (role == SketchPointRole::Center) { + const Vec2d d = v - (e.ctrl.empty() ? e.p0 : e.ctrl.front()); + for (auto& cp : e.ctrl) cp += d; + e.p0 += d; e.p1 += d; + } + break; + } +} + +// Nearest entity *point* (endpoint / centre) within tol, with its role. +bool DesignSketchTool::hit_test_point(const Vec2d& p, double tol, int& ei, SketchPointRole& role) const +{ + double best = tol; + bool found = false; + auto consider = [&](int i, SketchPointRole r, const Vec2d& q) { + const double d = (q - p).norm(); + if (d < best) { best = d; ei = i; role = r; found = true; } + }; + for (size_t i = 0; i < m_entities.size(); ++i) { + const SketchEntity& e = m_entities[i]; + switch (e.type) { + case SketchEntity::Type::Line: + consider(int(i), SketchPointRole::P0, e.p0); + consider(int(i), SketchPointRole::P1, e.p1); + break; + case SketchEntity::Type::Arc: + case SketchEntity::Type::EllipseArc: + consider(int(i), SketchPointRole::P0, e.p0); + consider(int(i), SketchPointRole::P1, e.p1); + consider(int(i), SketchPointRole::Center, e.center); + break; + case SketchEntity::Type::Circle: + case SketchEntity::Type::Ellipse: + consider(int(i), SketchPointRole::Center, e.center); + break; + case SketchEntity::Type::BSpline: + consider(int(i), SketchPointRole::P0, e.p0); + consider(int(i), SketchPointRole::P1, e.p1); + break; + case SketchEntity::Type::Point: + consider(int(i), SketchPointRole::P0, e.p0); + break; + } + } + return found; +} + +double DesignSketchTool::measure_dim(const DimAnnot& a) const +{ + Vec2d pa, pb; + switch (a.kind) { + case DimType::Length: + return (a.ea >= 0 && a.ea < int(m_entities.size())) + ? (m_entities[a.ea].p1 - m_entities[a.ea].p0).norm() : 0.0; + case DimType::Diameter: + return (a.ea >= 0 && a.ea < int(m_entities.size())) ? 2.0 * m_entities[a.ea].radius : 0.0; + case DimType::Radius: + return (a.ea >= 0 && a.ea < int(m_entities.size())) ? m_entities[a.ea].radius : 0.0; + case DimType::Angle: { + // Single line: angle to the +X axis, normalised to [0,360). (Line-to-line angle + // dimensions use the legacy dialog path.) + if (a.ea < 0 || a.ea >= int(m_entities.size())) return 0.0; + const SketchEntity& e = m_entities[a.ea]; + if (e.type != SketchEntity::Type::Line) return 0.0; + const Vec2d d = e.p1 - e.p0; + if (d.squaredNorm() < 1e-18) return 0.0; + double deg = std::atan2(d.y(), d.x()) * 180.0 / M_PI; + if (deg < 0.0) deg += 360.0; + return deg; + } + case DimType::Distance: + return (point_at(a.ea, a.ra, pa) && point_at(a.eb, a.rb, pb)) ? (pb - pa).norm() : 0.0; + case DimType::DistanceToLine: { + if (!point_at(a.ea, a.ra, pa) || a.eb < 0 || a.eb >= int(m_entities.size())) return 0.0; + const SketchEntity& L = m_entities[a.eb]; + const Vec2d d = L.p1 - L.p0; + const double n = d.norm(); + if (n < 1e-9) return 0.0; + const Vec2d nrm(-d.y() / n, d.x() / n); + return std::abs((pa - L.p0).dot(nrm)); + } + default: return 0.0; + } +} + +SketchEntityConstraintDef DesignSketchTool::constraint_for(const DimAnnot& a) const +{ + SketchEntityConstraintDef c; + switch (a.kind) { + case DimType::Length: + c.type = SketchConstraintType::Distance; + c.ea = a.ea; c.ra = SketchPointRole::P0; + c.eb = a.ea; c.rb = SketchPointRole::P1; c.value = a.value; + break; + case DimType::Diameter: c.type = SketchConstraintType::Diameter; c.ea = a.ea; c.value = a.value; break; + case DimType::Radius: c.type = SketchConstraintType::Radius; c.ea = a.ea; c.value = a.value; break; + case DimType::Distance: + if (a.value < 1e-9) c.type = SketchConstraintType::Coincident; + else { c.type = SketchConstraintType::Distance; c.value = a.value; } + c.ea = a.ea; c.ra = a.ra; c.eb = a.eb; c.rb = a.rb; + break; + case DimType::DistanceToLine: + c.type = SketchConstraintType::PointOnLine; + c.ea = a.ea; c.ra = a.ra; c.eb = a.eb; c.value = a.value; + break; + default: break; + } + return c; +} + +// Measure the just-picked dimension, append its driving constraint, live-solve, and +// fire the value-card callback so the user can override the value. +int DesignSketchTool::place_dimension(DimAnnot a) +{ + a.value = measure_dim(a); + a.con = int(m_constraints.size()); + m_constraints.push_back(constraint_for(a)); + m_dimensions.push_back(a); + resolve_live(); + open_value_editor(int(m_dimensions.size()) - 1); + return m_pending_dim; +} + +// Nearest placed-dimension label within tol (uses the centre cached by render). +int DesignSketchTool::hit_test_dimension(const Vec2d& p, double tol) const +{ + double best = tol; + int bi = -1; + for (size_t i = 0; i < m_dimensions.size(); ++i) { + const double d = (m_dimensions[i].label_pos - p).norm(); + if (d < best) { best = d; bi = int(i); } + } + return bi; +} + +// Reopen the value editor on an existing dimension (click/double-click its label). +void DesignSketchTool::edit_dimension(int di) +{ + if (di < 0 || di >= int(m_dimensions.size())) return; + open_value_editor(di); +} + +// Representative plane anchor for a dimension's value editor: the cached label centre +// once render has computed it, otherwise a geometric midpoint (length/distance) or the +// entity centre (radius/diameter). +Vec2d DesignSketchTool::dim_anchor(const DimAnnot& a) const +{ + if (a.label_pos.squaredNorm() > 1e-12) return a.label_pos; + Vec2d pa, pb; + if ((a.kind == DimType::Radius || a.kind == DimType::Diameter) && + point_at(a.ea, SketchPointRole::Center, pa)) + return pa; + const bool ga = point_at(a.ea, a.ra, pa); + const bool gb = point_at(a.eb, a.rb, pb); + if (ga && gb) return 0.5 * (pa + pb); + if (ga) return pa; + if (gb) return pb; + return Vec2d(0, 0); +} + +// Open the in-canvas value editor on dimension `di`. Projects the dimension's anchor +// to screen pixels and hands the host (DesignCanvas) a commit/cancel pair that drive +// the value through the existing set/cancel_dimension_value path. Falls back to the +// modal pick-complete callback when no inline-edit host is wired. +void DesignSketchTool::open_value_editor(int di) +{ + if (di < 0 || di >= int(m_dimensions.size())) return; + m_pending_dim = di; + if (!on_inline_edit) { + if (on_dimension_pick_complete) on_dimension_pick_complete(m_dimensions[di].value); + return; + } + const DimAnnot& a = m_dimensions[di]; + // Anchor the field OVER the dimension (project its label/anchor to the viewport), same as + // the draw-then-edit tools and Constrain mode; fall back to the click point if it projects + // off-screen. + wxPoint px(m_last_mouse_x, m_last_mouse_y); + const Camera& cam = wxGetApp().plater()->get_camera(); + const wxPoint lp = world_to_screen_px(cam, m_plane.to_world(dim_anchor(a))); + const std::array& vp = cam.get_viewport(); + if (lp.x >= vp[0] && lp.y >= vp[1] && lp.x <= vp[0] + vp[2] && lp.y <= vp[1] + vp[3]) + px = lp; + on_inline_edit(px, a.value, + [this](double v) { set_dimension_value(v); }, + [this]() { cancel_dimension_value(); }); +} + +// In-canvas editor for a line's angle-to-horizontal. Unlike length/radius, a single +// line's angle has no libslvs constraint here (SLVS_C_ANGLE is line-to-line), so the +// commit rotates the segment GEOMETRICALLY about P0 to the typed degrees, then re-solves +// — the angle is a free DoF, so the solver keeps the new orientation (mirrors the radius +// handle). Length-constrained lines keep their length. +void DesignSketchTool::open_angle_editor(int ei) +{ + if (ei < 0 || ei >= int(m_entities.size())) return; + if (m_entities[ei].type != SketchEntity::Type::Line) return; + if (!on_inline_edit) return; + DimAnnot a; a.kind = DimType::Angle; a.ea = ei; + const wxPoint px(m_last_mouse_x, m_last_mouse_y); + on_inline_edit(px, measure_dim(a), + [this, ei](double deg) { set_line_angle(ei, deg); }, + []() {}); +} + +void DesignSketchTool::set_line_angle(int ei, double deg) +{ + if (ei < 0 || ei >= int(m_entities.size())) return; + SketchEntity& e = m_entities[ei]; + if (e.type != SketchEntity::Type::Line) return; + const double L = (e.p1 - e.p0).norm(); + if (L < 1e-9) return; + drop_orientation_constraints(ei, ei + 1); // a typed angle overrides an inferred H/V + const double r = deg * M_PI / 180.0; + e.p1 = e.p0 + Vec2d(std::cos(r), std::sin(r)) * L; // rotate about P0, keep length + resolve_live(); +} + +// Open the queued scalar quote at m_autoedit_dim_idx. Commit appends the driving dimension +// AND advances to the next queued quote (deferred via CallAfter so the single SketchInlineEditor +// fully unwinds its Enter handler before being reopened). Cancel (Esc) aborts the whole chain — +// the shape is kept as drawn. This is what lets a rectangle edit Width THEN Height, a slot its +// centre-distance THEN width, etc., instead of only the first dimension. +void DesignSketchTool::open_next_autoedit_dim() +{ + if (!on_inline_edit || !m_active) { m_autoedit_dim_idx = -1; return; } + if (m_autoedit_dim_idx < 0 || m_autoedit_dim_idx >= int(m_autoedit_dims.size())) { + m_autoedit_dim_idx = -1; + return; + } + const AutoEditStep step = m_autoedit_dims[m_autoedit_dim_idx]; + // Anchor the field OVER this dimension's label (project its plane-coords centre to the + // viewport), not at the last cursor spot — otherwise each field pops up in an unrelated + // screen position. Fall back to the cursor if the label projects off-screen. + // Anchor the field OVER this dimension's label (project its plane-coords centre to the + // viewport). A label can project OFF-screen (near-degenerate perspective divide when the + // sketch plane is viewed at a grazing angle), in which case we fall back to the cursor — + // but staggered by step index, so a shape's successive fields (rrect W/H/R) don't all + // stack on the exact same pixel and hide each other. + const Camera& cam = wxGetApp().plater()->get_camera(); + const wxPoint lp = world_to_screen_px(cam, m_plane.to_world(step.label)); + const std::array& vp = cam.get_viewport(); + wxPoint px(m_last_mouse_x, m_last_mouse_y + m_autoedit_dim_idx * 34); + if (lp.x >= vp[0] && lp.y >= vp[1] && lp.x <= vp[0] + vp[2] && lp.y <= vp[1] + vp[3]) + px = lp; + on_inline_edit(px, step.value, + [this, step](double v) { // commit: apply this dimension, then next + if (step.apply) step.apply(v); + ++m_autoedit_dim_idx; + wxGetApp().CallAfter([this] { open_next_autoedit_dim(); }); + }, + [this]() { m_autoedit_dim_idx = -1; }); // cancel: keep as drawn, stop the chain +} + +// Polyline draw-then-edit: after each click places a chain vertex, refine THAT segment's +// Length then Angle through the same AutoEditStep queue. The polyline batch-creates its +// entities only when the chain ends, so here we edit the pending m_points vertex directly +// (geometric): Length rescales it along the segment, Angle rotates it about the previous +// vertex. The next click continues from the adjusted vertex. Same field/anchor/focus path as +// every other tool — Enter advances Length->Angle, Esc keeps the segment as clicked. +void DesignSketchTool::arm_polyline_segment_edit() +{ + const int k = int(m_points.size()) - 1; // index of the just-placed vertex + if (k < 1 || !on_inline_edit) return; + const Vec2d a = m_points[k - 1]; // segment anchor (previous vertex) + const Vec2d mid = 0.5 * (a + m_points[k]); // label anchor = segment midpoint + const Vec2d d = m_points[k] - a; + const double L = d.norm(); + if (L < 1e-9) return; + double deg = std::atan2(d.y(), d.x()) * 180.0 / M_PI; if (deg < 0.0) deg += 360.0; + + m_autoedit_dims.clear(); + m_autoedit_dims.push_back({ mid, L, [this, k, a](double len) { // Length + if (k < int(m_points.size())) { + Vec2d dd = m_points[k] - a; const double n = dd.norm(); + if (n > 1e-9 && len > 1e-9) m_points[k] = a + (len / n) * dd; + } + } }); + m_autoedit_dims.push_back({ mid, deg, [this, k, a](double dg) { // Angle + if (k < int(m_points.size())) { + const double len = (m_points[k] - a).norm(); + const double r = dg * M_PI / 180.0; + m_points[k] = a + len * Vec2d(std::cos(r), std::sin(r)); + } + } }); + m_autoedit_dim_idx = 0; + wxGetApp().CallAfter([this] { open_next_autoedit_dim(); }); +} + +// Draw-then-edit dispatcher: mirror the Select-mode quote-click logic, but target the +// freshly-drawn selection's PRIMARY value and use the tentative (clean-cancel) path for +// scalar quotes. Runs after render_live_quotes, so the live-quote state is populated. +void DesignSketchTool::open_primary_autoedit() +{ + if (!on_inline_edit || m_awaiting_length) return; // no host, or a field is already open + if (!m_active) return; // session ended before the deferred tick + + // Build ONE ordered list of edit steps covering EVERY characteristic dimension of the + // freshly-drawn shape — scalar quotes (constraint-based) AND geometric editors — so every + // 2D tool behaves like the rectangle: a linear sequence of value fields, each over its own + // label, Enter advances to the next, Esc keeps the shape as drawn. (Line keeps its own + // dedicated length field; Polyline/BSpline/Point have no two-click dimension set.) + m_autoedit_dims.clear(); + + // (1) Scalar quotes: rect Width+Height, slot Distance+Radius, circle/arc Radius, line + // Length. The lone Angle quote (only a single Line emits one) becomes a GEOMETRIC + // orientation step (set_line_angle, like the polygon angle) — so the Line tool gets + // Length THEN Angle, same as the rectangle gets W then H. + for (const DimAnnot& q : m_live_quotes) { + if (q.kind == DimType::Angle) { + const int ei = q.ea; + m_autoedit_dims.push_back({ q.label_pos, measure_dim(q), + [this, ei](double v) { set_line_angle(ei, v); }, { ei } }); + continue; + } + DimAnnot a = q; + a.value = measure_dim(a); + m_autoedit_dims.push_back({ a.label_pos, a.value, + [this, a](double v) mutable { + a.value = v; + a.con = int(m_constraints.size()); + m_constraints.push_back(constraint_for(a)); + m_dimensions.push_back(a); + resolve_live(); + }, { a.ea, a.eb } }); + } + + // (2) Geometric editors (mutate geometry directly, no constraint). Each reads the CURRENT + // feature/entity state inside apply(), so sequential edits compose correctly. + // Entities to highlight = the feature's whole [begin,end) span, so editing any of its + // characteristic dims lights up the shape it drives. + auto span = [this](int fi) { + std::vector v; + if (fi >= 0 && fi < int(m_features.size())) + for (int k = m_features[fi].begin; k < m_features[fi].end; ++k) v.push_back(k); + return v; + }; + if (m_live_poly_fi >= 0) { + const Feature& f = m_features[m_live_poly_fi]; + const int fi = m_live_poly_fi; + if (f.begin >= 0 && f.begin < int(m_entities.size())) { + const double side = (m_entities[f.begin].p1 - m_entities[f.begin].p0).norm(); + const Vec2d sp = m_entities[f.begin].p0 - f.c0; + double deg = std::atan2(sp.y(), sp.x()) * 180.0 / M_PI; if (deg < 0.0) deg += 360.0; + m_autoedit_dims.push_back({ m_live_poly_side_label, side, [this, fi](double v){ set_polygon_side(fi, v); }, span(fi) }); + m_autoedit_dims.push_back({ m_live_poly_angle_label, deg, [this, fi](double v){ set_polygon_angle(fi, v); }, span(fi) }); + } + } + if (m_live_rrect_fi >= 0) { + const Feature& f = m_features[m_live_rrect_fi]; + const int fi = m_live_rrect_fi; + const double w = std::abs(f.c1.x() - f.c0.x()), h = std::abs(f.c1.y() - f.c0.y()), r = f.param; + auto rr_w = [this, fi](double v){ const Feature& g = m_features[fi]; set_rounded_rect(fi, v, std::abs(g.c1.y()-g.c0.y()), g.param); }; + auto rr_h = [this, fi](double v){ const Feature& g = m_features[fi]; set_rounded_rect(fi, std::abs(g.c1.x()-g.c0.x()), v, g.param); }; + auto rr_r = [this, fi](double v){ const Feature& g = m_features[fi]; set_rounded_rect(fi, std::abs(g.c1.x()-g.c0.x()), std::abs(g.c1.y()-g.c0.y()), v); }; + m_autoedit_dims.push_back({ m_live_rrect_w_label, w, rr_w, span(fi) }); + m_autoedit_dims.push_back({ m_live_rrect_h_label, h, rr_h, span(fi) }); + m_autoedit_dims.push_back({ m_live_rrect_r_label, r, rr_r, span(fi) }); + } + if (m_live_aslot_fi >= 0) { + const Feature& f = m_features[m_live_aslot_fi]; + const int fi = m_live_aslot_fi; + const double Rc = (f.c1 - f.c0).norm(), fw = 2.0 * f.param; + m_autoedit_dims.push_back({ m_live_aslot_r_label, Rc, [this, fi](double v){ const Feature& g = m_features[fi]; set_arc_slot(fi, v, g.param); }, span(fi) }); + m_autoedit_dims.push_back({ m_live_aslot_w_label, fw, [this, fi](double v){ const Feature& g = m_features[fi]; set_arc_slot(fi, (g.c1-g.c0).norm(), std::max(1e-3, v*0.5)); }, span(fi) }); + } + if (m_live_arc_ei >= 0) { // arc Radius is already a scalar step above; add its sweep angle + const int ei = m_live_arc_ei; + const SketchEntity& e = m_entities[ei]; + const double swdeg = std::abs(e.end_angle - e.start_angle) * 180.0 / M_PI; + m_autoedit_dims.push_back({ m_live_arc_angle_label, swdeg, [this, ei](double v){ set_arc_sweep(ei, v); }, { ei } }); + } + if (m_live_ellipse_ei >= 0) { + const int ei = m_live_ellipse_ei; + const SketchEntity& e = m_entities[ei]; + m_autoedit_dims.push_back({ m_live_ellipse_major_label, e.radius, [this, ei](double v){ set_ellipse_axis(ei, true, v); }, { ei } }); + m_autoedit_dims.push_back({ m_live_ellipse_minor_label, e.rminor, [this, ei](double v){ set_ellipse_axis(ei, false, v); }, { ei } }); + if (e.type == SketchEntity::Type::EllipseArc) { // + included sweep + const double swdeg = std::abs(e.end_angle - e.start_angle) * 180.0 / M_PI; + m_autoedit_dims.push_back({ m_live_ellipsearc_sweep_label, swdeg, + [this, ei](double v){ set_ellipsearc_sweep(ei, v); }, { ei } }); + } + } + if (m_live_obrect_fi >= 0) { // oblique rect: W,H already added as scalars; + orientation + const int fi = m_live_obrect_fi; + const Feature& f = m_features[fi]; + const SketchEntity& e0 = m_entities[f.begin]; + double adeg = std::atan2(e0.p1.y() - e0.p0.y(), e0.p1.x() - e0.p0.x()) * 180.0 / M_PI; + if (adeg < 0.0) adeg += 360.0; + m_autoedit_dims.push_back({ m_live_obrect_angle_label, adeg, + [this, fi](double v){ set_rect_angle(fi, v); }, span(fi) }); + } + + if (!m_autoedit_dims.empty()) { + m_autoedit_dim_idx = 0; + open_next_autoedit_dim(); + } +} + +// A regular polygon is N raw lines with no centre entity, so (like the line angle) its +// side and orientation edits transform the whole loop GEOMETRICALLY about its centre. +void DesignSketchTool::open_polygon_side_editor(int fi) +{ + if (fi < 0 || fi >= int(m_features.size()) || !on_inline_edit) return; + const Feature& f = m_features[fi]; + if (f.begin < 0 || f.begin >= int(m_entities.size())) return; + const double side = (m_entities[f.begin].p1 - m_entities[f.begin].p0).norm(); + const wxPoint px(m_last_mouse_x, m_last_mouse_y); + on_inline_edit(px, side, + [this, fi](double v) { set_polygon_side(fi, v); }, + []() {}); +} + +void DesignSketchTool::open_polygon_angle_editor(int fi) +{ + if (fi < 0 || fi >= int(m_features.size()) || !on_inline_edit) return; + const Feature& f = m_features[fi]; + if (f.begin < 0 || f.begin >= int(m_entities.size())) return; + const Vec2d sp = m_entities[f.begin].p0 - f.c0; // centre -> vertex0 spoke + double deg = std::atan2(sp.y(), sp.x()) * 180.0 / M_PI; + if (deg < 0.0) deg += 360.0; + const wxPoint px(m_last_mouse_x, m_last_mouse_y); + on_inline_edit(px, deg, + [this, fi](double v) { set_polygon_angle(fi, v); }, + []() {}); +} + +// Scale the loop uniformly about its centre so an edge equals `side`. For a regular +// n-gon, circumradius R = side / (2 sin(pi/n)). +void DesignSketchTool::set_polygon_side(int fi, double side) +{ + if (fi < 0 || fi >= int(m_features.size()) || side < 1e-6) return; + const int n = std::max(3, m_features[fi].sides); + const double R = side / (2.0 * std::sin(M_PI / double(n))); + set_polygon_radius(fi, R); +} + +// Remove orientation constraints touching [begin,end). A pure rotation makes inferred +// per-edge Horizontal/Vertical (and Parallel/Perp/Angle/Lock) inconsistent, so leaving +// them in would make resolve_live collapse the shape to satisfy them. +void DesignSketchTool::drop_orientation_constraints(int begin, int end) +{ + using CT = SketchConstraintType; + auto orient = [](CT t) { + return t == CT::Horizontal || t == CT::Vertical || t == CT::Parallel || + t == CT::Perpendicular || t == CT::Angle || t == CT::LockX || t == CT::LockY; + }; + auto in = [&](int e) { return e >= begin && e < end; }; + std::vector remap(m_constraints.size(), -1); + std::vector kept; + kept.reserve(m_constraints.size()); + for (int i = 0; i < int(m_constraints.size()); ++i) { + const SketchEntityConstraintDef& c = m_constraints[i]; + if (orient(c.type) && (in(c.ea) || in(c.eb))) continue; // drop + remap[i] = int(kept.size()); + kept.push_back(c); + } + if (kept.size() == m_constraints.size()) return; // nothing dropped + m_constraints.swap(kept); + for (DimAnnot& a : m_dimensions) // fix cached con indices + if (a.con >= 0) a.con = (a.con < int(remap.size())) ? remap[a.con] : -1; +} + +void DesignSketchTool::drop_constraints_referencing(int ei) +{ + std::vector remap(m_constraints.size(), -1); + std::vector kept; + kept.reserve(m_constraints.size()); + for (int i = 0; i < int(m_constraints.size()); ++i) { + const SketchEntityConstraintDef& c = m_constraints[i]; + if (c.ea == ei || c.eb == ei || c.ec == ei) continue; // drop refs to the cut entity + remap[i] = int(kept.size()); + kept.push_back(c); + } + if (kept.size() == m_constraints.size()) return; + m_constraints.swap(kept); + for (DimAnnot& a : m_dimensions) + if (a.con >= 0) a.con = (a.con < int(remap.size())) ? remap[a.con] : -1; +} + +// Onshape scissors on the live sketch: cut the picked entity at its nearest intersection. +// trim_entity/extend_entity mutate the subject in place (slide one endpoint) given the other +// entities + the pick point — no entity is added/removed, so indices stay stable. +bool DesignSketchTool::apply_live_trim(const Vec2d& p, double tol, bool extend) +{ + double best = 1e30; int bi = -1; + for (size_t i = 0; i < m_entities.size(); ++i) { + const double d = entity_pick_dist(p, m_entities[i]); + if (d < best) { best = d; bi = int(i); } + } + if (bi < 0 || best > tol) return false; + using Ty = SketchEntity::Type; + const Ty st = m_entities[bi].type; + const bool subject_ok = extend ? (st == Ty::Line || st == Ty::Arc) + : (st == Ty::Line || st == Ty::Arc || st == Ty::Circle); + if (!subject_ok) return false; + std::vector others; + others.reserve(m_entities.size()); + for (size_t i = 0; i < m_entities.size(); ++i) + if (int(i) != bi) others.push_back(m_entities[i]); + const bool ok = extend ? SketchEngine::extend_entity(m_entities[bi], others, p) + : SketchEngine::trim_entity(m_entities[bi], others, p); + if (!ok) return false; + drop_constraints_referencing(bi); // the slid endpoint invalidates this entity's constraints + return true; +} + +// Hover preview for the Trim/Extend scissors: replay apply_live_trim's pick and the engine +// cut on a COPY of the subject, then diff the copy against the original to recover the exact +// sub-portion a click would remove (Trim) / add (Extend). Pure computation, mutates nothing. +bool DesignSketchTool::compute_trim_preview(const Vec2d& p, double tol, bool extend, + int& subject_ei, std::vector& removed_poly) const +{ + subject_ei = -1; + removed_poly.clear(); + + double best = 1e30; int bi = -1; + for (size_t i = 0; i < m_entities.size(); ++i) { + const double d = entity_pick_dist(p, m_entities[i]); + if (d < best) { best = d; bi = int(i); } + } + if (bi < 0 || best > tol) return false; + + using Ty = SketchEntity::Type; + const Ty st = m_entities[bi].type; + const bool subject_ok = extend ? (st == Ty::Line || st == Ty::Arc) + : (st == Ty::Line || st == Ty::Arc || st == Ty::Circle); + if (!subject_ok) return false; + + std::vector others; + others.reserve(m_entities.size()); + for (size_t i = 0; i < m_entities.size(); ++i) + if (int(i) != bi) others.push_back(m_entities[i]); + + const SketchEntity& orig = m_entities[bi]; + SketchEntity trimmed = orig; // cut on the copy, never the live entity + const bool ok = extend ? SketchEngine::extend_entity(trimmed, others, p) + : SketchEngine::trim_entity(trimmed, others, p); + if (!ok) return false; + + // The highlighted portion is where `trimmed` differs from `orig`: the dropped sub-segment + // (Trim) or the grown one (Extend). Rebuild it as a temp entity and sample its polyline. + const double EPS2 = 1e-14; // squared plane-unit endpoint tolerance + const double AEPS = 1e-7; // radian tolerance + bool closed = false; + + if (orig.type == Ty::Line) { + SketchEntity seg = orig; + if ((trimmed.p0 - orig.p0).squaredNorm() > EPS2) { + seg.p0 = orig.p0; seg.p1 = trimmed.p0; // start endpoint moved + } else if ((trimmed.p1 - orig.p1).squaredNorm() > EPS2) { + seg.p0 = trimmed.p1; seg.p1 = orig.p1; // end endpoint moved + } else { + return false; // nothing changed + } + removed_poly = entity_polyline(seg, closed); + } else if (orig.type == Ty::Arc) { + SketchEntity arc = orig; // same centre/radius + if (std::abs(trimmed.start_angle - orig.start_angle) > AEPS) { + arc.start_angle = orig.start_angle; arc.end_angle = trimmed.start_angle; + } else if (std::abs(trimmed.end_angle - orig.end_angle) > AEPS) { + arc.start_angle = trimmed.end_angle; arc.end_angle = orig.end_angle; + } else { + return false; + } + removed_poly = entity_polyline(arc, closed); + } else if (orig.type == Ty::Circle) { + // Trim opens the Circle into the kept Arc [start,end]; the removed gap is its + // complement, swept from the kept arc's end round to its start. + if (trimmed.type != Ty::Arc) return false; + SketchEntity gap = orig; + gap.type = Ty::Arc; + gap.start_angle = trimmed.end_angle; + gap.end_angle = trimmed.start_angle + 2.0 * M_PI; + removed_poly = entity_polyline(gap, closed); + } else { + return false; + } + + if (removed_poly.size() < 2) return false; + subject_ei = bi; + return true; +} + +// Rotate the whole loop about its centre so the centre->vertex0 spoke points at `deg` +// (degrees from +X). +void DesignSketchTool::set_polygon_angle(int fi, double deg) +{ + if (fi < 0 || fi >= int(m_features.size())) return; + Feature& f = m_features[fi]; + if (f.begin < 0 || f.begin >= int(m_entities.size())) return; + const Vec2d c = f.c0; + const Vec2d sp = m_entities[f.begin].p0 - c; // centre -> vertex0 + if (sp.squaredNorm() < 1e-12) return; + const double cur = std::atan2(sp.y(), sp.x()); + const double da = deg * M_PI / 180.0 - cur; + drop_orientation_constraints(f.begin, f.end); // rotation invalidates edge H/V + const double ca = std::cos(da), sa = std::sin(da); + // -> Vec2d is REQUIRED: an auto return deduces an Eigen expression template that holds + // a reference to the destroyed `c + Vec2d(...)` temporary (dangling -> garbage coords). + auto rot = [&](const Vec2d& pt) -> Vec2d { const Vec2d r = pt - c; + return c + Vec2d(r.x() * ca - r.y() * sa, r.x() * sa + r.y() * ca); }; + for (int i = f.begin; i < f.end && i < int(m_entities.size()); ++i) { + SketchEntity& e = m_entities[i]; + if (e.type != SketchEntity::Type::Line) continue; + e.p0 = rot(e.p0); e.p1 = rot(e.p1); + } + // Do NOT re-solve: the rotated geometry is already a correct regular polygon, and the + // inferred loop (redundant Coincident, now with no H/V anchor) collapses to a point + // under libslvs. We only drop the stale edge H/V (above) so a later commit-solve stays + // sane; the display renders the mutated entities directly. +} + +// Drag a polygon vertex keeping the loop regular: scale + rotate the whole polygon +// about its centroid so the grabbed vertex lands on `target`. This adjusts both the +// circumradius (|target-centroid|) and the orientation (its direction) at once. +void DesignSketchTool::drag_polygon_vertex(int fi, int ei, SketchPointRole role, const Vec2d& target) +{ + if (fi < 0 || fi >= int(m_features.size())) return; + Feature& f = m_features[fi]; + if (f.begin < 0 || f.end > int(m_entities.size())) return; + // Centroid of the loop = the regular polygon's centre (robust if it was moved). + Vec2d c(0, 0); int n = 0; + for (int i = f.begin; i < f.end; ++i) + if (m_entities[i].type == SketchEntity::Type::Line) { c += m_entities[i].p0; ++n; } + if (n == 0) return; + c /= double(n); + Vec2d vpos; + if (!point_at(ei, role, vpos)) return; + const Vec2d cur = vpos - c; // current grabbed-vertex spoke + const Vec2d tgt = target - c; // desired spoke + const double curR = cur.norm(), newR = tgt.norm(); + if (curR < 1e-9 || newR < 1e-6) return; + const double s = newR / curR; + const double da = std::atan2(tgt.y(), tgt.x()) - std::atan2(cur.y(), cur.x()); + const double ca = std::cos(da), sa = std::sin(da); + auto tf = [&](const Vec2d& p) -> Vec2d { const Vec2d r = (p - c) * s; // -> Vec2d: avoid + return c + Vec2d(r.x() * ca - r.y() * sa, r.x() * sa + r.y() * ca); }; // Eigen dangling + + for (int i = f.begin; i < f.end; ++i) { + SketchEntity& e = m_entities[i]; + if (e.type != SketchEntity::Type::Line) continue; + e.p0 = tf(e.p0); e.p1 = tf(e.p1); + } + f.c0 = c; f.param = newR; + drop_orientation_constraints(f.begin, f.end); // the drag rotates -> edge H/V invalid + // No re-solve (see set_polygon_angle): the transformed geometry is already a correct + // regular polygon; solving the anchorless redundant loop would collapse it. +} + +void DesignSketchTool::set_polygon_radius(int fi, double R) +{ + if (fi < 0 || fi >= int(m_features.size()) || R < 1e-6) return; + Feature& f = m_features[fi]; + if (f.begin < 0 || f.begin >= int(m_entities.size())) return; + const Vec2d c = f.c0; + const double curR = (m_entities[f.begin].p0 - c).norm(); + if (curR < 1e-9) return; + const double s = R / curR; // uniform scale about the centre + for (int i = f.begin; i < f.end && i < int(m_entities.size()); ++i) { + SketchEntity& e = m_entities[i]; + if (e.type != SketchEntity::Type::Line) continue; + e.p0 = c + (e.p0 - c) * s; + e.p1 = c + (e.p1 - c) * s; + } + f.param = R; + // Geometric only (no solve): consistent with the rotation edits, and avoids collapsing + // the loop if its H/V anchors were already dropped by a prior rotation. +} + +void DesignSketchTool::open_arc_angle_editor(int ei) +{ + if (ei < 0 || ei >= int(m_entities.size()) || !on_inline_edit) return; + const SketchEntity& e = m_entities[ei]; + if (e.type != SketchEntity::Type::Arc) return; + double swdeg = std::abs(e.end_angle - e.start_angle) * 180.0 / M_PI; + const wxPoint px(m_last_mouse_x, m_last_mouse_y); + on_inline_edit(px, swdeg, + [this, ei](double v) { set_arc_sweep(ei, v); }, + []() {}); +} + +// Set the arc's included (sweep) angle to `deg`, keeping the start point and radius fixed +// and rotating the end point about the centre. Geometric (SLVS angle is line-to-line), so +// no re-solve; the mutated entity renders directly. Direction (CCW/CW) of the original +// sweep is preserved. +void DesignSketchTool::set_arc_sweep(int ei, double deg) +{ + if (ei < 0 || ei >= int(m_entities.size())) return; + SketchEntity& e = m_entities[ei]; + if (e.type != SketchEntity::Type::Arc || e.radius < 1e-6) return; + double sweep = std::max(1e-3, std::min(deg, 359.999)) * M_PI / 180.0; + const double sign = (e.end_angle >= e.start_angle) ? 1.0 : -1.0; + e.end_angle = e.start_angle + sign * sweep; + e.p1 = e.center + e.radius * Vec2d(std::cos(e.end_angle), std::sin(e.end_angle)); + resolve_live(); +} + +// Drag one of the arc's three handles. Roles are split so each grip changes ONE property +// (Onshape-like): Center -> translate; START point (P0) -> radius only; END point (P1) -> +// sweep angle only. Geometric (mutates the entity directly), then re-solve for any +// coincident constraints on the arc endpoints. +void DesignSketchTool::drag_arc_handle(int ei, SketchPointRole role, const Vec2d& target) +{ + if (ei < 0 || ei >= int(m_entities.size())) return; + SketchEntity& e = m_entities[ei]; + if (e.type != SketchEntity::Type::Arc) return; + if (role == SketchPointRole::Center) { + const Vec2d d = target - e.center; // rigid translate, keep R + angles + e.center = target; e.p0 += d; e.p1 += d; + } else if (role == SketchPointRole::P0) { // start = RADIUS handle (keep angles) + const double R = (target - e.center).norm(); + if (R < 1e-6) return; + e.radius = R; + e.p0 = e.center + R * Vec2d(std::cos(e.start_angle), std::sin(e.start_angle)); + e.p1 = e.center + R * Vec2d(std::cos(e.end_angle), std::sin(e.end_angle)); + } else if (role == SketchPointRole::P1) { // end = ANGLE handle (keep radius) + const Vec2d d = target - e.center; + if (d.squaredNorm() < 1e-12) return; + // Keep the CCW sweep continuous (0,2pi) so the arc never flips to its complement. + double da = std::atan2(d.y(), d.x()) - e.start_angle; + while (da < 0.0) da += 2.0 * M_PI; + while (da >= 2.0 * M_PI) da -= 2.0 * M_PI; + e.end_angle = e.start_angle + da; + e.p1 = e.center + e.radius * Vec2d(std::cos(e.end_angle), std::sin(e.end_angle)); + } + resolve_live(); +} + +// Drag an elliptical-arc grip. Center rigidly translates (endpoints + frame move with it); +// P0/P1 set the sweep start/end to the cursor's parametric angle on the ellipse, keeping +// the ellipse shape (a/b/phi). Geometric, then resolve_live(). +void DesignSketchTool::drag_ellipsearc_handle(int ei, SketchPointRole role, const Vec2d& target) +{ + if (ei < 0 || ei >= int(m_entities.size())) return; + SketchEntity& e = m_entities[ei]; + if (e.type != SketchEntity::Type::EllipseArc) return; + if (role == SketchPointRole::Center) { + const Vec2d d = target - e.center; + e.center = target; e.p0 += d; e.p1 += d; + } else if (role == SketchPointRole::P0 || role == SketchPointRole::P1) { + const double t = ellipse_param_of(e.center, e.radius, e.rminor, e.rotation, target); + if (role == SketchPointRole::P0) { + e.start_angle = t; + e.p0 = ellipse_point(e.center, e.radius, e.rminor, e.rotation, t); + } else { + // Keep the CCW sweep (end strictly after start) so the arc never inverts. + double t1 = t; while (t1 <= e.start_angle) t1 += 2.0 * M_PI; + e.end_angle = t1; + e.p1 = ellipse_point(e.center, e.radius, e.rminor, e.rotation, t1); + } + } + resolve_live(); +} + +void DesignSketchTool::open_ellipse_axis_editor(int ei, bool major) +{ + if (ei < 0 || ei >= int(m_entities.size()) || !on_inline_edit) return; + const SketchEntity& e = m_entities[ei]; + if (e.type != SketchEntity::Type::Ellipse && e.type != SketchEntity::Type::EllipseArc) return; + const double v = major ? e.radius : e.rminor; + const wxPoint px(m_last_mouse_x, m_last_mouse_y); + on_inline_edit(px, v, + [this, ei, major](double nv) { set_ellipse_axis(ei, major, nv); }, + []() {}); +} + +// Set a semi-axis to `v`: major -> e.radius, minor -> e.rminor; keep OCCT a >= b. +void DesignSketchTool::set_ellipse_axis(int ei, bool major, double v) +{ + if (ei < 0 || ei >= int(m_entities.size()) || v < 1e-6) return; + SketchEntity& e = m_entities[ei]; + if (e.type != SketchEntity::Type::Ellipse && e.type != SketchEntity::Type::EllipseArc) return; + if (major) e.radius = std::max(v, e.rminor); + else e.rminor = std::min(v, e.radius); + if (e.type == SketchEntity::Type::EllipseArc) { // endpoints ride the reshaped frame + e.p0 = ellipse_point(e.center, e.radius, e.rminor, e.rotation, e.start_angle); + e.p1 = ellipse_point(e.center, e.radius, e.rminor, e.rotation, e.end_angle); + } + resolve_live(); +} + +// Set an elliptical arc's included (parametric) sweep, keeping the start fixed and moving the +// end. Geometric (mirrors set_arc_sweep), then resolve_live for any endpoint coincidences. +void DesignSketchTool::set_ellipsearc_sweep(int ei, double deg) +{ + if (ei < 0 || ei >= int(m_entities.size())) return; + SketchEntity& e = m_entities[ei]; + if (e.type != SketchEntity::Type::EllipseArc) return; + const double sweep = std::max(1e-3, std::min(deg, 359.999)) * M_PI / 180.0; + const double sign = (e.end_angle >= e.start_angle) ? 1.0 : -1.0; + e.end_angle = e.start_angle + sign * sweep; + e.p1 = ellipse_point(e.center, e.radius, e.rminor, e.rotation, e.end_angle); + resolve_live(); +} + +// Rotate an oblique rectangle to an absolute orientation (angle of edge0 to +X), pivoting on +// its anchor corner f.c0. Geometric, mirrors set_polygon_angle: drop the now-inconsistent edge +// H/V first, rotate every member point + the opposite corner, and DON'T re-solve (the rotated +// loop is already consistent; a length Distance the user may have set is rotation-invariant). +void DesignSketchTool::set_rect_angle(int fi, double deg) +{ + if (fi < 0 || fi >= int(m_features.size())) return; + Feature& f = m_features[fi]; + if (f.begin < 0 || f.end > int(m_entities.size()) || f.end <= f.begin) return; + const SketchEntity& e0 = m_entities[f.begin]; + const Vec2d d0 = e0.p1 - e0.p0; + if (d0.squaredNorm() < 1e-12) return; + const double da = deg * M_PI / 180.0 - std::atan2(d0.y(), d0.x()); + drop_orientation_constraints(f.begin, f.end); + const Vec2d pivot = f.c0; + const double ca = std::cos(da), sa = std::sin(da); + // -> Vec2d REQUIRED (see set_polygon_angle): an auto return deduces an Eigen expression + // template referencing the destroyed temporary -> dangling. + auto rot = [&](const Vec2d& pt) -> Vec2d { const Vec2d r = pt - pivot; + return pivot + Vec2d(r.x() * ca - r.y() * sa, r.x() * sa + r.y() * ca); }; + for (int i = f.begin; i < f.end && i < int(m_entities.size()); ++i) { + SketchEntity& e = m_entities[i]; + e.p0 = rot(e.p0); e.p1 = rot(e.p1); + } + f.c1 = rot(f.c1); // keep the opposite corner consistent for later W/H quotes +} + +// Screen anchor for a Constrain-mode value field: over the picked geometry (its representative +// point — circle/arc centre, else segment midpoint; averaged when two entities are picked), +// projected to the viewport. Lets a dimensional constraint's field open ON the geometry like +// the draw-then-edit tools, instead of floating at viewport centre. False if no valid pick or +// it projects off-screen (caller falls back to centre). +bool DesignSketchTool::constrain_value_anchor(wxPoint& out) const +{ + if (m_pick0 < 0 || m_pick0 >= int(m_entities.size())) return false; + auto rep = [](const SketchEntity& e) -> Vec2d { + using T = SketchEntity::Type; + if (e.type == T::Circle || e.type == T::Arc || + e.type == T::Ellipse || e.type == T::EllipseArc) return e.center; + return 0.5 * (e.p0 + e.p1); + }; + Vec2d p = rep(m_entities[m_pick0]); + if (m_pick1 >= 0 && m_pick1 < int(m_entities.size())) + p = 0.5 * (p + rep(m_entities[m_pick1])); + const Camera& cam = wxGetApp().plater()->get_camera(); + const wxPoint sp = world_to_screen_px(cam, m_plane.to_world(p)); + const std::array& vp = cam.get_viewport(); + if (sp.x < vp[0] || sp.y < vp[1] || sp.x > vp[0] + vp[2] || sp.y > vp[1] + vp[3]) return false; + out = sp; + return true; +} + +void DesignSketchTool::open_rounded_rect_editor(int fi, int which) +{ + if (fi < 0 || fi >= int(m_features.size()) || !on_inline_edit) return; + const Feature& f = m_features[fi]; + const double w = std::abs(f.c1.x() - f.c0.x()); + const double h = std::abs(f.c1.y() - f.c0.y()); + const double r = f.param; + const double v = (which == 0) ? w : (which == 1) ? h : r; + const wxPoint px(m_last_mouse_x, m_last_mouse_y); + on_inline_edit(px, v, + [this, fi, which](double nv) { + const Feature& g = m_features[fi]; + double gw = std::abs(g.c1.x() - g.c0.x()); + double gh = std::abs(g.c1.y() - g.c0.y()); + double gr = g.param; + if (which == 0) gw = nv; else if (which == 1) gh = nv; else gr = nv; + set_rounded_rect(fi, gw, gh, gr); + }, + []() {}); +} + +// Rebuild the rounded-rect's 8 entities in place for a new width/height/fillet radius, +// keeping the min corner (c0) fixed. Geometric (entity order/count preserved so constraint +// refs stay valid); fillet clamped to (0, min(w,h)/2]. +void DesignSketchTool::set_rounded_rect(int fi, double w, double h, double r) +{ + if (fi < 0 || fi >= int(m_features.size())) return; + Feature& f = m_features[fi]; + if (f.begin < 0 || f.end > int(m_entities.size()) || f.end <= f.begin) return; + w = std::max(w, 1e-3); h = std::max(h, 1e-3); + r = std::max(1e-3, std::min(r, std::min(w, h) * 0.5 - 1e-4)); + const double xmin = std::min(f.c0.x(), f.c1.x()), ymin = std::min(f.c0.y(), f.c1.y()); + const double xmax = xmin + w, ymax = ymin + h; + std::vector rebuilt = rounded_rect_entities(xmin, ymin, xmax, ymax, r); + if (int(rebuilt.size()) != f.end - f.begin) return; // count must match to keep con refs + for (int i = 0; i < int(rebuilt.size()); ++i) { + rebuilt[i].construction = m_entities[f.begin + i].construction; // preserve flag + m_entities[f.begin + i] = rebuilt[i]; + } + f.c0 = Vec2d(xmin, ymin); f.c1 = Vec2d(xmax, ymax); f.param = r; + resolve_live(); +} + +void DesignSketchTool::open_arc_slot_editor(int fi, bool radius) +{ + if (fi < 0 || fi >= int(m_features.size()) || !on_inline_edit) return; + const Feature& f = m_features[fi]; + const double Rc = (f.c1 - f.c0).norm(); + const double v = radius ? Rc : (2.0 * f.param); // width quote shows the FULL width + const wxPoint px(m_last_mouse_x, m_last_mouse_y); + on_inline_edit(px, v, + [this, fi, radius](double nv) { + const Feature& g = m_features[fi]; + const double gRc = (g.c1 - g.c0).norm(); + if (radius) set_arc_slot(fi, nv, g.param); + else set_arc_slot(fi, gRc, std::max(1e-3, nv * 0.5)); // full width -> half + }, + []() {}); +} + +// Rebuild the arc-slot's 4 arcs in place for a new centreline radius / half-width. Centre +// + the two centreline directions are kept (the end direction is recovered from the cap@E +// arc centre). Geometric; entity count preserved so constraint refs stay valid. +void DesignSketchTool::set_arc_slot(int fi, double Rc, double w) +{ + if (fi < 0 || fi >= int(m_features.size())) return; + Feature& f = m_features[fi]; + if (f.begin < 0 || f.end > int(m_entities.size()) || f.end - f.begin != 4) return; + const Vec2d center = f.c0; + Vec2d dirS = f.c1 - center; + const Vec2d Ec = m_entities[f.begin + 1].center; // cap@E centre = centreline end + Vec2d dirE = Ec - center; + if (dirS.squaredNorm() < 1e-12 || dirE.squaredNorm() < 1e-12) return; + dirS.normalize(); dirE.normalize(); + Rc = std::max(Rc, 2e-3); + w = std::max(1e-3, std::min(w, Rc - 1e-3)); // make_arc_slot needs w < Rc + std::vector rebuilt = + make_arc_slot(center, center + Rc * dirS, center + Rc * dirE, w); + if (int(rebuilt.size()) != 4) return; + for (int i = 0; i < 4; ++i) { + rebuilt[i].construction = m_entities[f.begin + i].construction; + m_entities[f.begin + i] = rebuilt[i]; + } + f.c1 = center + Rc * dirS; f.param = w; + resolve_live(); +} + +// Resize an axis-aligned rectangle by dragging a corner: the diagonally-opposite corner +// (captured at grab as m_drag_rect_anchor) stays fixed; the box becomes [anchor, cursor]. +// Geometric rebuild in place (4 lines, same order) — edges stay axis-aligned so the +// inferred H/V + corner-coincident constraints remain satisfied (no re-solve needed). +void DesignSketchTool::drag_rect_corner(int fi, const Vec2d& cursor) +{ + if (fi < 0 || fi >= int(m_features.size())) return; + Feature& f = m_features[fi]; + if (f.end - f.begin != 4) return; + const Vec2d A = m_drag_rect_anchor, B = cursor; + if (std::abs(B.x() - A.x()) < 1e-4 || std::abs(B.y() - A.y()) < 1e-4) return; // degenerate + const Vec2d corners[4] = { A, Vec2d(B.x(), A.y()), B, Vec2d(A.x(), B.y()) }; + for (int i = 0; i < 4; ++i) { + SketchEntity e; e.type = SketchEntity::Type::Line; + e.p0 = corners[i]; e.p1 = corners[(i + 1) % 4]; + e.construction = m_entities[f.begin + i].construction; + m_entities[f.begin + i] = e; + } + f.c0 = A; f.c1 = B; +} + +// Move one end of a slot by dragging its cap centre (which cap captured at grab); the other +// centre + half-width are kept. Rebuilds the 4-entity span via make_slot. Geometric. +void DesignSketchTool::drag_slot_handle(int fi, const Vec2d& cursor) +{ + if (fi < 0 || fi >= int(m_features.size())) return; + Feature& f = m_features[fi]; + if (f.end - f.begin != 4) return; + const Vec2d c0 = m_drag_slot_c1 ? f.c0 : cursor; + const Vec2d c1 = m_drag_slot_c1 ? cursor : f.c1; + std::vector rebuilt = make_slot(c0, c1, f.param); + if (int(rebuilt.size()) != 4) return; + for (int i = 0; i < 4; ++i) { + rebuilt[i].construction = m_entities[f.begin + i].construction; + m_entities[f.begin + i] = rebuilt[i]; + } + f.c0 = c0; f.c1 = c1; +} + +DesignSketchTool::DimType DesignSketchTool::pending_dimension_type() const +{ + return (m_pending_dim >= 0 && m_pending_dim < int(m_dimensions.size())) + ? m_dimensions[m_pending_dim].kind : DimType::None; +} + +void DesignSketchTool::set_dimension_value(double v) +{ + if (m_pending_dim < 0 || m_pending_dim >= int(m_dimensions.size())) return; + DimAnnot& a = m_dimensions[m_pending_dim]; + a.value = v; + if (a.con >= 0 && a.con < int(m_constraints.size())) + m_constraints[a.con] = constraint_for(a); + resolve_live(); + m_pending_dim = -1; +} + +void DesignSketchTool::cancel_dimension_value() +{ + m_pending_dim = -1; // keep the placed dimension at its measured value +} + +std::string DesignSketchTool::dim_text(const DimAnnot& a) const +{ + char buf[32]; + const char* prefix = (a.kind == DimType::Diameter) ? "\xC3\x98" // 'Ø' + : (a.kind == DimType::Radius) ? "R" : ""; + const char* suffix = (a.kind == DimType::Angle) ? "\xC2\xB0" : ""; // '°' + std::snprintf(buf, sizeof(buf), "%s%.1f%s", prefix, a.value, suffix); + // Force the international (en) decimal point: wx sets LC_NUMERIC to the user + // locale at startup, so snprintf("%.1f") can emit a comma. Normalise it. + for (char& ch : buf) + if (ch == ',') ch = '.'; + return std::string(buf); +} + +void DesignSketchTool::apply_segment_length(double len) +{ + if (m_points.size() == 2 && len > 1e-9) { + const Vec2d d = m_points[1] - m_points[0]; + const double r = d.norm(); + if (r > 1e-9) + m_points[1] = m_points[0] + (len / r) * d; + } + keep_segment_as_drawn(); + // Driving length constraint on the just-committed Line entity. + if (len > 1e-9 && !m_entities.empty()) { + const int i = int(m_entities.size()) - 1; + if (m_entities[i].type == SketchEntity::Type::Line) { + SketchEntityConstraintDef c; + c.type = SketchConstraintType::Distance; + c.ea = i; c.ra = SketchPointRole::P0; + c.eb = i; c.rb = SketchPointRole::P1; + c.value = len; + m_constraints.push_back(c); + } + } + resolve_live(); // live-solve the in-session sketch +} + +void DesignSketchTool::keep_segment_as_drawn() +{ + if (m_points.size() == 2) { + const int base = int(m_entities.size()); + push_line(m_points[0], m_points[1]); // accrues into the session's entities + infer_auto_constraints(base); // auto Coincident at snapped ends + H/V + } + m_points.clear(); + m_has_cursor = false; + m_awaiting_length = false; +} + +void DesignSketchTool::finish() +{ + if (op_ready()) confirm_op(); // apply a pending edit-op gizmo before committing + if (tf_ready()) confirm_transform(); // apply a pending transform gizmo before committing + auto cb = on_commit_entities; + std::vector ents = m_entities; + std::vector cons = m_constraints; + SketchPlane pl = m_plane; + m_active = false; + m_points.clear(); + m_entities.clear(); + m_constraints.clear(); + m_dimensions.clear(); + m_point_sel.clear(); + m_dim_has0 = false; + m_pending_dim = -1; + m_has_cursor = false; + m_features.clear(); + m_open_feature = -1; + if (cb) + cb(ents, cons, pl); +} + +void DesignSketchTool::begin_constrain(const SketchProfile& prof, const SketchPlane& plane) +{ + m_plane = plane; + m_mode = Mode::Constrain; + m_points = prof.points; + m_entities.clear(); + m_has_cursor = false; + m_sel_a = m_sel_b = -1; + m_constrain_entities = false; + m_pick0 = m_pick1 = m_pick2 = -1; + m_active = true; +} + +void DesignSketchTool::begin_constrain_entities(const std::vector& ents, + const SketchPlane& plane) +{ + m_plane = plane; + m_mode = Mode::Constrain; + m_constrain_entities = true; + m_points.clear(); + m_entities = ents; + m_has_cursor = false; + m_sel_a = m_sel_b = -1; + m_pick0 = m_pick1 = m_pick2 = -1; + m_active = true; +} + +bool DesignSketchTool::selected_segment(int& a, int& b) const +{ + if (m_sel_a < 0 || m_sel_b < 0) + return false; + a = m_sel_a; + b = m_sel_b; + return true; +} + +bool DesignSketchTool::screen_to_plane(GLCanvas3D& canvas, const wxMouseEvent& evt, Vec2d& out) const +{ + Point pos(evt.GetX(), evt.GetY()); + Linef3 r = canvas.mouse_ray(pos); + out = m_plane.project(r.a, r.vector()); + return true; +} + +bool DesignSketchTool::near_first(const Vec2d& p) const +{ + return m_points.size() >= 3 && (p - m_points[0]).squaredNorm() < 4.0; +} + +Vec2d DesignSketchTool::snap_dir(const Vec2d& anchor, const Vec2d& raw, bool& locked) const +{ + locked = false; + if (m_snap_off) return raw; + + const Vec2d d = raw - anchor; + const double r = d.norm(); + if (r < 1e-9) return raw; + + const double tol_deg = 5.0; // inference half-window + double ang = std::atan2(d.y(), d.x()) * 180.0 / M_PI; // (-180,180] + if (ang < 0.0) ang += 360.0; // [0,360) + + // Base angles within one quadrant, replicated every 90 deg up to 360. + static const double base[] = {0.0, 30.0, 45.0, 60.0}; + double best_cand = ang, best_diff = 1e30; + for (int q = 0; q < 4; ++q) { + for (double b : base) { + const double cand = b + 90.0 * q; + double diff = std::abs(ang - cand); + if (diff > 180.0) diff = 360.0 - diff; + if (diff < best_diff) { best_diff = diff; best_cand = cand; } + } + } + if (best_diff > tol_deg) return raw; + + locked = true; + const double rad = best_cand * M_PI / 180.0; + return anchor + r * Vec2d(std::cos(rad), std::sin(rad)); +} + +double DesignSketchTool::screen_tol(GLCanvas3D& canvas, const wxMouseEvent& evt, + const Vec2d& at, double px) const +{ + // Project a point `px` screen pixels away and measure the gap in plane units. + const Linef3 r2 = canvas.mouse_ray(Point(evt.GetX() + int(px), evt.GetY())); + const Vec2d p2 = m_plane.project(r2.a, r2.vector()); + return std::max(1e-3, (p2 - at).norm()); +} + +InferenceSnap DesignSketchTool::infer_at(GLCanvas3D& canvas, const wxMouseEvent& evt, + const Vec2d& raw) const +{ + if (evt.ShiftDown()) { InferenceSnap s; s.point = raw; return s; } // Shift suppresses + const double tol = screen_tol(canvas, evt, raw); + return infer_point_snap(m_entities, raw, tol); +} + +Vec2d DesignSketchTool::snap_vertex(GLCanvas3D& canvas, const wxMouseEvent& evt, + const Vec2d& raw, bool& snapped) const +{ + const InferenceSnap s = infer_at(canvas, evt, raw); + // Cache the target so render() can draw a snap hint; only endpoint/centre/origin + // count as a "vertex" snap for the callers that gate angle inference on it. + const_cast(this)->m_cursor_snap = s; + snapped = s.snapped(); // any hard snap moves the cursor + suppresses angle lock + return s.point; +} + +bool DesignSketchTool::has_coincident(int ea, SketchPointRole ra, int eb, SketchPointRole rb) const +{ + for (const auto& c : m_constraints) { + if (c.type != SketchConstraintType::Coincident) continue; + if ((c.ea == ea && c.ra == ra && c.eb == eb && c.rb == rb) || + (c.ea == eb && c.ra == rb && c.eb == ea && c.rb == ra)) + return true; + } + return false; +} + +bool DesignSketchTool::try_add_constraints(const std::vector& cands) +{ + if (cands.empty()) return true; + const size_t mark = m_constraints.size(); + for (const auto& c : cands) m_constraints.push_back(c); + if (solve_sketch_entities(m_entities, m_constraints)) + return true; + m_constraints.resize(mark); // roll back the conflicting batch + solve_sketch_entities(m_entities, m_constraints); // restore prior solved state + return false; +} + +void DesignSketchTool::infer_auto_constraints(int base) +{ + const int n = int(m_entities.size()); + if (base < 0 || base >= n) return; + + // Endpoint roles an entity exposes for coincidence matching. + auto roles_of = [](const SketchEntity& e, SketchPointRole out[2]) -> int { + switch (e.type) { + case SketchEntity::Type::Line: out[0] = SketchPointRole::P0; out[1] = SketchPointRole::P1; return 2; + case SketchEntity::Type::Arc: out[0] = SketchPointRole::P0; out[1] = SketchPointRole::P1; return 2; + case SketchEntity::Type::BSpline:out[0] = SketchPointRole::P0; out[1] = SketchPointRole::P1; return 2; + case SketchEntity::Type::Point: out[0] = SketchPointRole::P0; return 1; + default: return 0; // circle: centre coincidence handled by Concentric, not here + } + }; + + // 1) Coincident between a new endpoint and any (co-located) endpoint of another + // entity. snap_vertex already drove the coordinates together; this records it + // so a re-solve keeps the loop closed. + std::vector coincs; + for (int i = base; i < n; ++i) { + SketchPointRole ir[2]; const int ni = roles_of(m_entities[i], ir); + for (int a = 0; a < ni; ++a) { + Vec2d pa; if (!point_at(i, ir[a], pa)) continue; + for (int j = 0; j < n; ++j) { + if (j == i) continue; + SketchPointRole jr[2]; const int nj = roles_of(m_entities[j], jr); + for (int b = 0; b < nj; ++b) { + if (j >= base && j < i) continue; // avoid duplicate (i,j)/(j,i) + Vec2d pb; if (!point_at(j, jr[b], pb)) continue; + if ((pa - pb).squaredNorm() > 1e-6) continue; + if (has_coincident(i, ir[a], j, jr[b])) continue; + SketchEntityConstraintDef c; + c.type = SketchConstraintType::Coincident; + c.ea = i; c.ra = ir[a]; c.eb = j; c.rb = jr[b]; + coincs.push_back(c); + } + } + } + } + try_add_constraints(coincs); // co-located points: consistent by construction + + // 2) Horizontal / Vertical on axis-aligned new line segments (added one at a time + // so a single conflict never drops the others). + for (int i = base; i < n; ++i) { + if (m_entities[i].type != SketchEntity::Type::Line) continue; + auto ax = infer_axis_constraint(m_entities[i].p0, m_entities[i].p1); + if (!ax) continue; + SketchEntityConstraintDef c; + c.type = *ax; + c.ea = i; c.ra = SketchPointRole::P0; + c.eb = i; c.rb = SketchPointRole::P1; + try_add_constraints({ c }); + } + + resolve_live(); +} + +static std::vector circle_polygon(const Vec2d& c, double r, int n = 48) +{ + std::vector v; v.reserve(n); + for (int i = 0; i < n; ++i) { + const double a = 2.0 * M_PI * double(i) / double(n); + v.push_back(Vec2d(c.x() + r * std::cos(a), c.y() + r * std::sin(a))); + } + return v; +} + +// Point on an ellipse at parametric angle theta: center + R(phi)*(a cos t, b sin t). +static Vec2d ellipse_point(const Vec2d& c, double a, double b, double phi, double t) +{ + const double cu = std::cos(phi), su = std::sin(phi); + const double x = a * std::cos(t), y = b * std::sin(t); + return Vec2d(c.x() + x * cu - y * su, c.y() + x * su + y * cu); +} + +// Tessellate an ellipse arc parametric range [t0,t1] into a polyline. +static std::vector ellipse_polyline(const Vec2d& c, double a, double b, double phi, + double t0, double t1, int n = 48) +{ + std::vector v; v.reserve(n + 1); + for (int i = 0; i <= n; ++i) + v.push_back(ellipse_point(c, a, b, phi, t0 + (t1 - t0) * double(i) / double(n))); + return v; +} + +// Clamped uniform B-spline (degree min(3, n-1)) through control poles. The knot +// construction mirrors SketchEngine::entities_to_wire's OCCT Geom_BSplineCurve so +// the previewed/extruded curve match. de Boor evaluation. +static int bspline_degree(int n) { return n >= 4 ? 3 : (n >= 2 ? n - 1 : 0); } + +static std::vector bspline_knots(int n, int p) +{ + std::vector U; // full knot vector, length n+p+1 + const int interior = n - p - 1; + for (int i = 0; i <= p; ++i) U.push_back(0.0); + for (int i = 1; i <= interior; ++i) U.push_back(double(i)); + const double last = double(interior + 1); + for (int i = 0; i <= p; ++i) U.push_back(last); + return U; +} + +static Vec2d bspline_eval(const std::vector& P, const std::vector& U, int p, double u) +{ + const int n = int(P.size()); + if (u <= U[p]) return P.front(); + if (u >= U[n]) return P.back(); // U[n] == domain max (clamped) + int k = p; + while (k < n - 1 && U[k + 1] <= u) ++k; // span: U[k] <= u < U[k+1] + std::vector d(p + 1); + for (int j = 0; j <= p; ++j) d[j] = P[j + k - p]; + for (int r = 1; r <= p; ++r) + for (int j = p; j >= r; --j) { + const double denom = U[j + 1 + k - r] - U[j + k - p]; + const double a = denom > 1e-12 ? (u - U[j + k - p]) / denom : 0.0; + d[j] = (1.0 - a) * d[j - 1] + a * d[j]; + } + return d[p]; +} + +static std::vector bspline_polyline(const std::vector& ctrl, int samples = 0) +{ + const int n = int(ctrl.size()); + if (n < 2) return ctrl; + const int p = bspline_degree(n); + const std::vector U = bspline_knots(n, p); + const double umax = double(n - p); + if (samples <= 0) samples = std::max(24, 14 * (n - 1)); + std::vector out; out.reserve(samples + 1); + for (int i = 0; i <= samples; ++i) + out.push_back(bspline_eval(ctrl, U, p, umax * double(i) / double(samples))); + return out; +} + +// ---- entity builders -------------------------------------------------------- + +void DesignSketchTool::push_line(const Vec2d& a, const Vec2d& b) +{ + if ((b - a).squaredNorm() < 1e-9) + return; + SketchEntity e; + e.type = SketchEntity::Type::Line; + e.p0 = a; + e.p1 = b; + e.construction = m_construction; + m_entities.push_back(e); +} + +void DesignSketchTool::push_closed_lines(const std::vector& corners) +{ + const size_t n = corners.size(); + if (n < 2) return; + for (size_t i = 0; i < n; ++i) + push_line(corners[i], corners[(i + 1) % n]); +} + +void DesignSketchTool::push_open_chain(const std::vector& pts) +{ + for (size_t i = 0; i + 1 < pts.size(); ++i) + push_line(pts[i], pts[i + 1]); +} + +void DesignSketchTool::push_circle(const Vec2d& center, double radius) +{ + if (radius < 1e-3) return; + SketchEntity e; + e.type = SketchEntity::Type::Circle; + e.center = center; + e.p0 = center; + e.radius = radius; + e.construction = m_construction; + m_entities.push_back(e); +} + +void DesignSketchTool::push_point(const Vec2d& p) +{ + SketchEntity e; + e.type = SketchEntity::Type::Point; + e.p0 = p; + e.center = p; + e.construction = m_construction; + m_entities.push_back(e); +} + +void DesignSketchTool::append_entities(const std::vector& ents) +{ + for (const SketchEntity& e : ents) + m_entities.push_back(e); +} + +static double wrap_2pi(double a) +{ + while (a < 0.0) a += 2.0 * M_PI; + while (a >= 2.0 * M_PI) a -= 2.0 * M_PI; + return a; +} + +// Circumcircle of 3 points. Returns false if (nearly) collinear. +static bool circumcircle(const Vec2d& a, const Vec2d& b, const Vec2d& c, + Vec2d& center, double& radius) +{ + const double d = 2.0 * (a.x() * (b.y() - c.y()) + + b.x() * (c.y() - a.y()) + + c.x() * (a.y() - b.y())); + if (std::abs(d) < 1e-9) + return false; + const double a2 = a.squaredNorm(), b2 = b.squaredNorm(), c2 = c.squaredNorm(); + const double ux = (a2 * (b.y() - c.y()) + b2 * (c.y() - a.y()) + c2 * (a.y() - b.y())) / d; + const double uy = (a2 * (c.x() - b.x()) + b2 * (a.x() - c.x()) + c2 * (b.x() - a.x())) / d; + center = Vec2d(ux, uy); + radius = (a - center).norm(); + return true; +} + +// Build an Arc entity that sweeps start -> end passing through `through`. The +// kernel reconstructs the mid from (start_angle+end_angle)/2, so the angle pair +// must bracket `through` on the correct side of the circle. +static SketchEntity make_arc_through(const Vec2d& center, double radius, + const Vec2d& start, const Vec2d& end, + const Vec2d& through, bool construction) +{ + const double a_start = std::atan2(start.y() - center.y(), start.x() - center.x()); + const double a_end = std::atan2(end.y() - center.y(), end.x() - center.x()); + const double a_thru = std::atan2(through.y() - center.y(), through.x() - center.x()); + const double de = wrap_2pi(a_end - a_start); // CCW sweep to end (0,2π) + const double d3 = wrap_2pi(a_thru - a_start); // CCW position of through + SketchEntity e; + e.type = SketchEntity::Type::Arc; + e.center = center; + e.radius = radius; + e.p0 = start; + e.p1 = end; + e.start_angle = a_start; + e.end_angle = (d3 <= de) ? (a_start + de) : (a_start + de - 2.0 * M_PI); + e.construction = construction; + return e; +} + +std::vector DesignSketchTool::make_three_point_circle(const Vec2d& a, const Vec2d& b, const Vec2d& c) const +{ + Vec2d center; double radius; + if (!circumcircle(a, b, c, center, radius)) + return {}; + SketchEntity e; + e.type = SketchEntity::Type::Circle; + e.center = center; + e.p0 = center; + e.radius = radius; + e.construction = m_construction; + return { e }; +} + +std::vector DesignSketchTool::make_three_point_arc(const Vec2d& start, const Vec2d& end, const Vec2d& on_arc) const +{ + Vec2d center; double radius; + if (!circumcircle(start, end, on_arc, center, radius)) + return {}; + return { make_arc_through(center, radius, start, end, on_arc, m_construction) }; +} + +std::vector DesignSketchTool::make_tangent_arc(const Vec2d& start, const Vec2d& end) const +{ + // Tangent direction at `start` = exit direction of the previous entity. + Vec2d t(1, 0); + bool have_t = false; + if (!m_entities.empty()) { + const SketchEntity& prev = m_entities.back(); + if (prev.type == SketchEntity::Type::Line) { + t = prev.p1 - prev.p0; have_t = (t.squaredNorm() > 1e-12); + } else if (prev.type == SketchEntity::Type::Arc) { + // Tangent at the arc end p1 is perpendicular to its radius, in the + // sweep direction. + const Vec2d r = prev.p1 - prev.center; + const double sweep = prev.end_angle - prev.start_angle; + t = (sweep >= 0.0) ? Vec2d(-r.y(), r.x()) : Vec2d(r.y(), -r.x()); + have_t = (r.squaredNorm() > 1e-12); + } + } + const Vec2d se = end - start; + if (!have_t || se.squaredNorm() < 1e-12) { + // No tangent reference or zero length: fall back to a straight line. + SketchEntity e; e.type = SketchEntity::Type::Line; e.p0 = start; e.p1 = end; + e.construction = m_construction; + return { e }; + } + t.normalize(); + const Vec2d n(-t.y(), t.x()); // unit normal to the tangent + const double denom = 2.0 * n.dot(se); + if (std::abs(denom) < 1e-9) { // end lies along the tangent: line + SketchEntity e; e.type = SketchEntity::Type::Line; e.p0 = start; e.p1 = end; + e.construction = m_construction; + return { e }; + } + const double R = se.squaredNorm() / denom; // signed radius along n + const Vec2d center = start + n * R; + const double radius = std::abs(R); + // Mid of the tangent arc: project the chord midpoint outward onto the circle. + const Vec2d chord_mid = (start + end) * 0.5; + Vec2d to_mid = chord_mid - center; + if (to_mid.squaredNorm() < 1e-12) to_mid = n; + to_mid.normalize(); + const Vec2d through = center + to_mid * radius; + return { make_arc_through(center, radius, start, end, through, m_construction) }; +} + +std::vector DesignSketchTool::make_center_arc(const Vec2d& center, const Vec2d& start, const Vec2d& end_dir) const +{ + const double radius = (start - center).norm(); + if (radius < 1e-9) + return {}; + const double a_start = std::atan2(start.y() - center.y(), start.x() - center.x()); + const double a_end = std::atan2(end_dir.y() - center.y(), end_dir.x() - center.x()); + const double de = wrap_2pi(a_end - a_start); // CCW sweep start -> end (0,2π) + const Vec2d end = center + radius * Vec2d(std::cos(a_end), std::sin(a_end)); + const double a_mid = a_start + de * 0.5; // bisector brackets the sweep + const Vec2d through = center + radius * Vec2d(std::cos(a_mid), std::sin(a_mid)); + return { make_arc_through(center, radius, start, end, through, m_construction) }; +} + +std::vector DesignSketchTool::make_slot(const Vec2d& c0, const Vec2d& c1, double half_width) const +{ + std::vector out; + Vec2d u = c1 - c0; + if (u.squaredNorm() < 1e-12 || half_width < 1e-6) + return out; + u.normalize(); + const Vec2d nrm(-u.y(), u.x()); + const double w = half_width; + // Names avoid termios macros (B0 is a baud-rate #define pulled in transitively). + const Vec2d top0 = c0 + nrm * w, top1 = c1 + nrm * w; // upper side (c0 -> c1) + const Vec2d bot1 = c1 - nrm * w, bot0 = c0 - nrm * w; // lower side (c1 -> c0) + + auto line = [&](const Vec2d& p0, const Vec2d& p1) { + SketchEntity e; e.type = SketchEntity::Type::Line; e.p0 = p0; e.p1 = p1; + e.construction = m_construction; return e; + }; + out.push_back(line(top0, top1)); // top + out.push_back(make_arc_through(c1, w, top1, bot1, c1 + u * w, m_construction)); // cap @c1 (+u) + out.push_back(line(bot1, bot0)); // bottom + out.push_back(make_arc_through(c0, w, bot0, top0, c0 - u * w, m_construction)); // cap @c0 (-u) + return out; +} + +// Arc slot: a slot whose centerline is a circular arc (center, start, end_dir on +// the same radius). Bounded by an outer arc (Rc+w), an inner arc (Rc-w) and two +// semicircular end caps. CCW closed loop, mirroring make_slot's structure. +std::vector DesignSketchTool::make_arc_slot(const Vec2d& center, const Vec2d& start, + const Vec2d& end_dir, double half_width) const +{ + std::vector out; + const double Rc = (start - center).norm(); + const double w = half_width; + if (Rc < 1e-6 || w < 1e-6 || w >= Rc) return out; + const Vec2d dirS = (start - center) / Rc; + Vec2d de = end_dir - center; + if (de.squaredNorm() < 1e-12) return out; + const Vec2d dirE = de.normalized(); + const double aS = std::atan2(dirS.y(), dirS.x()); + const double aE = std::atan2(dirE.y(), dirE.x()); + const double sweep = wrap_2pi(aE - aS); // CCW start -> end + const double aMid = aS + sweep * 0.5; + const Vec2d uMid(std::cos(aMid), std::sin(aMid)); + const Vec2d Sc = center + Rc * dirS; // centerline start point (cap centre) + const Vec2d Ec = center + Rc * dirE; // centerline end point (cap centre) + const Vec2d S_out = center + (Rc + w) * dirS, S_in = center + (Rc - w) * dirS; + const Vec2d E_out = center + (Rc + w) * dirE, E_in = center + (Rc - w) * dirE; + const Vec2d tE(-dirE.y(), dirE.x()); // CCW travel-forward tangent at E + const Vec2d tS(-dirS.y(), dirS.x()); // CCW travel-forward tangent at S + out.push_back(make_arc_through(center, Rc + w, S_out, E_out, center + (Rc + w) * uMid, m_construction)); // outer + out.push_back(make_arc_through(Ec, w, E_out, E_in, Ec + w * tE, m_construction)); // cap @E (forward) + out.push_back(make_arc_through(center, Rc - w, E_in, S_in, center + (Rc - w) * uMid, m_construction)); // inner + out.push_back(make_arc_through(Sc, w, S_in, S_out, Sc - w * tS, m_construction)); // cap @S (backward) + return out; +} + +// Rounded rectangle: axis-aligned box (a,b opposite corners) with filleted +// corners. radius_pt's distance to the nearest corner sets the fillet radius. +// 4 straight edges + 4 quarter arcs, CCW. +// Build the 8 entities (4 lines + 4 corner arcs, CCW) of an axis-aligned rounded box +// from explicit bounds + fillet radius. Shared by make_rounded_rect (gesture) and +// set_rounded_rect (label/handle edit) so the entity order/count is identical → a rebuild +// in place keeps constraint indices into the feature span valid. +std::vector DesignSketchTool::rounded_rect_entities(double xmin, double ymin, + double xmax, double ymax, double r) const +{ + std::vector out; + auto line = [&](const Vec2d& p0, const Vec2d& p1) { + SketchEntity e; e.type = SketchEntity::Type::Line; e.p0 = p0; e.p1 = p1; + e.construction = m_construction; return e; }; + auto corner = [&](const Vec2d& O, const Vec2d& sharp, const Vec2d& start, const Vec2d& end) { + const Vec2d thr = O + r * (sharp - O).normalized(); + return make_arc_through(O, r, start, end, thr, m_construction); }; + out.push_back(line({xmin + r, ymin}, {xmax - r, ymin})); // bottom + out.push_back(corner({xmax - r, ymin + r}, {xmax, ymin}, {xmax - r, ymin}, {xmax, ymin + r})); // BR + out.push_back(line({xmax, ymin + r}, {xmax, ymax - r})); // right + out.push_back(corner({xmax - r, ymax - r}, {xmax, ymax}, {xmax, ymax - r}, {xmax - r, ymax})); // TR + out.push_back(line({xmax - r, ymax}, {xmin + r, ymax})); // top + out.push_back(corner({xmin + r, ymax - r}, {xmin, ymax}, {xmin + r, ymax}, {xmin, ymax - r})); // TL + out.push_back(line({xmin, ymax - r}, {xmin, ymin + r})); // left + out.push_back(corner({xmin + r, ymin + r}, {xmin, ymin}, {xmin, ymin + r}, {xmin + r, ymin})); // BL + return out; +} + +std::vector DesignSketchTool::make_rounded_rect(const Vec2d& a, const Vec2d& b, const Vec2d& radius_pt) const +{ + std::vector out; + const double xmin = std::min(a.x(), b.x()), xmax = std::max(a.x(), b.x()); + const double ymin = std::min(a.y(), b.y()), ymax = std::max(a.y(), b.y()); + const double bw = xmax - xmin, bh = ymax - ymin; + if (bw < 1e-6 || bh < 1e-6) return out; + const Vec2d cs[4] = { {xmin,ymin}, {xmax,ymin}, {xmax,ymax}, {xmin,ymax} }; + double r = 1e18; + for (const Vec2d& c : cs) r = std::min(r, (radius_pt - c).norm()); + r = std::min(r, std::min(bw, bh) * 0.5); + if (r < 1e-6) { // degenerate -> plain rectangle + auto line = [&](const Vec2d& p0, const Vec2d& p1) { + SketchEntity e; e.type = SketchEntity::Type::Line; e.p0 = p0; e.p1 = p1; + e.construction = m_construction; return e; }; + for (int i = 0; i < 4; ++i) out.push_back(line(cs[i], cs[(i + 1) % 4])); + return out; + } + return rounded_rect_entities(xmin, ymin, xmax, ymax, r); +} + +std::vector DesignSketchTool::make_polygon(const Vec2d& center, const Vec2d& vertex, int sides) const +{ + std::vector out; + if (sides < 3) sides = 3; + const Vec2d rv = vertex - center; + const double d = rv.norm(); + if (d < 1e-6) return out; + // Inscribed: cursor is a vertex (circumradius = d). Circumscribed: cursor is an + // edge midpoint (apothem = d) → circumradius R = d / cos(pi/n), rotated by half + // a step so an edge midpoint points at the cursor. + double R = d, a0 = std::atan2(rv.y(), rv.x()); + if (m_polygon_circumscribed) { + R = d / std::cos(M_PI / double(sides)); + a0 = std::atan2(rv.y(), rv.x()) - M_PI / double(sides); + } + std::vector verts; verts.reserve(sides); + for (int i = 0; i < sides; ++i) { + const double a = a0 + 2.0 * M_PI * double(i) / double(sides); + verts.push_back(Vec2d(center.x() + R * std::cos(a), center.y() + R * std::sin(a))); + } + for (int i = 0; i < sides; ++i) { + SketchEntity e; e.type = SketchEntity::Type::Line; + e.p0 = verts[i]; e.p1 = verts[(i + 1) % sides]; + e.construction = m_construction; + out.push_back(e); + } + return out; +} + +// Derive (a, b, phi) of an ellipse from the 3 defining clicks. First axis click = +// major (a, phi); the minor point's perpendicular distance to the major axis = b, +// clamped to a so OCCT's a >= b holds. +static void ellipse_axes(const Vec2d& center, const Vec2d& major_end, const Vec2d& minor_pt, + double& a, double& b, double& phi) +{ + const Vec2d maj = major_end - center; + a = std::max(maj.norm(), 1e-6); + phi = std::atan2(maj.y(), maj.x()); + const Vec2d n(-std::sin(phi), std::cos(phi)); // minor-axis direction + b = std::min(std::abs((minor_pt - center).dot(n)), a); +} + +// Parametric angle on an ellipse of the point nearest `q` (q projected onto the frame). +static double ellipse_param_of(const Vec2d& center, double a, double b, double phi, const Vec2d& q) +{ + const Vec2d d = q - center; + const double cu = std::cos(phi), su = std::sin(phi); + const double u = d.x() * cu + d.y() * su; // along major + const double v = -d.x() * su + d.y() * cu; // along minor + return std::atan2(v / std::max(b, 1e-9), u / std::max(a, 1e-9)); +} + +std::vector DesignSketchTool::make_ellipse(const Vec2d& center, const Vec2d& major_end, + const Vec2d& minor_pt) const +{ + double a, b, phi; + ellipse_axes(center, major_end, minor_pt, a, b, phi); + if (b < 1e-6) return {}; + SketchEntity e; + e.type = SketchEntity::Type::Ellipse; + e.center = center; e.p0 = center; + e.radius = a; e.rminor = b; e.rotation = phi; + e.start_angle = 0.0; e.end_angle = 2.0 * M_PI; + e.construction = m_construction; + return { e }; +} + +std::vector DesignSketchTool::make_ellipse_arc(const Vec2d& center, const Vec2d& major_end, + const Vec2d& minor_pt, const Vec2d& start_pt, + const Vec2d& end_pt) const +{ + double a, b, phi; + ellipse_axes(center, major_end, minor_pt, a, b, phi); + if (b < 1e-6) return {}; + double t0 = ellipse_param_of(center, a, b, phi, start_pt); + double t1 = ellipse_param_of(center, a, b, phi, end_pt); + // CCW sweep from t0 to t1. + while (t1 <= t0) t1 += 2.0 * M_PI; + SketchEntity e; + e.type = SketchEntity::Type::EllipseArc; + e.center = center; + e.radius = a; e.rminor = b; e.rotation = phi; + e.start_angle = t0; e.end_angle = t1; + e.p0 = ellipse_point(center, a, b, phi, t0); + e.p1 = ellipse_point(center, a, b, phi, t1); + e.construction = m_construction; + return { e }; +} + +std::vector DesignSketchTool::make_bspline(const std::vector& ctrl) const +{ + if (ctrl.size() < 2) return {}; + SketchEntity e; + e.type = SketchEntity::Type::BSpline; + e.ctrl = ctrl; + e.p0 = ctrl.front(); + e.p1 = ctrl.back(); + e.construction = m_construction; + return { e }; +} + +std::vector DesignSketchTool::entity_polyline(const SketchEntity& e, bool& closed) const +{ + closed = false; + switch (e.type) { + case SketchEntity::Type::Line: + return { e.p0, e.p1 }; + case SketchEntity::Type::Circle: + closed = true; + return circle_polygon(e.center, e.radius); + case SketchEntity::Type::Arc: { + const int n = 24; + std::vector pts; pts.reserve(n + 1); + for (int i = 0; i <= n; ++i) { + const double a = e.start_angle + (e.end_angle - e.start_angle) * double(i) / double(n); + pts.push_back(Vec2d(e.center.x() + e.radius * std::cos(a), + e.center.y() + e.radius * std::sin(a))); + } + return pts; + } + case SketchEntity::Type::Ellipse: + closed = true; + return ellipse_polyline(e.center, e.radius, e.rminor, e.rotation, 0.0, 2.0 * M_PI); + case SketchEntity::Type::EllipseArc: + return ellipse_polyline(e.center, e.radius, e.rminor, e.rotation, e.start_angle, e.end_angle); + case SketchEntity::Type::BSpline: + return bspline_polyline(e.ctrl); + case SketchEntity::Type::Point: + return { e.p0 }; + } + return {}; +} + +std::vector> DesignSketchTool::closed_regions() const +{ + return closed_regions(m_entities); +} + +std::vector> DesignSketchTool::closed_regions(const std::vector& ents) const +{ + std::vector> out; + for (RegionLoop& r : region_loops(ents)) out.push_back(std::move(r.poly)); + return out; +} + +std::vector> +DesignSketchTool::region_entity_indices(const std::vector& ents) const +{ + std::vector> out; + for (RegionLoop& r : region_loops(ents)) out.push_back(std::move(r.ents)); + return out; +} + +std::vector DesignSketchTool::selected_loop_entities() const +{ + if (m_display_pick < 0 || m_display_pick_region < 0) return {}; + for (const DisplaySketch& d : m_display_sketches) { + if (d.feature != m_display_pick) continue; + const std::vector loops = region_loops(d.entities); + if (m_display_pick_region >= int(loops.size())) return {}; + std::vector out; + for (int ei : loops[m_display_pick_region].ents) + if (ei >= 0 && ei < int(d.entities.size())) out.push_back(d.entities[ei]); + return out; + } + return {}; +} + +// ---- Solid topology selection (whole -> face -> edge cycle) ---- + +void DesignSketchTool::set_solid_pick(const std::vector* bodies, const TriangleMesh* mesh, + const std::vector* tri_face, const std::vector* tri_body, + const std::vector* visible, + const std::vector* xform) +{ + // Treat no bodies or an empty mesh as "no solid" so has_display()/picking stay off. + if (bodies == nullptr || bodies->empty() || mesh == nullptr || mesh->its.indices.empty()) { + m_solid_bodies = nullptr; m_solid_mesh = nullptr; m_solid_tri_face = nullptr; m_solid_tri_body = nullptr; + m_solid_visible = nullptr; m_solid_xform = nullptr; + } else { + m_solid_bodies = bodies; m_solid_mesh = mesh; m_solid_tri_face = tri_face; m_solid_tri_body = tri_body; + m_solid_visible = visible; m_solid_xform = xform; + } + clear_solid_selection(); +} + +// Map a point sampled from the (untransformed) OCCT body shape through the body's display +// transform, so edge picking/highlight track a moved body. The pick MESH is already +// transformed by the host; only OCCT-sampled edges need this. +Vec3d DesignSketchTool::body_xform_pt(int body, const Vec3d& p) const +{ + if (m_solid_xform != nullptr && body >= 0 && body < int(m_solid_xform->size())) + return (*m_solid_xform)[body] * p; + return p; +} + +// A body is pickable unless an explicit visibility vector marks it hidden. +bool DesignSketchTool::body_pickable(int b) const +{ + if (b < 0) return false; + if (m_solid_visible == nullptr || b >= int(m_solid_visible->size())) return true; + return (*m_solid_visible)[b]; +} + +void DesignSketchTool::clear_solid_selection() +{ + m_solid_sel = SolidSel::None; + m_sel_body = m_sel_face = m_sel_edge = -1; + m_sel_edge_pts.clear(); +} + +void DesignSketchTool::select_body(int body) +{ + // Hidden bodies aren't highlighted (the tint overlay would otherwise draw over a + // body whose GLVolume is off, leaving a ghost after a hide). + if (m_solid_bodies == nullptr || body < 0 || body >= int(m_solid_bodies->size()) + || !body_pickable(body)) { + clear_solid_selection(); + return; + } + m_sel_body = body; + m_sel_face = m_sel_edge = -1; + m_sel_edge_pts.clear(); + m_solid_sel = SolidSel::Whole; // render_solid_highlight tints just this body +} + +// LeftDown on the solid cycles whole->face->edge. Returns true if the click hit the solid +// (consumed); false otherwise so the caller can try committed-sketch loop picking. +bool DesignSketchTool::handle_solid_click(GLCanvas3D& canvas, const wxMouseEvent& evt) +{ + if (m_solid_bodies == nullptr || m_solid_mesh == nullptr) return false; + const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + const Vec3d ro = r.a, rd = r.b - r.a; + + // 1) nearest solid face under the cursor (ray vs display-mesh triangles). Resolve WHICH + // body and which face-within-that-body via the per-triangle (tri_body, tri_face) tags. + const indexed_triangle_set& its = m_solid_mesh->its; + int best_face = -1, best_body = -1; double best_t = 1e30; + for (size_t i = 0; i < its.indices.size(); ++i) { + const auto& idx = its.indices[i]; + const Vec3d v0 = its.vertices[idx(0)].cast(); + const Vec3d v1 = its.vertices[idx(1)].cast(); + const Vec3d v2 = its.vertices[idx(2)].cast(); + double t; + if (ray_triangle(ro, rd, v0, v1, v2, t) && t < best_t) { + const int cand_body = (m_solid_tri_body && i < m_solid_tri_body->size()) ? (*m_solid_tri_body)[i] : -1; + if (!body_pickable(cand_body)) continue; // hidden bodies don't catch clicks + best_t = t; + best_face = (m_solid_tri_face && i < m_solid_tri_face->size()) ? (*m_solid_tri_face)[i] : -1; + best_body = cand_body; + } + } + if (best_face < 0 || best_body < 0 || best_body >= int(m_solid_bodies->size())) + return false; // missed the solid + + if (best_body != m_sel_body || best_face != m_sel_face) { + // First click on a (new) body/face selects the WHOLE solid; refine on repeat clicks. + m_sel_body = best_body; m_sel_face = best_face; m_sel_edge = -1; m_sel_edge_pts.clear(); + m_solid_sel = SolidSel::Whole; + } else if (m_solid_sel == SolidSel::Whole) { + m_solid_sel = SolidSel::Face; + } else if (m_solid_sel == SolidSel::Face) { + // Advance to the face's edge nearest the click (deterministic cycle step). + const TopoDS_Shape& bshape = (*m_solid_bodies)[m_sel_body].shape; + const TopoDS_Face face = GeometryEngine::face_by_index(bshape, m_sel_face); + int eid = -1; double best_ed = 1e30; std::vector best_pts; TopoDS_Edge best_edge; + if (!face.IsNull()) { + const std::vector edges = GeometryEngine::edges_of_face(face); + for (int k = 0; k < int(edges.size()); ++k) { + std::vector pts = GeometryEngine::sample_edge_world(edges[k]); + for (Vec3d& q : pts) q = body_xform_pt(m_sel_body, q); // follow a moved body + double d = 1e30; + for (size_t s = 1; s < pts.size(); ++s) + d = std::min(d, ray_segment_dist3(ro, rd, pts[s - 1], pts[s])); + if (d < best_ed) { best_ed = d; eid = k; best_pts = pts; best_edge = edges[k]; } + } + } + if (eid >= 0) { + // Promote the face-relative pick to a STABLE GLOBAL edge id so dress-up ops + // (fillet/chamfer) can target this exact edge across recomputes. + m_sel_edge = GeometryEngine::edge_index_of(bshape, best_edge); + m_sel_edge_pts = std::move(best_pts); + m_solid_sel = SolidSel::Edge; + } else { m_solid_sel = SolidSel::Whole; m_sel_edge = -1; m_sel_edge_pts.clear(); } + } else { // Edge -> back to Whole + m_solid_sel = SolidSel::Whole; m_sel_edge = -1; m_sel_edge_pts.clear(); + } + if (on_solid_selection_changed) + on_solid_selection_changed(int(m_solid_sel), m_sel_body, m_sel_face, m_sel_edge); + return true; +} + +// Cyan overlay for the picked face (translucent, depth-tested + offset) or edge (opaque +// ribbon billboarded to the camera, depth off so it reads on top). Whole-solid tint is the +// panel's job (set_body_highlight). Called from render() while no sketch session is active. +void DesignSketchTool::render_solid_highlight() +{ + using EPT = GLModel::Geometry::EPrimitiveType; + using EVL = GLModel::Geometry::EVertexLayout; + const ColorRGBA cyan(0.20f, 0.85f, 1.0f, 1.0f); + + // Whole tints the picked BODY (all its triangles, lighter alpha); Face tints just the + // picked face on that body. Both filter by m_sel_body so other bodies stay untinted. + if ((m_solid_sel == SolidSel::Face || m_solid_sel == SolidSel::Whole) + && m_solid_mesh != nullptr && m_solid_tri_body != nullptr && m_sel_body >= 0) { + const bool face_only = (m_solid_sel == SolidSel::Face); + const indexed_triangle_set& its = m_solid_mesh->its; + GLModel::Geometry g; g.format = { EPT::Triangles, EVL::P3 }; + unsigned int base = 0; + for (size_t i = 0; i < its.indices.size(); ++i) { + if (i >= m_solid_tri_body->size() || (*m_solid_tri_body)[i] != m_sel_body) continue; + if (face_only && (m_solid_tri_face == nullptr || i >= m_solid_tri_face->size() + || (*m_solid_tri_face)[i] != m_sel_face)) continue; + const auto& idx = its.indices[i]; + for (int j = 0; j < 3; ++j) g.add_vertex(its.vertices[idx(j)]); + g.add_triangle(base, base + 1, base + 2); base += 3; + } + if (base > 0) { + glsafe(::glEnable(GL_DEPTH_TEST)); + glsafe(::glEnable(GL_POLYGON_OFFSET_FILL)); + glsafe(::glPolygonOffset(-2.0f, -2.0f)); + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + m_solid_face_model.reset(); + m_solid_face_model.init_from(std::move(g)); + m_solid_face_model.set_color(ColorRGBA(0.20f, 0.85f, 1.0f, face_only ? 0.40f : 0.22f)); + m_solid_face_model.render(); + glsafe(::glDisable(GL_BLEND)); + glsafe(::glDisable(GL_POLYGON_OFFSET_FILL)); + glsafe(::glDisable(GL_DEPTH_TEST)); + } + } else if (m_solid_sel == SolidSel::Edge && m_sel_edge_pts.size() >= 2) { + const Camera& cam = wxGetApp().plater()->get_camera(); + const Vec3d vd = cam.get_dir_forward(); + const double hw = 2.0 / std::max(cam.get_zoom(), 1e-6); // ~2 px ribbon half-width + GLModel::Geometry g; g.format = { EPT::Triangles, EVL::P3 }; + unsigned int base = 0; + for (size_t s = 1; s < m_sel_edge_pts.size(); ++s) { + const Vec3d a = m_sel_edge_pts[s - 1], b = m_sel_edge_pts[s]; + Vec3d dir = b - a; if (dir.norm() < 1e-9) continue; dir.normalize(); + Vec3d off = dir.cross(vd); + if (off.norm() < 1e-9) off = dir.cross(cam.get_dir_up()); + if (off.norm() < 1e-9) continue; + off.normalize(); off *= hw; + g.add_vertex((Vec3f)(a + off).cast()); + g.add_vertex((Vec3f)(b + off).cast()); + g.add_vertex((Vec3f)(b - off).cast()); + g.add_vertex((Vec3f)(a - off).cast()); + g.add_triangle(base, base + 1, base + 2); + g.add_triangle(base, base + 2, base + 3); base += 4; + } + if (base > 0) { + glsafe(::glDisable(GL_DEPTH_TEST)); + m_solid_edge_model.reset(); + m_solid_edge_model.init_from(std::move(g)); + m_solid_edge_model.set_color(cyan); + m_solid_edge_model.render(); + } + } +} + +// Datum/reference planes (Plane feature) have no solid; draw each as a translucent indigo +// rectangle + border so it is visible in the viewport (Onshape-style finite plane). World +// space, depth-test off so it reads over the bed; indigo to stay distinct from the cyan +// solid-selection tint, orange sketches and amber feature ghosts. +void DesignSketchTool::render_datum_planes() +{ + if (m_datum_planes.empty()) return; + using EPT = GLModel::Geometry::EPrimitiveType; + using EVL = GLModel::Geometry::EVertexLayout; + const double H = 40.0; // half-extent of the drawn rectangle (mm) + const Camera& cam = wxGetApp().plater()->get_camera(); + const Vec3d vd = cam.get_dir_forward(); + const double hw = 1.5 / std::max(cam.get_zoom(), 1e-6); // ~1.5 px border ribbon + + GLModel::Geometry fill; fill.format = { EPT::Triangles, EVL::P3 }; + GLModel::Geometry border; border.format = { EPT::Triangles, EVL::P3 }; + unsigned int fb = 0, bb = 0; + for (const SketchPlane& p : m_datum_planes) { + const Vec3d c[4] = { p.to_world(Vec2d(-H, -H)), p.to_world(Vec2d(H, -H)), + p.to_world(Vec2d(H, H)), p.to_world(Vec2d(-H, H)) }; + fill.add_vertex((Vec3f)c[0].cast()); fill.add_vertex((Vec3f)c[1].cast()); + fill.add_vertex((Vec3f)c[2].cast()); fill.add_triangle(fb, fb + 1, fb + 2); fb += 3; + fill.add_vertex((Vec3f)c[0].cast()); fill.add_vertex((Vec3f)c[2].cast()); + fill.add_vertex((Vec3f)c[3].cast()); fill.add_triangle(fb, fb + 1, fb + 2); fb += 3; + for (int s = 0; s < 4; ++s) { + const Vec3d a = c[s], b = c[(s + 1) & 3]; + Vec3d dir = b - a; if (dir.norm() < 1e-9) continue; dir.normalize(); + Vec3d off = dir.cross(vd); + if (off.norm() < 1e-9) off = dir.cross(cam.get_dir_up()); + if (off.norm() < 1e-9) continue; + off.normalize(); off *= hw; + border.add_vertex((Vec3f)(a + off).cast()); + border.add_vertex((Vec3f)(b + off).cast()); + border.add_vertex((Vec3f)(b - off).cast()); + border.add_vertex((Vec3f)(a - off).cast()); + border.add_triangle(bb, bb + 1, bb + 2); + border.add_triangle(bb, bb + 2, bb + 3); bb += 4; + } + } + glsafe(::glDisable(GL_DEPTH_TEST)); + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + if (fb > 0) { + GLModel fm; fm.init_from(std::move(fill)); + fm.set_color(ColorRGBA(0.62f, 0.52f, 0.95f, 0.10f)); + fm.render(); + } + if (bb > 0) { + GLModel bm; bm.init_from(std::move(border)); + bm.set_color(ColorRGBA(0.70f, 0.60f, 1.0f, 0.85f)); + bm.render(); + } + glsafe(::glDisable(GL_BLEND)); +} + +// ---- Visual Extrude gizmo (C5b) ------------------------------------------------------- +void DesignSketchTool::set_extrude_gizmo(const SketchPlane& plane, const Vec2d& centroid, + double depth, double depth2, bool two_sided, bool flip) +{ + m_ex_active = true; + m_ex_plane = plane; + m_ex_centroid = centroid; + m_ex_depth = std::max(0.0, depth); + m_ex_depth2 = std::max(0.0, depth2); + m_ex_two_sided = two_sided; + m_ex_flip = flip; +} + +void DesignSketchTool::clear_extrude_gizmo() +{ + m_ex_active = false; + m_ex_drag = -1; +} + +// Camera-billboarded depth arrow(s) along the profile normal, drawn in WORLD via a billboard +// SketchPlane at the centroid (draw_strokes/draw_text lift 2D coords through m_plane.to_world, +// so swapping m_plane to a screen-facing frame renders a flat, screen-aligned arrow + label). +void DesignSketchTool::render_extrude_gizmo() +{ + if (!m_ex_active) return; + const Camera& cam = wxGetApp().plater()->get_camera(); + const Vec3d right = cam.get_dir_right().normalized(); + const Vec3d up = cam.get_dir_up().normalized(); + const Vec3d fwd = cam.get_dir_forward().normalized(); + const Vec3d base = m_ex_plane.to_world(m_ex_centroid); + const Vec3d ndir = (m_ex_flip ? -1.0 : 1.0) * m_ex_plane.normal.normalized(); + const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6); + const double th = std::max(15.0 * upp, 1e-4); + + // Express everything in the billboard frame (origin=base) so it always faces the camera. + const SketchPlane saved = m_plane; + SketchPlane bb; bb.origin = base; bb.x_axis = right; bb.y_axis = up; bb.normal = fwd; + m_plane = bb; + const ColorRGBA arrowc(1.0f, 0.62f, 0.16f, 1.0f); // CAD amber + + auto draw_arrow = [&](double depth, bool flip_side) { + if (depth <= 1e-6) return; + const Vec3d dirw = (flip_side ? -1.0 : 1.0) * ndir; // world arrow direction + const Vec3d tipw = base + dirw * depth; + const Vec2d tip2((tipw - base).dot(right), (tipw - base).dot(up)); + if (tip2.norm() < 1e-6) return; // axis ~ parallel to view: no arrow + const Vec2d u = tip2.normalized(); + const Vec2d nrm(-u.y(), u.x()); + std::vector> segs; + segs.emplace_back(Vec2d(0, 0), tip2); + const double as = std::max(tip2.norm() * 0.18, th * 0.9); // arrowhead size + const Vec2d back = tip2 - u * as; + segs.emplace_back(tip2, back + nrm * (as * 0.5)); + segs.emplace_back(tip2, back - nrm * (as * 0.5)); + draw_strokes(m_ex_arrow_model, segs, std::max(0.7 * upp, 1e-4), arrowc); + DimAnnot a; a.kind = DimType::Distance; a.value = depth; + draw_text(m_line_model, dim_text(a), tip2 + u * (th * 1.4), th, arrowc); + }; + + glsafe(::glDisable(GL_DEPTH_TEST)); + draw_arrow(m_ex_depth, false); + if (m_ex_two_sided) draw_arrow(m_ex_depth2, true); + m_plane = saved; +} + +// Ray vs the arrow segment(s) in world space; `which` = 0 primary, 1 second side. +bool DesignSketchTool::hit_test_extrude_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int& which) const +{ + if (!m_ex_active) return false; + const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + const Vec3d ro = r.a, rd = r.b - r.a; + const Camera& cam = wxGetApp().plater()->get_camera(); + const double tol = 7.0 / std::max(cam.get_zoom(), 1e-6); // ~7 px in world units + const Vec3d ndir = (m_ex_flip ? -1.0 : 1.0) * m_ex_plane.normal.normalized(); + const Vec3d base = m_ex_plane.to_world(m_ex_centroid); + double dA = 1e30, dB = 1e30; + if (m_ex_depth > 1e-6) dA = ray_segment_dist3(ro, rd, base, base + ndir * m_ex_depth); + if (m_ex_two_sided && m_ex_depth2 > 1e-6) + dB = ray_segment_dist3(ro, rd, base, base - ndir * m_ex_depth2); + if (dA <= tol && dA <= dB) { which = 0; return true; } + if (dB <= tol) { which = 1; return true; } + return false; +} + +// Drag the arrow handle: closest point on the world arrow axis to the mouse ray (skew-line +// closest-point), projected onto the axis direction -> signed depth (clamped positive). +void DesignSketchTool::drag_extrude_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int which) +{ + const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + const Vec3d ro = r.a, rd = r.b - r.a; + const Vec3d nd = (m_ex_flip ? -1.0 : 1.0) * m_ex_plane.normal.normalized(); + const Vec3d e = (which == 1 ? -1.0 : 1.0) * nd; // axis dir for this arrow + const Vec3d base = m_ex_plane.to_world(m_ex_centroid); + const Vec3d w0 = base - ro; + const double a = e.dot(e), b = e.dot(rd), c = rd.dot(rd), dd = e.dot(w0), ee = rd.dot(w0); + const double denom = a * c - b * b; + if (std::abs(denom) < 1e-7) return; // camera ∥ axis: leave depth as-is + const double depth = std::max(0.01, (b * ee - c * dd) / denom); + if (which == 1) m_ex_depth2 = depth; else m_ex_depth = depth; + if (on_extrude_depth_changed) on_extrude_depth_changed(depth, which == 1); +} + +void DesignSketchTool::open_extrude_editor(int which) +{ + if (!on_inline_edit) return; + const double cur = (which == 1) ? m_ex_depth2 : m_ex_depth; + const wxPoint px(m_last_mouse_x, m_last_mouse_y); + on_inline_edit(px, cur, + [this, which](double v) { + const double d = std::max(0.01, v); + if (which == 1) m_ex_depth2 = d; else m_ex_depth = d; + if (on_extrude_depth_changed) on_extrude_depth_changed(d, which == 1); + }, + []() {}); +} + +// ---- Move-body gizmo (M5) ------------------------------------------------------------- +void DesignSketchTool::set_move_gizmo(int body, const Vec3d& pivot, const Transform3d& base_xform) +{ + m_mv_active = true; + m_mv_body = body; + m_mv_base = pivot; // body's world centroid at Move-open = rotation pivot + m_mv_base_xform = base_xform; // pose the deltas compose onto + m_mv_offset = Vec3d::Zero(); + m_mv_rot = Eigen::Matrix3d::Identity(); + m_mv_drag = -1; +} + +void DesignSketchTool::clear_move_gizmo() +{ + m_mv_active = false; + m_mv_drag = -1; + m_mv_body = -1; + m_mv_offset = Vec3d::Zero(); + m_mv_rot = Eigen::Matrix3d::Identity(); +} + +// Final body transform = translate(delta) then rotate(delta, about pivot) on the open pose. +Transform3d DesignSketchTool::compose_move_xform() const +{ + const Vec3d p = m_mv_base; + Transform3d R = Transform3d::Identity(); + R.linear() = m_mv_rot; + const Transform3d rot_about_pivot = + Transform3d(Eigen::Translation3d(p)) * R * Transform3d(Eigen::Translation3d(-p)); + return Transform3d(Eigen::Translation3d(m_mv_offset)) * rot_about_pivot * m_mv_base_xform; +} + +// World axis `e` of ring `axis` plus an in-plane orthonormal basis (u,v). +void DesignSketchTool::ring_basis(int axis, Vec3d& e, Vec3d& u, Vec3d& v) const +{ + switch (axis) { + case 0: e = Vec3d::UnitX(); u = Vec3d::UnitY(); v = Vec3d::UnitZ(); break; + case 1: e = Vec3d::UnitY(); u = Vec3d::UnitZ(); v = Vec3d::UnitX(); break; + default:e = Vec3d::UnitZ(); u = Vec3d::UnitX(); v = Vec3d::UnitY(); break; + } +} + +// Three world-axis arrows (X red / Y green / Z blue) from the body centroid + current +// offset, billboarded into a screen-facing frame like the extrude depth arrow. +void DesignSketchTool::render_move_gizmo() +{ + if (!m_mv_active) return; + const Camera& cam = wxGetApp().plater()->get_camera(); + const Vec3d right = cam.get_dir_right().normalized(); + const Vec3d up = cam.get_dir_up().normalized(); + const Vec3d fwd = cam.get_dir_forward().normalized(); + const Vec3d anchor = m_mv_base + m_mv_offset; + const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6); + const double th = std::max(15.0 * upp, 1e-4); + const double L = 70.0 * upp; // fixed screen-size arrow length + + const SketchPlane saved = m_plane; + SketchPlane bb; bb.origin = anchor; bb.x_axis = right; bb.y_axis = up; bb.normal = fwd; + m_plane = bb; + + const Vec3d axes[3] = { Vec3d::UnitX(), Vec3d::UnitY(), Vec3d::UnitZ() }; + const ColorRGBA cols[3] = { ColorRGBA(0.92f, 0.28f, 0.28f, 1.0f), // X red + ColorRGBA(0.30f, 0.80f, 0.34f, 1.0f), // Y green + ColorRGBA(0.32f, 0.55f, 0.95f, 1.0f) }; // Z blue + + glsafe(::glDisable(GL_DEPTH_TEST)); + for (int a = 0; a < 3; ++a) { + const Vec3d tipw = anchor + axes[a] * L; + const Vec2d tip2((tipw - anchor).dot(right), (tipw - anchor).dot(up)); + if (tip2.norm() < 1e-6) continue; // axis ~parallel to view: skip + const Vec2d u = tip2.normalized(); + const Vec2d nrm(-u.y(), u.x()); + std::vector> segs; + segs.emplace_back(Vec2d(0, 0), tip2); + const double as = std::max(tip2.norm() * 0.20, th * 0.9); + const Vec2d back = tip2 - u * as; + segs.emplace_back(tip2, back + nrm * (as * 0.5)); + segs.emplace_back(tip2, back - nrm * (as * 0.5)); + draw_strokes(m_mv_arrow_model, segs, std::max(0.7 * upp, 1e-4), cols[a]); + const double off = m_mv_offset[a]; + if (std::abs(off) > 1e-4) { + DimAnnot da; da.kind = DimType::Distance; da.value = std::abs(off); + draw_text(m_line_model, dim_text(da), tip2 + u * (th * 1.4), th, cols[a]); + } + } + m_plane = saved; + + // Three world-axis rotation rings (X/Y/Z), each a circle in the plane perpendicular to + // its axis through the gizmo anchor — drag a ring to rotate the body about that axis. + const double R = 58.0 * upp; + for (int a = 0; a < 3; ++a) { + Vec3d e, u, v; ring_basis(a, e, u, v); + SketchPlane rp; rp.origin = anchor; rp.x_axis = u; rp.y_axis = v; rp.normal = e; + m_plane = rp; + std::vector> segs; + const int N = 48; + Vec2d prev(R, 0.0); + for (int i = 1; i <= N; ++i) { + const double t = 2.0 * M_PI * double(i) / double(N); + const Vec2d cur(R * std::cos(t), R * std::sin(t)); + segs.emplace_back(prev, cur); prev = cur; + } + draw_strokes(m_mv_arrow_model, segs, std::max(0.55 * upp, 1e-4), cols[a]); + } + m_plane = saved; +} + +// Ray vs each world-axis arrow segment; nearest within ~7 px wins. +bool DesignSketchTool::hit_test_move_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int& axis) const +{ + if (!m_mv_active) return false; + const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + const Vec3d ro = r.a, rd = r.b - r.a; + const Camera& cam = wxGetApp().plater()->get_camera(); + const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6); + const double L = 70.0 * upp; + const Vec3d anchor = m_mv_base + m_mv_offset; + const Vec3d axes[3] = { Vec3d::UnitX(), Vec3d::UnitY(), Vec3d::UnitZ() }; + int best = -1; double bestd = 7.0 * upp; // ~7 px tolerance + for (int a = 0; a < 3; ++a) { + const double d = ray_segment_dist3(ro, rd, anchor, anchor + axes[a] * L); + if (d <= bestd) { bestd = d; best = a; } + } + if (best < 0) return false; + axis = best; return true; +} + +// Skew-line closest point of the mouse ray to the axis line through the ORIGINAL centroid +// -> signed offset along that axis (no clamp; a body can move either way). +void DesignSketchTool::drag_move_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis) +{ + const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + const Vec3d ro = r.a, rd = r.b - r.a; + const Vec3d axes[3] = { Vec3d::UnitX(), Vec3d::UnitY(), Vec3d::UnitZ() }; + const Vec3d e = axes[axis]; + const Vec3d w0 = m_mv_base - ro; + const double a = e.dot(e), b = e.dot(rd), c = rd.dot(rd), dd = e.dot(w0), ee = rd.dot(w0); + const double denom = a * c - b * b; + if (std::abs(denom) < 1e-7) return; // camera ∥ axis: leave offset as-is + m_mv_offset[axis] = (b * ee - c * dd) / denom; + if (on_body_move_changed) on_body_move_changed(m_mv_body, compose_move_xform()); +} + +void DesignSketchTool::open_move_editor(int axis) +{ + if (!on_inline_edit) return; + const wxPoint px(m_last_mouse_x, m_last_mouse_y); + on_inline_edit(px, m_mv_offset[axis], + [this, axis](double v) { + m_mv_offset[axis] = v; + if (on_body_move_changed) on_body_move_changed(m_mv_body, compose_move_xform()); + }, + []() {}); +} + +// Ray vs each rotation ring (sampled polyline); nearest within ~7 px wins -> axis 0/1/2. +bool DesignSketchTool::hit_test_move_arc(GLCanvas3D& canvas, const wxMouseEvent& evt, int& axis) const +{ + if (!m_mv_active) return false; + const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + const Vec3d ro = r.a, rd = r.b - r.a; + const Camera& cam = wxGetApp().plater()->get_camera(); + const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6); + const double R = 58.0 * upp; + const Vec3d anchor = m_mv_base + m_mv_offset; + int best = -1; double bestd = 7.0 * upp; + const int N = 48; + for (int a = 0; a < 3; ++a) { + Vec3d e, u, v; ring_basis(a, e, u, v); + Vec3d prev = anchor + R * u; + for (int i = 1; i <= N; ++i) { + const double t = 2.0 * M_PI * double(i) / double(N); + const Vec3d cur = anchor + R * (std::cos(t) * u + std::sin(t) * v); + const double d = ray_segment_dist3(ro, rd, prev, cur); + if (d <= bestd) { bestd = d; best = a; } + prev = cur; + } + } + if (best < 0) return false; + axis = best; return true; +} + +// Intersect the mouse ray with ring `axis`'s plane through the anchor -> in-plane angle. +bool DesignSketchTool::arc_mouse_angle(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis, double& ang) const +{ + const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + const Vec3d ro = r.a, rd = r.b - r.a; + Vec3d e, u, v; ring_basis(axis, e, u, v); + const Vec3d p = m_mv_base + m_mv_offset; + const double denom = rd.dot(e); + if (std::abs(denom) < 1e-9) return false; // ray ∥ ring plane + const Vec3d hit = ro + ((p - ro).dot(e) / denom) * rd; + const Vec3d d = hit - p; + ang = std::atan2(d.dot(v), d.dot(u)); + return true; +} + +// Drag a ring -> rotate the body about that world axis by (mouse angle - grab angle). +void DesignSketchTool::drag_move_arc(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis) +{ + double ang; + if (!arc_mouse_angle(canvas, evt, axis, ang)) return; + Vec3d e, u, v; ring_basis(axis, e, u, v); + const double delta = ang - m_mv_arc_a0; + m_mv_rot = Eigen::AngleAxisd(delta, e).toRotationMatrix() * m_mv_rot_start; + if (on_body_move_changed) on_body_move_changed(m_mv_body, compose_move_xform()); +} + +// ---- Fillet/Chamfer radius gizmo ------------------------------------------------------ +// Anchor a single radius arrow at the picked edge midpoint (m_sel_edge_pts is already in world +// space, body-transformed), perpendicular to the edge and pointing away from the body centroid — +// the natural outward direction a fillet/chamfer grows. +bool DesignSketchTool::set_fillet_gizmo(const Vec3d& body_centroid, double radius) +{ + if (m_sel_edge_pts.size() < 2) { m_fl_active = false; return false; } + const size_t n = m_sel_edge_pts.size(); + // True geometric midpoint along the edge: a straight edge often samples to just its two + // endpoints, so the middle INDEX would land on an end. Walk the polyline to its half-length. + double total = 0.0; + for (size_t i = 1; i < n; ++i) total += (m_sel_edge_pts[i] - m_sel_edge_pts[i - 1]).norm(); + m_fl_anchor = m_sel_edge_pts[0]; + Vec3d t = m_sel_edge_pts[n - 1] - m_sel_edge_pts[0]; + const double half = 0.5 * total; + double acc = 0.0; + for (size_t i = 1; i < n; ++i) { + const Vec3d seg = m_sel_edge_pts[i] - m_sel_edge_pts[i - 1]; + const double L = seg.norm(); + if (acc + L >= half && L > 1e-12) { + m_fl_anchor = m_sel_edge_pts[i - 1] + seg * ((half - acc) / L); + t = seg; + break; + } + acc += L; + } + if (t.norm() < 1e-9) return false; + t.normalize(); + Vec3d r = m_fl_anchor - body_centroid; // radial offset from the body centre + r -= r.dot(t) * t; // strip the along-edge component + if (r.norm() < 1e-6) { // edge passes through the centroid + r = t.cross(Vec3d::UnitZ()); + if (r.norm() < 1e-6) r = t.cross(Vec3d::UnitX()); + } + m_fl_dir = r.normalized(); + m_fl_radius = std::max(0.01, radius); + if (!m_fl_active) m_fl_drag = false; // re-anchored every preview: preserve an in-progress drag + m_fl_active = true; + return true; +} + +void DesignSketchTool::clear_fillet_gizmo() +{ + m_fl_active = false; + m_fl_drag = false; +} + +// Single billboarded radius arrow from the edge midpoint along m_fl_dir; length = radius (world), +// floored to a grabbable screen size. Label shows the true radius (R-prefixed). +void DesignSketchTool::render_fillet_gizmo() +{ + if (!m_fl_active) return; + const Camera& cam = wxGetApp().plater()->get_camera(); + const Vec3d right = cam.get_dir_right().normalized(); + const Vec3d up = cam.get_dir_up().normalized(); + const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6); + const double th = std::max(15.0 * upp, 1e-4); + const double L = std::max(m_fl_radius, 40.0 * upp); // WYSIWYG, floored to a comfortable handle + const Vec3d tipw = m_fl_anchor + m_fl_dir * L; + + const SketchPlane saved = m_plane; + SketchPlane bb; bb.origin = m_fl_anchor; bb.x_axis = right; bb.y_axis = up; bb.normal = cam.get_dir_forward().normalized(); + m_plane = bb; + const ColorRGBA arrowc(1.0f, 0.62f, 0.16f, 1.0f); // CAD amber + + const Vec2d tip2((tipw - m_fl_anchor).dot(right), (tipw - m_fl_anchor).dot(up)); + if (tip2.norm() > 1e-6) { + const Vec2d u = tip2.normalized(); + const Vec2d nrm(-u.y(), u.x()); + std::vector> segs; + segs.emplace_back(Vec2d(0, 0), tip2); + const double as = std::max(tip2.norm() * 0.20, th * 0.9); + const Vec2d back = tip2 - u * as; + segs.emplace_back(tip2, back + nrm * (as * 0.5)); + segs.emplace_back(tip2, back - nrm * (as * 0.5)); + glsafe(::glDisable(GL_DEPTH_TEST)); + draw_strokes(m_fl_arrow_model, segs, std::max(0.7 * upp, 1e-4), arrowc); + DimAnnot da; da.kind = DimType::Radius; da.value = m_fl_radius; + draw_text(m_line_model, dim_text(da), tip2 + u * (th * 1.4), th, arrowc); + } + m_plane = saved; +} + +// Ray vs the radius arrow segment; ~12 px tolerance over the (floored) handle length. +bool DesignSketchTool::hit_test_fillet_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt) const +{ + if (!m_fl_active) return false; + const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + const Vec3d ro = r.a, rd = r.b - r.a; + const Camera& cam = wxGetApp().plater()->get_camera(); + const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6); + const double L = std::max(m_fl_radius, 40.0 * upp); + return ray_segment_dist3(ro, rd, m_fl_anchor, m_fl_anchor + m_fl_dir * L) <= 12.0 * upp; +} + +// Skew-line closest point of the mouse ray to the radius axis -> signed distance along m_fl_dir +// from the anchor. NaN when the camera is ~parallel to the axis (no meaningful projection). +double DesignSketchTool::fillet_axis_proj(GLCanvas3D& canvas, const wxMouseEvent& evt) const +{ + const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + const Vec3d ro = r.a, rd = r.b - r.a; + const Vec3d e = m_fl_dir; + const Vec3d w0 = m_fl_anchor - ro; + const double a = e.dot(e), b = e.dot(rd), c = rd.dot(rd), dd = e.dot(w0), ee = rd.dot(w0); + const double denom = a * c - b * b; + if (std::abs(denom) < 1e-7) return std::nan(""); + return (b * ee - c * dd) / denom; +} + +// Record the grab reference so the drag is RELATIVE (grab anywhere on the handle without the +// radius snapping to the grab point — important since the handle is floored to a min size). +void DesignSketchTool::start_fillet_drag(GLCanvas3D& canvas, const wxMouseEvent& evt) +{ + m_fl_drag = true; + m_fl_press_x = evt.GetX(); + m_fl_press_y = evt.GetY(); + m_fl_grab_radius = m_fl_radius; + const double p = fillet_axis_proj(canvas, evt); + m_fl_grab_proj = std::isnan(p) ? 0.0 : p; +} + +void DesignSketchTool::drag_fillet_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt) +{ + const double proj = fillet_axis_proj(canvas, evt); + if (std::isnan(proj)) return; // camera ∥ axis: leave radius as-is + m_fl_radius = std::max(0.01, m_fl_grab_radius + (proj - m_fl_grab_proj)); + if (on_fillet_radius_changed) on_fillet_radius_changed(m_fl_radius); +} + +void DesignSketchTool::open_fillet_editor() +{ + if (!on_inline_edit) return; + const wxPoint px(m_last_mouse_x, m_last_mouse_y); + on_inline_edit(px, m_fl_radius, + [this](double v) { + m_fl_radius = std::max(0.01, v); + if (on_fillet_radius_changed) on_fillet_radius_changed(m_fl_radius); + }, + []() {}); +} + +// ---- Hole gizmo ----------------------------------------------------------------------------- +// Positioned circular cut: footprint circle on the plane + a radial diameter arrow (plane u-axis) +// + a normal-axis depth arrow (drawn only when !through, matching the kernel's blind cut) + a +// draggable centre marker. The panel re-pushes this every preview, so (like the fillet gizmo) we +// must NOT reset an in-progress drag when already active. +void DesignSketchTool::set_hole_gizmo(const SketchPlane& plane, double x, double y, + double diameter, double depth, bool through) +{ + m_hl_plane = plane; + m_hl_x = x; + m_hl_y = y; + m_hl_diameter = std::max(0.01, diameter); + m_hl_depth = std::max(0.01, depth); + m_hl_through = through; + if (!m_hl_active) m_hl_drag = -1; // re-pushed every preview: preserve an in-progress drag + m_hl_active = true; +} + +void DesignSketchTool::clear_hole_gizmo() +{ + m_hl_active = false; + m_hl_drag = -1; +} + +void DesignSketchTool::set_hole_face_bounds(bool has, double umin, double umax, double vmin, double vmax) +{ + m_hl_has_bounds = has; + m_hl_umin = umin; m_hl_umax = umax; m_hl_vmin = vmin; m_hl_vmax = vmax; +} + +void DesignSketchTool::render_hole_gizmo() +{ + if (!m_hl_active) return; + const Camera& cam = wxGetApp().plater()->get_camera(); + const Vec3d right = cam.get_dir_right().normalized(); + const Vec3d up = cam.get_dir_up().normalized(); + const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6); + const double th = std::max(15.0 * upp, 1e-4); + + const Vec3d centre = m_hl_plane.to_world(Vec2d(m_hl_x, m_hl_y)); + const Vec3d nrm = m_hl_plane.normal.normalized(); + const Vec3d ddir = m_hl_plane.x_axis.normalized(); // diameter arrow runs along plane u + const double r = std::max(0.01, m_hl_diameter * 0.5); + + const SketchPlane saved = m_plane; + + // (1) Footprint circle drawn ON the plane (lifts plane u/v -> world through to_world). + { + m_plane = m_hl_plane; + std::vector> segs; + const int N = 48; + for (int i = 0; i < N; ++i) { + const double a0 = (2.0 * M_PI * i) / N, a1 = (2.0 * M_PI * (i + 1)) / N; + segs.emplace_back(Vec2d(m_hl_x + r * std::cos(a0), m_hl_y + r * std::sin(a0)), + Vec2d(m_hl_x + r * std::cos(a1), m_hl_y + r * std::sin(a1))); + } + glsafe(::glDisable(GL_DEPTH_TEST)); + draw_strokes(m_hl_stroke_model, segs, std::max(0.6 * upp, 1e-4), ColorRGBA(1.0f, 0.62f, 0.16f, 1.0f)); + m_plane = saved; + } + + // (1b) #2 Part B: construction-line dimensions positioning the hole from the face SIDES — a + // horizontal leg from the u-side (umin = one edge) and a vertical leg from the v-side (vmin = + // the adjacent edge) to the hole centre, each with an editable distance label (what users ask + // for: distance from the sides, not x/y from the centre). Falls back to the datum (0,0) when no + // face bounds are known (dropdown-plane hole). Drawn ON the plane, construction green. + { + m_plane = m_hl_plane; + const ColorRGBA con(0.55f, 0.85f, 0.55f, 1.0f); + const double o = 16.0 * upp; + const double ru = m_hl_has_bounds ? m_hl_umin : 0.0; // reference u-side (face edge / datum) + const double rv = m_hl_has_bounds ? m_hl_vmin : 0.0; // reference v-side (face edge / datum) + std::vector> segs; + segs.emplace_back(Vec2d(ru, m_hl_y), Vec2d(m_hl_x, m_hl_y)); // from the u-side to the hole + segs.emplace_back(Vec2d(m_hl_x, rv), Vec2d(m_hl_x, m_hl_y)); // from the v-side to the hole + glsafe(::glDisable(GL_DEPTH_TEST)); + draw_strokes(m_hl_stroke_model, segs, std::max(0.5 * upp, 1e-4), con); + DimAnnot dx; dx.kind = DimType::Distance; dx.value = m_hl_x - ru; // distance from the u-side + draw_text(m_line_model, dim_text(dx), Vec2d((ru + m_hl_x) * 0.5, m_hl_y - o), th, con); + DimAnnot dy; dy.kind = DimType::Distance; dy.value = m_hl_y - rv; // distance from the v-side + draw_text(m_line_model, dim_text(dy), Vec2d(m_hl_x + o, (rv + m_hl_y) * 0.5), th, con); + m_plane = saved; + } + + // (2) Billboarded handles (centre marker + diameter arrow + depth arrow), screen-facing frame + // at the centre so draw_strokes/draw_text read on top regardless of orientation. + SketchPlane bb; bb.origin = centre; bb.x_axis = right; bb.y_axis = up; bb.normal = cam.get_dir_forward().normalized(); + m_plane = bb; + const ColorRGBA amber(1.0f, 0.62f, 0.16f, 1.0f); + const ColorRGBA blue (0.30f, 0.55f, 1.0f, 1.0f); + + auto arrow_to = [&](const Vec3d& tipw, const ColorRGBA& col, const DimAnnot& da) { + const Vec2d tip2((tipw - centre).dot(right), (tipw - centre).dot(up)); + if (tip2.norm() <= 1e-6) return; + const Vec2d u = tip2.normalized(); + const Vec2d nrm2(-u.y(), u.x()); + std::vector> segs; + segs.emplace_back(Vec2d(0, 0), tip2); + const double as = std::max(tip2.norm() * 0.20, th * 0.9); + const Vec2d back = tip2 - u * as; + segs.emplace_back(tip2, back + nrm2 * (as * 0.5)); + segs.emplace_back(tip2, back - nrm2 * (as * 0.5)); + glsafe(::glDisable(GL_DEPTH_TEST)); + draw_strokes(m_hl_stroke_model, segs, std::max(0.7 * upp, 1e-4), col); + draw_text(m_line_model, dim_text(da), tip2 + u * (th * 1.4), th, col); + }; + + // Diameter arrow: WYSIWYG radius, floored to a grabbable handle; label shows Ø (full diameter). + { + const double L = std::max(r, 40.0 * upp); + DimAnnot da; da.kind = DimType::Diameter; da.value = m_hl_diameter; + arrow_to(centre + ddir * L, amber, da); + } + // Depth arrow along +normal (blind cut). Through cuts ignore depth, so skip the arrow then. + if (!m_hl_through) { + const double L = std::max(m_hl_depth, 40.0 * upp); + DimAnnot da; da.kind = DimType::Distance; da.value = m_hl_depth; + arrow_to(centre + nrm * L, blue, da); + } + + // Centre marker: a small billboarded square so the reposition handle is visible + grabbable. + { + const double s = 7.0 * upp; + std::vector> segs; + segs.emplace_back(Vec2d(-s, -s), Vec2d(s, -s)); + segs.emplace_back(Vec2d( s, -s), Vec2d(s, s)); + segs.emplace_back(Vec2d( s, s), Vec2d(-s, s)); + segs.emplace_back(Vec2d(-s, s), Vec2d(-s, -s)); + glsafe(::glDisable(GL_DEPTH_TEST)); + draw_strokes(m_hl_stroke_model, segs, std::max(0.7 * upp, 1e-4), amber); + } + + m_plane = saved; +} + +// Best-matching hole handle under the cursor: centre (0) / diameter (1) / depth (2), or -1. +// Tested by ray-to-segment distance in world; ~12 px tolerance. Centre wins at the shared base. +int DesignSketchTool::hit_test_hole_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const +{ + if (!m_hl_active) return -1; + const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + const Vec3d ro = r.a, rd = r.b - r.a; + const Camera& cam = wxGetApp().plater()->get_camera(); + const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6); + + const Vec3d centre = m_hl_plane.to_world(Vec2d(m_hl_x, m_hl_y)); + const Vec3d nrm = m_hl_plane.normal.normalized(); + const Vec3d ddir = m_hl_plane.x_axis.normalized(); + const double rad = std::max(m_hl_diameter * 0.5, 40.0 * upp); + const double depL = std::max(m_hl_depth, 40.0 * upp); + const double tol = 12.0 * upp; + + // Centre wins at the shared base: all three handles spring from the centre, so a grab within + // tolerance of the centre point is a reposition (the arrows are only grabbable along the shaft + // that extends outward from here). This also keeps an edge-on arrow — e.g. the depth arrow in + // top view, which collapses onto the centre — from stealing the reposition grab. + if (ray_segment_dist3(ro, rd, centre, centre) <= tol) return 0; + + int best = -1; double bestd = tol; + const double dD = ray_segment_dist3(ro, rd, centre, centre + ddir * rad); + if (dD < bestd) { bestd = dD; best = 1; } + if (!m_hl_through) { + const double dZ = ray_segment_dist3(ro, rd, centre, centre + nrm * depL); + if (dZ < bestd) { bestd = dZ; best = 2; } + } + if (best >= 0) return best; + + // #2 Part B: the X/Y construction-dim labels (edit-only) — only when no arrow handle was hit. + // Larger tolerance since they are text; positions mirror render_hole_gizmo's label offsets. + const double o = 16.0 * upp, ltol = 16.0 * upp; + const double ru = m_hl_has_bounds ? m_hl_umin : 0.0; + const double rv = m_hl_has_bounds ? m_hl_vmin : 0.0; + const Vec3d xlbl = m_hl_plane.to_world(Vec2d((ru + m_hl_x) * 0.5, m_hl_y - o)); + const Vec3d ylbl = m_hl_plane.to_world(Vec2d(m_hl_x + o, (rv + m_hl_y) * 0.5)); + const double dXl = ray_segment_dist3(ro, rd, xlbl, xlbl); + const double dYl = ray_segment_dist3(ro, rd, ylbl, ylbl); + if (dXl <= ltol && dXl <= dYl) return 3; + if (dYl <= ltol) return 4; + return -1; +} + +// Skew-line closest point of the mouse ray to an axis (anchor + t*dir) -> signed distance along +// dir. NaN when the camera is ~parallel to the axis (no meaningful projection). +double DesignSketchTool::hole_axis_proj(GLCanvas3D& canvas, const wxMouseEvent& evt, + const Vec3d& anchor, const Vec3d& dir) const +{ + const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + const Vec3d ro = r.a, rd = r.b - r.a; + const Vec3d e = dir; + const Vec3d w0 = anchor - ro; + const double a = e.dot(e), b = e.dot(rd), c = rd.dot(rd), dd = e.dot(w0), ee = rd.dot(w0); + const double denom = a * c - b * b; + // Relative near-parallel guard: when the camera ray is ~along the axis (e.g. the depth axis in + // top view) denom collapses; a tiny absolute floor lets a huge, unstable projection through. + if (std::abs(denom) < 1e-4 * std::max(a * c, 1e-12)) return std::nan(""); + return (b * ee - c * dd) / denom; +} + +void DesignSketchTool::start_hole_drag(GLCanvas3D& canvas, const wxMouseEvent& evt, int which) +{ + m_hl_drag = which; + m_hl_press_x = evt.GetX(); + m_hl_press_y = evt.GetY(); + const Vec3d centre = m_hl_plane.to_world(Vec2d(m_hl_x, m_hl_y)); + if (which == 0) { + const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + m_hl_grab_uv = m_hl_plane.project(r.a, r.b - r.a); + m_hl_grab_x = m_hl_x; + m_hl_grab_y = m_hl_y; + } else if (which == 1 || which == 2) { + const Vec3d dir = (which == 1) ? m_hl_plane.x_axis.normalized() : m_hl_plane.normal.normalized(); + const double p = hole_axis_proj(canvas, evt, centre, dir); + m_hl_grab_proj = std::isnan(p) ? 0.0 : p; + m_hl_grab_val = (which == 1) ? m_hl_diameter * 0.5 : m_hl_depth; + } + // which == 3/4 (X/Y dim labels) are edit-only: a stationary click opens the inline editor. +} + +void DesignSketchTool::drag_hole_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) +{ + if (m_hl_drag >= 3) return; // X/Y dim labels are click-to-edit, not drag + if (m_hl_drag == 0) { // reposition centre on the plane + const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + const Vec2d uv = m_hl_plane.project(r.a, r.b - r.a); + m_hl_x = m_hl_grab_x + (uv.x() - m_hl_grab_uv.x()); + m_hl_y = m_hl_grab_y + (uv.y() - m_hl_grab_uv.y()); + } else { // diameter (1) or depth (2): relative axis drag + const Vec3d centre = m_hl_plane.to_world(Vec2d(m_hl_x, m_hl_y)); + const Vec3d dir = (m_hl_drag == 1) ? m_hl_plane.x_axis.normalized() : m_hl_plane.normal.normalized(); + const double proj = hole_axis_proj(canvas, evt, centre, dir); + if (std::isnan(proj)) return; // camera ∥ axis: leave value as-is + const double v = std::max(0.01, m_hl_grab_val + (proj - m_hl_grab_proj)); + if (m_hl_drag == 1) m_hl_diameter = 2.0 * v; + else m_hl_depth = v; + } + if (on_hole_changed) on_hole_changed(m_hl_x, m_hl_y, m_hl_diameter, m_hl_depth); +} + +void DesignSketchTool::open_hole_editor(int which) +{ + if (!on_inline_edit) return; + const wxPoint px(m_last_mouse_x, m_last_mouse_y); + if (which == 1) { + on_inline_edit(px, m_hl_diameter, + [this](double v) { + m_hl_diameter = std::max(0.01, v); + if (on_hole_changed) on_hole_changed(m_hl_x, m_hl_y, m_hl_diameter, m_hl_depth); + }, + []() {}); + } else if (which == 2) { + on_inline_edit(px, m_hl_depth, + [this](double v) { + m_hl_depth = std::max(0.01, v); + if (on_hole_changed) on_hole_changed(m_hl_x, m_hl_y, m_hl_diameter, m_hl_depth); + }, + []() {}); + } else if (which == 3) { // #2 Part B: edit the distance from the u-side + const double ru = m_hl_has_bounds ? m_hl_umin : 0.0; + on_inline_edit(px, m_hl_x - ru, + [this, ru](double v) { + m_hl_x = ru + v; + if (on_hole_changed) on_hole_changed(m_hl_x, m_hl_y, m_hl_diameter, m_hl_depth); + }, + []() {}); + } else if (which == 4) { // edit the distance from the v-side + const double rv = m_hl_has_bounds ? m_hl_vmin : 0.0; + on_inline_edit(px, m_hl_y - rv, + [this, rv](double v) { + m_hl_y = rv + v; + if (on_hole_changed) on_hole_changed(m_hl_x, m_hl_y, m_hl_diameter, m_hl_depth); + }, + []() {}); + } + // which == 0 (centre): no scalar to edit inline — it's a drag-only reposition handle. +} + +// ---- Thread gizmo --------------------------------------------------------------------------- +// Mirrors the hole gizmo: footprint circle on the plane at the nominal radius + a radial radius +// arrow (R label) + a normal-axis length arrow (always shown) + a draggable centre marker. +// Reuses hole_axis_proj() for the relative axis drags. +void DesignSketchTool::set_thread_gizmo(const SketchPlane& plane, double x, double y, + double radius, double height) +{ + m_th_plane = plane; + m_th_x = x; + m_th_y = y; + m_th_radius = std::max(0.01, radius); + m_th_height = std::max(0.01, height); + if (!m_th_active) m_th_drag = -1; // re-pushed every preview: preserve an in-progress drag + m_th_active = true; +} + +void DesignSketchTool::clear_thread_gizmo() +{ + m_th_active = false; + m_th_drag = -1; +} + +void DesignSketchTool::render_thread_gizmo() +{ + if (!m_th_active) return; + const Camera& cam = wxGetApp().plater()->get_camera(); + const Vec3d right = cam.get_dir_right().normalized(); + const Vec3d up = cam.get_dir_up().normalized(); + const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6); + const double th = std::max(15.0 * upp, 1e-4); + + const Vec3d centre = m_th_plane.to_world(Vec2d(m_th_x, m_th_y)); + const Vec3d nrm = m_th_plane.normal.normalized(); + const Vec3d ddir = m_th_plane.x_axis.normalized(); // radius arrow runs along plane u + const double r = std::max(0.01, m_th_radius); + + const SketchPlane saved = m_plane; + + // (1) Footprint circle on the plane at the nominal radius. + { + m_plane = m_th_plane; + std::vector> segs; + const int N = 48; + for (int i = 0; i < N; ++i) { + const double a0 = (2.0 * M_PI * i) / N, a1 = (2.0 * M_PI * (i + 1)) / N; + segs.emplace_back(Vec2d(m_th_x + r * std::cos(a0), m_th_y + r * std::sin(a0)), + Vec2d(m_th_x + r * std::cos(a1), m_th_y + r * std::sin(a1))); + } + glsafe(::glDisable(GL_DEPTH_TEST)); + draw_strokes(m_th_stroke_model, segs, std::max(0.6 * upp, 1e-4), ColorRGBA(0.55f, 0.80f, 0.30f, 1.0f)); + m_plane = saved; + } + + // (2) Billboarded handles. + SketchPlane bb; bb.origin = centre; bb.x_axis = right; bb.y_axis = up; bb.normal = cam.get_dir_forward().normalized(); + m_plane = bb; + const ColorRGBA green(0.55f, 0.80f, 0.30f, 1.0f); + const ColorRGBA blue (0.30f, 0.55f, 1.0f, 1.0f); + + auto arrow_to = [&](const Vec3d& tipw, const ColorRGBA& col, const DimAnnot& da) { + const Vec2d tip2((tipw - centre).dot(right), (tipw - centre).dot(up)); + if (tip2.norm() <= 1e-6) return; + const Vec2d u = tip2.normalized(); + const Vec2d nrm2(-u.y(), u.x()); + std::vector> segs; + segs.emplace_back(Vec2d(0, 0), tip2); + const double as = std::max(tip2.norm() * 0.20, th * 0.9); + const Vec2d back = tip2 - u * as; + segs.emplace_back(tip2, back + nrm2 * (as * 0.5)); + segs.emplace_back(tip2, back - nrm2 * (as * 0.5)); + glsafe(::glDisable(GL_DEPTH_TEST)); + draw_strokes(m_th_stroke_model, segs, std::max(0.7 * upp, 1e-4), col); + draw_text(m_line_model, dim_text(da), tip2 + u * (th * 1.4), th, col); + }; + + { // Radius arrow (R label), floored to a grabbable handle. + const double L = std::max(r, 40.0 * upp); + DimAnnot da; da.kind = DimType::Radius; da.value = m_th_radius; + arrow_to(centre + ddir * L, green, da); + } + { // Length arrow along +normal. + const double L = std::max(m_th_height, 40.0 * upp); + DimAnnot da; da.kind = DimType::Distance; da.value = m_th_height; + arrow_to(centre + nrm * L, blue, da); + } + { // Centre marker. + const double s = 7.0 * upp; + std::vector> segs; + segs.emplace_back(Vec2d(-s, -s), Vec2d(s, -s)); + segs.emplace_back(Vec2d( s, -s), Vec2d(s, s)); + segs.emplace_back(Vec2d( s, s), Vec2d(-s, s)); + segs.emplace_back(Vec2d(-s, s), Vec2d(-s, -s)); + glsafe(::glDisable(GL_DEPTH_TEST)); + draw_strokes(m_th_stroke_model, segs, std::max(0.7 * upp, 1e-4), green); + } + + m_plane = saved; +} + +int DesignSketchTool::hit_test_thread_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const +{ + if (!m_th_active) return -1; + const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + const Vec3d ro = r.a, rd = r.b - r.a; + const Camera& cam = wxGetApp().plater()->get_camera(); + const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6); + + const Vec3d centre = m_th_plane.to_world(Vec2d(m_th_x, m_th_y)); + const Vec3d nrm = m_th_plane.normal.normalized(); + const Vec3d ddir = m_th_plane.x_axis.normalized(); + const double rad = std::max(m_th_radius, 40.0 * upp); + const double hL = std::max(m_th_height, 40.0 * upp); + const double tol = 12.0 * upp; + + if (ray_segment_dist3(ro, rd, centre, centre) <= tol) return 0; // centre wins at the base + int best = -1; double bestd = tol; + const double dR = ray_segment_dist3(ro, rd, centre, centre + ddir * rad); + if (dR < bestd) { bestd = dR; best = 1; } + const double dH = ray_segment_dist3(ro, rd, centre, centre + nrm * hL); + if (dH < bestd) { bestd = dH; best = 2; } + return best; +} + +void DesignSketchTool::start_thread_drag(GLCanvas3D& canvas, const wxMouseEvent& evt, int which) +{ + m_th_drag = which; + m_th_press_x = evt.GetX(); + m_th_press_y = evt.GetY(); + const Vec3d centre = m_th_plane.to_world(Vec2d(m_th_x, m_th_y)); + if (which == 0) { + const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + m_th_grab_uv = m_th_plane.project(r.a, r.b - r.a); + m_th_grab_x = m_th_x; + m_th_grab_y = m_th_y; + } else { + const Vec3d dir = (which == 1) ? m_th_plane.x_axis.normalized() : m_th_plane.normal.normalized(); + const double p = hole_axis_proj(canvas, evt, centre, dir); + m_th_grab_proj = std::isnan(p) ? 0.0 : p; + m_th_grab_val = (which == 1) ? m_th_radius : m_th_height; + } +} + +void DesignSketchTool::drag_thread_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) +{ + if (m_th_drag == 0) { + const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + const Vec2d uv = m_th_plane.project(r.a, r.b - r.a); + m_th_x = m_th_grab_x + (uv.x() - m_th_grab_uv.x()); + m_th_y = m_th_grab_y + (uv.y() - m_th_grab_uv.y()); + } else { + const Vec3d centre = m_th_plane.to_world(Vec2d(m_th_x, m_th_y)); + const Vec3d dir = (m_th_drag == 1) ? m_th_plane.x_axis.normalized() : m_th_plane.normal.normalized(); + const double proj = hole_axis_proj(canvas, evt, centre, dir); + if (std::isnan(proj)) return; + const double v = std::max(0.01, m_th_grab_val + (proj - m_th_grab_proj)); + if (m_th_drag == 1) m_th_radius = v; + else m_th_height = v; + } + if (on_thread_changed) on_thread_changed(m_th_x, m_th_y, m_th_radius, m_th_height); +} + +void DesignSketchTool::open_thread_editor(int which) +{ + if (!on_inline_edit) return; + const wxPoint px(m_last_mouse_x, m_last_mouse_y); + if (which == 1) { + on_inline_edit(px, m_th_radius, + [this](double v) { + m_th_radius = std::max(0.01, v); + if (on_thread_changed) on_thread_changed(m_th_x, m_th_y, m_th_radius, m_th_height); + }, + []() {}); + } else if (which == 2) { + on_inline_edit(px, m_th_height, + [this](double v) { + m_th_height = std::max(0.01, v); + if (on_thread_changed) on_thread_changed(m_th_x, m_th_y, m_th_radius, m_th_height); + }, + []() {}); + } +} + +// ---- Shell gizmo ---------------------------------------------------------------------------- +// A single inward thickness arrow at the picked open-face centroid (mirrors the fillet radius +// arrow). RELATIVE drag (like fillet), reusing hole_axis_proj for the projection. +void DesignSketchTool::set_shell_gizmo(const Vec3d& face_centroid, const Vec3d& inward_dir, + double thickness) +{ + m_sh_anchor = face_centroid; + if (inward_dir.norm() > 1e-9) m_sh_dir = inward_dir.normalized(); + m_sh_thickness = std::max(0.01, thickness); + if (!m_sh_active) m_sh_drag = false; // re-pushed every preview: preserve an in-progress drag + m_sh_active = true; +} + +void DesignSketchTool::clear_shell_gizmo() +{ + m_sh_active = false; + m_sh_drag = false; +} + +void DesignSketchTool::render_shell_gizmo() +{ + if (!m_sh_active) return; + const Camera& cam = wxGetApp().plater()->get_camera(); + const Vec3d right = cam.get_dir_right().normalized(); + const Vec3d up = cam.get_dir_up().normalized(); + const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6); + const double th = std::max(15.0 * upp, 1e-4); + const double L = std::max(m_sh_thickness, 40.0 * upp); // WYSIWYG, floored to a handle + const Vec3d tipw = m_sh_anchor + m_sh_dir * L; + + const SketchPlane saved = m_plane; + SketchPlane bb; bb.origin = m_sh_anchor; bb.x_axis = right; bb.y_axis = up; bb.normal = cam.get_dir_forward().normalized(); + m_plane = bb; + const ColorRGBA teal(0.20f, 0.80f, 0.75f, 1.0f); + + const Vec2d tip2((tipw - m_sh_anchor).dot(right), (tipw - m_sh_anchor).dot(up)); + if (tip2.norm() > 1e-6) { + const Vec2d u = tip2.normalized(); + const Vec2d nrm(-u.y(), u.x()); + std::vector> segs; + segs.emplace_back(Vec2d(0, 0), tip2); + const double as = std::max(tip2.norm() * 0.20, th * 0.9); + const Vec2d back = tip2 - u * as; + segs.emplace_back(tip2, back + nrm * (as * 0.5)); + segs.emplace_back(tip2, back - nrm * (as * 0.5)); + glsafe(::glDisable(GL_DEPTH_TEST)); + draw_strokes(m_sh_stroke_model, segs, std::max(0.7 * upp, 1e-4), teal); + DimAnnot da; da.kind = DimType::Distance; da.value = m_sh_thickness; + draw_text(m_line_model, dim_text(da), tip2 + u * (th * 1.4), th, teal); + } + m_plane = saved; +} + +bool DesignSketchTool::hit_test_shell_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt) const +{ + if (!m_sh_active) return false; + const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + const Vec3d ro = r.a, rd = r.b - r.a; + const Camera& cam = wxGetApp().plater()->get_camera(); + const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6); + const double L = std::max(m_sh_thickness, 40.0 * upp); + return ray_segment_dist3(ro, rd, m_sh_anchor, m_sh_anchor + m_sh_dir * L) <= 12.0 * upp; +} + +void DesignSketchTool::start_shell_drag(GLCanvas3D& canvas, const wxMouseEvent& evt) +{ + m_sh_drag = true; + m_sh_press_x = evt.GetX(); + m_sh_press_y = evt.GetY(); + m_sh_grab_val = m_sh_thickness; + const double p = hole_axis_proj(canvas, evt, m_sh_anchor, m_sh_dir); + m_sh_grab_proj = std::isnan(p) ? 0.0 : p; +} + +void DesignSketchTool::drag_shell_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt) +{ + const double proj = hole_axis_proj(canvas, evt, m_sh_anchor, m_sh_dir); + if (std::isnan(proj)) return; + m_sh_thickness = std::max(0.01, m_sh_grab_val + (proj - m_sh_grab_proj)); + if (on_shell_thickness_changed) on_shell_thickness_changed(m_sh_thickness); +} + +void DesignSketchTool::open_shell_editor() +{ + if (!on_inline_edit) return; + const wxPoint px(m_last_mouse_x, m_last_mouse_y); + on_inline_edit(px, m_sh_thickness, + [this](double v) { + m_sh_thickness = std::max(0.01, v); + if (on_shell_thickness_changed) on_shell_thickness_changed(m_sh_thickness); + }, + []() {}); +} + +// ---- Revolve angle-arc gizmo ---------------------------------------------------------------- +// Arc in the revolve plane (perpendicular to the axis) at the profile's radius. The arc center is +// the projection of the profile centroid onto the axis, so the arc rides at the profile's height. +// The yaxis sense follows m_rv_flip, matching the kernel's negative-angle reversed sweep. +static Vec3d rv_yaxis(const Vec3d& axis, const Vec3d& ref, bool flip) +{ + Vec3d y = axis.cross(ref); + if (y.norm() < 1e-9) return ref; // degenerate; never used (ref ⟂ axis by construction) + y.normalize(); + return flip ? -y : y; +} + +void DesignSketchTool::set_revolve_gizmo(const SketchPlane& plane, const Vec2d& centroid, + int axis_sel, double angle, bool flip) +{ + const Vec3d ax = (axis_sel == 1 ? plane.y_axis : plane.x_axis).normalized(); + const Vec3d cw = plane.to_world(centroid); + const double axial = (cw - plane.origin).dot(ax); + m_rv_center = plane.origin + axial * ax; // foot of the centroid on the axis line + Vec3d ref = cw - m_rv_center; // perpendicular to ax by construction + double r = ref.norm(); + if (r < 1e-6) { ref = plane.normal.normalized(); r = std::max(plane.normal.norm(), 1.0); } + m_rv_axis = ax; + m_rv_ref = ref / ref.norm(); + m_rv_radius = std::max(r, 1.0); + m_rv_angle = std::min(360.0, std::max(1.0, std::abs(angle))); + m_rv_flip = flip; + if (!m_rv_active) m_rv_drag = false; // re-pushed every preview: keep an in-progress drag + m_rv_active = true; +} + +void DesignSketchTool::clear_revolve_gizmo() +{ + m_rv_active = false; + m_rv_drag = false; +} + +void DesignSketchTool::render_revolve_gizmo() +{ + if (!m_rv_active) return; + const Camera& cam = wxGetApp().plater()->get_camera(); + const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6); + const double th = std::max(15.0 * upp, 1e-4); + const Vec3d yax = rv_yaxis(m_rv_axis, m_rv_ref, m_rv_flip); + + // The arc lives in the true revolve plane (NOT billboarded) so the sweep reads in 3D. + const SketchPlane saved = m_plane; + SketchPlane rp; rp.origin = m_rv_center; rp.x_axis = m_rv_ref; rp.y_axis = yax; rp.normal = m_rv_axis; + m_plane = rp; + // Vivid cyan: the revolve ghost is amber, so the arc manipulator must contrast with it + // (it overlaps the solid, unlike the extrude depth-arrow which points away into empty space). + const ColorRGBA arcc(0.15f, 0.92f, 1.0f, 1.0f); + const double r = m_rv_radius; + const double a = m_rv_angle * M_PI / 180.0; + const int N = std::max(8, int(a / (M_PI / 32.0))); // ~ every 5.6° + + std::vector> segs; + Vec2d prev(r, 0.0); + for (int i = 1; i <= N; ++i) { + const double t = a * double(i) / double(N); + const Vec2d cur(r * std::cos(t), r * std::sin(t)); + segs.emplace_back(prev, cur); + prev = cur; + } + const Vec2d tip(r * std::cos(a), r * std::sin(a)); + segs.emplace_back(Vec2d(0, 0), Vec2d(r, 0)); // spoke at angle 0 + segs.emplace_back(Vec2d(0, 0), tip); // spoke at the swept angle (the handle) + // Arrowhead at the tip, pointing along the sweep tangent (-sin,cos) rotated by a. + const Vec2d tang(-std::sin(a), std::cos(a)); + const Vec2d radial = tip.normalized(); + const double as = std::max(r * 0.14, th); + segs.emplace_back(tip, tip - tang * as - radial * (as * 0.5)); + segs.emplace_back(tip, tip - tang * as + radial * (as * 0.5)); + // A diamond grab-handle at the tip so the draggable target is unmistakable. + const double hs = std::max(th * 0.8, r * 0.05); + const Vec2d du = radial * hs, dv = Vec2d(-radial.y(), radial.x()) * hs; + segs.emplace_back(tip + du, tip + dv); + segs.emplace_back(tip + dv, tip - du); + segs.emplace_back(tip - du, tip - dv); + segs.emplace_back(tip - dv, tip + du); + + glsafe(::glDisable(GL_DEPTH_TEST)); + draw_strokes(m_rv_stroke_model, segs, std::max(0.8 * upp, 1e-4), arcc); + DimAnnot da; da.kind = DimType::Angle; da.value = m_rv_angle; + draw_text(m_line_model, dim_text(da), tip * 1.14, th, arcc); + m_plane = saved; +} + +bool DesignSketchTool::hit_test_revolve_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const +{ + if (!m_rv_active) return false; + const Linef3 ray = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + const Vec3d ro = ray.a, rd = ray.b - ray.a; + const Camera& cam = wxGetApp().plater()->get_camera(); + const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6); + const Vec3d yax = rv_yaxis(m_rv_axis, m_rv_ref, m_rv_flip); + const double a = m_rv_angle * M_PI / 180.0; + const double tol = 16.0 * upp; + // Grab anywhere on the whole gizmo (Onshape-style): the arc curve OR either radial spoke. + // Take the nearest ray-to-point distance over a dense sampling. + auto ray_pt = [&](const Vec3d& p) { + const double t = (p - ro).dot(rd) / std::max(rd.dot(rd), 1e-12); + return (p - (ro + t * rd)).norm(); + }; + const Vec3d tip = m_rv_center + m_rv_radius * (std::cos(a) * m_rv_ref + std::sin(a) * yax); + const Vec3d ref = m_rv_center + m_rv_radius * m_rv_ref; // angle-0 spoke end + const int N = 48; + for (int i = 0; i <= N; ++i) { + const double f = double(i) / double(N); + const double th = a * f; + const Vec3d arc = m_rv_center + m_rv_radius * (std::cos(th) * m_rv_ref + std::sin(th) * yax); + if (ray_pt(arc) <= tol) return true; // on the arc curve + if (ray_pt(m_rv_center + f * (tip - m_rv_center)) <= tol) return true; // on the swept spoke + if (ray_pt(m_rv_center + f * (ref - m_rv_center)) <= tol) return true; // on the ref spoke + } + return false; +} + +// Intersect the mouse ray with the revolve plane, read its angle around the center -> sweep angle. +void DesignSketchTool::drag_revolve_arc(GLCanvas3D& canvas, const wxMouseEvent& evt) +{ + const Linef3 ray = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + const Vec3d ro = ray.a, rd = ray.b - ray.a; + const double denom = rd.dot(m_rv_axis); + if (std::abs(denom) < 1e-9) return; // ray ∥ revolve plane: leave angle as-is + const double t = (m_rv_center - ro).dot(m_rv_axis) / denom; + const Vec3d p = ro + t * rd; + const Vec3d yax = rv_yaxis(m_rv_axis, m_rv_ref, m_rv_flip); + const double u = (p - m_rv_center).dot(m_rv_ref); + const double v = (p - m_rv_center).dot(yax); + double deg = std::atan2(v, u) * 180.0 / M_PI; + if (deg < 0.0) deg += 360.0; + deg = std::min(360.0, std::max(1.0, deg)); + m_rv_angle = deg; + if (on_revolve_angle_changed) on_revolve_angle_changed(deg); +} + +void DesignSketchTool::open_revolve_editor() +{ + if (!on_inline_edit) return; + const wxPoint px(m_last_mouse_x, m_last_mouse_y); + on_inline_edit(px, m_rv_angle, + [this](double v) { + m_rv_angle = std::min(360.0, std::max(1.0, v)); + if (on_revolve_angle_changed) on_revolve_angle_changed(m_rv_angle); + }, + []() {}); +} + +// ---- Pattern gizmo (linear spacing arrow | circular angle-arc) ------------------------------ +// Linear: a 3D arrow along the world march axis with a tick at each copy; the diamond at the end +// drags the spacing. Circular: a Revolve-style arc about the plane normal through the plane origin. +void DesignSketchTool::set_pattern_gizmo(const SketchPlane& plane, const Vec3d& body_centroid, + bool circular, int count, int dir, double spacing, double angle) +{ + m_pt_circular = circular; + m_pt_base = body_centroid; + m_pt_count = std::max(1, count); + m_pt_spacing = std::max(0.01, spacing); + m_pt_angle = std::min(360.0, std::max(1.0, angle)); + m_pt_dirw = (dir == 1 ? plane.y_axis : plane.x_axis).normalized(); + m_pt_origin = plane.origin; + m_pt_normal = plane.normal.normalized(); + // Circular arc center = foot of the body centroid on the rotation axis; ref = perpendicular dir. + const double axial = (body_centroid - plane.origin).dot(m_pt_normal); + m_pt_ccenter = plane.origin + axial * m_pt_normal; + Vec3d ref = body_centroid - m_pt_ccenter; + double r = ref.norm(); + if (r < 1e-6) { ref = m_pt_dirw; r = 1.0; } // body centred on the axis: nominal radius + m_pt_cref = ref / ref.norm(); + m_pt_radius = std::max(r, 1.0); + if (!m_pt_active) m_pt_drag = false; // re-pushed every preview: keep an in-progress drag + m_pt_active = true; +} + +void DesignSketchTool::clear_pattern_gizmo() +{ + m_pt_active = false; + m_pt_drag = false; +} + +// Linear arrow span = spacing*(count-1), at least one step so count=1 still shows a direction. +static double pt_linear_len(double spacing, int count) +{ + return std::max(spacing * double(std::max(1, count) - 1), spacing); +} + +void DesignSketchTool::render_pattern_gizmo() +{ + if (!m_pt_active) return; + const Camera& cam = wxGetApp().plater()->get_camera(); + const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6); + const double th = std::max(15.0 * upp, 1e-4); + const ColorRGBA col(0.15f, 0.92f, 1.0f, 1.0f); // vivid cyan, matches the gizmo family + const SketchPlane saved = m_plane; + std::vector> segs; + + if (!m_pt_circular) { + // The arrow lives in the true world plane (NOT billboarded) so the march reads in 3D. + const Vec3d perp = m_pt_normal.cross(m_pt_dirw).normalized(); + SketchPlane fp; fp.origin = m_pt_base; fp.x_axis = m_pt_dirw; fp.y_axis = perp; fp.normal = m_pt_normal; + m_plane = fp; + const int copies = std::max(1, m_pt_count); + const double L = pt_linear_len(m_pt_spacing, copies); + segs.emplace_back(Vec2d(0, 0), Vec2d(L, 0)); // shaft + const double as = std::max(L * 0.12, th); // arrowhead + segs.emplace_back(Vec2d(L, 0), Vec2d(L - as, as * 0.5)); + segs.emplace_back(Vec2d(L, 0), Vec2d(L - as, -as * 0.5)); + const double tk = std::max(th, L * 0.05); // copy tick half-height + for (int i = 0; i < copies; ++i) { + const double x = m_pt_spacing * i; + segs.emplace_back(Vec2d(x, -tk), Vec2d(x, tk)); + } + const Vec2d E(L, 0); // diamond grab-handle + const double hs = std::max(th * 0.8, L * 0.04); + segs.emplace_back(E + Vec2d(hs, 0), E + Vec2d(0, hs)); + segs.emplace_back(E + Vec2d(0, hs), E + Vec2d(-hs, 0)); + segs.emplace_back(E + Vec2d(-hs, 0), E + Vec2d(0, -hs)); + segs.emplace_back(E + Vec2d(0, -hs), E + Vec2d(hs, 0)); + glsafe(::glDisable(GL_DEPTH_TEST)); + draw_strokes(m_pt_stroke_model, segs, std::max(0.8 * upp, 1e-4), col); + DimAnnot da; da.kind = DimType::Distance; da.value = m_pt_spacing; + draw_text(m_line_model, dim_text(da), Vec2d(m_pt_spacing * 0.5, tk * 1.7), th, col); + m_plane = saved; + return; + } + + // ---- Circular: clone the Revolve arc about the plane normal through the plane origin ---- + const Vec3d yax = m_pt_normal.cross(m_pt_cref).normalized(); + SketchPlane rp; rp.origin = m_pt_ccenter; rp.x_axis = m_pt_cref; rp.y_axis = yax; rp.normal = m_pt_normal; + m_plane = rp; + const double r = m_pt_radius; + const double a = m_pt_angle * M_PI / 180.0; + const int N = std::max(8, int(a / (M_PI / 32.0))); + Vec2d prev(r, 0.0); + for (int i = 1; i <= N; ++i) { + const double t = a * double(i) / double(N); + const Vec2d cur(r * std::cos(t), r * std::sin(t)); + segs.emplace_back(prev, cur); + prev = cur; + } + const Vec2d tip(r * std::cos(a), r * std::sin(a)); + segs.emplace_back(Vec2d(0, 0), Vec2d(r, 0)); // angle-0 spoke + segs.emplace_back(Vec2d(0, 0), tip); // swept spoke (the handle) + const Vec2d tang(-std::sin(a), std::cos(a)); + const Vec2d radial = tip.normalized(); + const double as = std::max(r * 0.14, th); + segs.emplace_back(tip, tip - tang * as - radial * (as * 0.5)); + segs.emplace_back(tip, tip - tang * as + radial * (as * 0.5)); + const double hs = std::max(th * 0.8, r * 0.05); + const Vec2d du = radial * hs, dv = Vec2d(-radial.y(), radial.x()) * hs; + segs.emplace_back(tip + du, tip + dv); + segs.emplace_back(tip + dv, tip - du); + segs.emplace_back(tip - du, tip - dv); + segs.emplace_back(tip - dv, tip + du); + glsafe(::glDisable(GL_DEPTH_TEST)); + draw_strokes(m_pt_stroke_model, segs, std::max(0.8 * upp, 1e-4), col); + DimAnnot da; da.kind = DimType::Angle; da.value = m_pt_angle; + draw_text(m_line_model, dim_text(da), tip * 1.14, th, col); + m_plane = saved; +} + +bool DesignSketchTool::hit_test_pattern_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const +{ + if (!m_pt_active) return false; + const Linef3 ray = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + const Vec3d ro = ray.a, rd = ray.b - ray.a; + const Camera& cam = wxGetApp().plater()->get_camera(); + const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6); + auto ray_pt = [&](const Vec3d& p) { + const double t = (p - ro).dot(rd) / std::max(rd.dot(rd), 1e-12); + return (p - (ro + t * rd)).norm(); + }; + if (!m_pt_circular) { + const double tol = 8.0 * upp; + const double L = pt_linear_len(m_pt_spacing, m_pt_count); + return ray_segment_dist3(ro, rd, m_pt_base, m_pt_base + m_pt_dirw * L) <= tol; + } + const double tol = 16.0 * upp; + const Vec3d yax = m_pt_normal.cross(m_pt_cref).normalized(); + const double a = m_pt_angle * M_PI / 180.0; + const Vec3d tip = m_pt_ccenter + m_pt_radius * (std::cos(a) * m_pt_cref + std::sin(a) * yax); + const Vec3d ref = m_pt_ccenter + m_pt_radius * m_pt_cref; + const int N = 48; + for (int i = 0; i <= N; ++i) { + const double f = double(i) / double(N); + const Vec3d arc = m_pt_ccenter + m_pt_radius * (std::cos(a * f) * m_pt_cref + std::sin(a * f) * yax); + if (ray_pt(arc) <= tol) return true; + if (ray_pt(m_pt_ccenter + f * (tip - m_pt_ccenter)) <= tol) return true; + if (ray_pt(m_pt_ccenter + f * (ref - m_pt_ccenter)) <= tol) return true; + } + return false; +} + +void DesignSketchTool::drag_pattern_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) +{ + const Linef3 ray = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + const Vec3d ro = ray.a, rd = ray.b - ray.a; + if (!m_pt_circular) { + // Skew-line closest point of the mouse ray to the march axis -> span -> spacing. + const Vec3d e = m_pt_dirw; + const Vec3d w0 = m_pt_base - ro; + const double a = e.dot(e), b = e.dot(rd), c = rd.dot(rd), dd = e.dot(w0), ee = rd.dot(w0); + const double denom = a * c - b * b; + if (std::abs(denom) < 1e-7) return; // camera ∥ axis: leave spacing as-is + const double span = std::max(0.01, (b * ee - c * dd) / denom); + const int div = std::max(1, m_pt_count - 1); + m_pt_spacing = std::max(0.1, span / double(div)); + if (on_pattern_changed) on_pattern_changed(m_pt_spacing); + return; + } + const double denom = rd.dot(m_pt_normal); + if (std::abs(denom) < 1e-9) return; // ray ∥ rotation plane: leave angle as-is + const double t = (m_pt_ccenter - ro).dot(m_pt_normal) / denom; + const Vec3d p = ro + t * rd; + const Vec3d yax = m_pt_normal.cross(m_pt_cref).normalized(); + double deg = std::atan2((p - m_pt_ccenter).dot(yax), (p - m_pt_ccenter).dot(m_pt_cref)) * 180.0 / M_PI; + if (deg < 0.0) deg += 360.0; + m_pt_angle = std::min(360.0, std::max(1.0, deg)); + if (on_pattern_changed) on_pattern_changed(m_pt_angle); +} + +void DesignSketchTool::open_pattern_editor() +{ + if (!on_inline_edit) return; + const wxPoint px(m_last_mouse_x, m_last_mouse_y); + const double cur = m_pt_circular ? m_pt_angle : m_pt_spacing; + on_inline_edit(px, cur, + [this](double v) { + if (m_pt_circular) m_pt_angle = std::min(360.0, std::max(1.0, v)); + else m_pt_spacing = std::max(0.1, v); + if (on_pattern_changed) on_pattern_changed(m_pt_circular ? m_pt_angle : m_pt_spacing); + }, + []() {}); +} + +// Closed loops + the entity indices that form each one. A circle/ellipse is its own loop; +// line/arc chains are walked endpoint-to-endpoint. Entity membership lets a single loop be +// highlighted and extruded on its own. +std::vector +DesignSketchTool::region_loops(const std::vector& ents) const +{ + std::vector regions; + const double eps2 = 1e-3 * 1e-3; + auto near = [&](const Vec2d& a, const Vec2d& b) { return (a - b).squaredNorm() < eps2; }; + + // Circles are self-closed regions; lines/arcs are open segments to be chained. Each + // Seg remembers the entity index it came from. + struct Seg { std::vector pts; int ent{-1}; bool used{false}; }; + std::vector segs; + for (int i = 0; i < int(ents.size()); ++i) { + const SketchEntity& e = ents[i]; + if (e.construction) continue; + bool closed = false; + if (e.type == SketchEntity::Type::Circle || e.type == SketchEntity::Type::Ellipse) { + regions.push_back({ entity_polyline(e, closed), { i } }); + } else if (e.type == SketchEntity::Type::Line || e.type == SketchEntity::Type::Arc + || e.type == SketchEntity::Type::EllipseArc + || e.type == SketchEntity::Type::BSpline) { + std::vector p = entity_polyline(e, closed); + if (p.size() >= 2) segs.push_back({ std::move(p), i, false }); + } + } + + // Walk each unused segment endpoint-to-endpoint until the chain returns to its + // start (a closed loop) or stalls (an open chain, discarded). + for (size_t s = 0; s < segs.size(); ++s) { + if (segs[s].used) continue; + segs[s].used = true; + std::vector loop = segs[s].pts; + std::vector loop_ents = { segs[s].ent }; + const Vec2d start = loop.front(); + Vec2d cur = loop.back(); + bool extended = true; + while (extended && !near(cur, start)) { + extended = false; + for (size_t t = 0; t < segs.size(); ++t) { + if (segs[t].used) continue; + const std::vector& q = segs[t].pts; + if (near(q.front(), cur)) { + for (size_t k = 1; k < q.size(); ++k) loop.push_back(q[k]); + cur = q.back(); + } else if (near(q.back(), cur)) { + for (int k = int(q.size()) - 2; k >= 0; --k) loop.push_back(q[k]); + cur = q.front(); + } else { + continue; + } + segs[t].used = true; + loop_ents.push_back(segs[t].ent); + extended = true; + break; + } + } + if (near(cur, start) && loop.size() >= 4) { + loop.pop_back(); // drop the duplicate closing vertex + regions.push_back({ std::move(loop), std::move(loop_ents) }); + } + } + return regions; +} + +int DesignSketchTool::region_at(const Vec2d& p) const +{ + const std::vector> regions = closed_regions(); + auto inside = [](const Vec2d& q, const std::vector& poly) { + bool in = false; + for (size_t i = 0, j = poly.size() - 1; i < poly.size(); j = i++) { + const Vec2d& a = poly[i]; + const Vec2d& b = poly[j]; + if (((a.y() > q.y()) != (b.y() > q.y())) && + (q.x() < (b.x() - a.x()) * (q.y() - a.y()) / (b.y() - a.y()) + a.x())) + in = !in; + } + return in; + }; + for (size_t i = 0; i < regions.size(); ++i) + if (inside(p, regions[i])) return int(i); + return -1; +} + +// ---- rendering -------------------------------------------------------------- + +void DesignSketchTool::draw_quad_strip(GLModel& model, const std::vector& pts, bool closed, const ColorRGBA& color) +{ + if (pts.size() < 2) + return; + + const double hw = 0.6; + GLModel::Geometry g; + g.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + unsigned int base = 0; + + const size_t segs = closed ? pts.size() : pts.size() - 1; + for (size_t i = 0; i < segs; ++i) { + const Vec2d a = pts[i]; + const Vec2d b = pts[(i + 1) % pts.size()]; + const Vec2d d = b - a; + const double len = d.norm(); + if (len < 1e-6) + continue; + const Vec2d n(-d.y() / len, d.x() / len); + const Vec2d o = n * hw; + g.add_vertex((Vec3f)m_plane.to_world(a + o).cast()); + g.add_vertex((Vec3f)m_plane.to_world(b + o).cast()); + g.add_vertex((Vec3f)m_plane.to_world(b - o).cast()); + g.add_vertex((Vec3f)m_plane.to_world(a - o).cast()); + g.add_triangle(base, base + 1, base + 2); + g.add_triangle(base, base + 2, base + 3); + base += 4; + } + + if (base > 0) { + model.reset(); + model.init_from(std::move(g)); + model.set_color(color); + model.render(); + } +} + +namespace { +double poly_signed_area(const std::vector& p) +{ + double a = 0.0; + for (size_t i = 0, n = p.size(); i < n; ++i) { + const Vec2d& u = p[i]; + const Vec2d& v = p[(i + 1) % n]; + a += u.x() * v.y() - v.x() * u.y(); + } + return 0.5 * a; +} +bool pt_in_tri(const Vec2d& p, const Vec2d& a, const Vec2d& b, const Vec2d& c) +{ + auto cross = [](const Vec2d& u, const Vec2d& v, const Vec2d& w) { + return (v.x() - u.x()) * (w.y() - u.y()) - (v.y() - u.y()) * (w.x() - u.x()); + }; + const double d1 = cross(a, b, p), d2 = cross(b, c, p), d3 = cross(c, a, p); + const bool neg = (d1 < 0) || (d2 < 0) || (d3 < 0); + const bool pos = (d1 > 0) || (d2 > 0) || (d3 > 0); + return !(neg && pos); // inside iff all cross products share a sign +} +// Ear-clipping triangulation of a simple polygon; returns index triples into `poly`. +std::vector> ear_clip(const std::vector& poly) +{ + std::vector> tris; + const size_t n = poly.size(); + if (n < 3) return tris; + std::vector idx(n); + for (unsigned i = 0; i < n; ++i) idx[i] = i; + if (poly_signed_area(poly) < 0.0) std::reverse(idx.begin(), idx.end()); // work CCW + int guard = 0; + while (idx.size() > 3 && guard++ < int(10 * n)) { + bool clipped = false; + const int m = int(idx.size()); + for (int i = 0; i < m; ++i) { + const unsigned i0 = idx[(i + m - 1) % m]; + const unsigned i1 = idx[i]; + const unsigned i2 = idx[(i + 1) % m]; + const Vec2d& a = poly[i0]; const Vec2d& b = poly[i1]; const Vec2d& c = poly[i2]; + const double cr = (b.x() - a.x()) * (c.y() - a.y()) - (b.y() - a.y()) * (c.x() - a.x()); + if (cr <= 0.0) continue; // reflex vertex, not an ear tip + bool ear = true; + for (int j = 0; j < m; ++j) { + const unsigned ij = idx[j]; + if (ij == i0 || ij == i1 || ij == i2) continue; + if (pt_in_tri(poly[ij], a, b, c)) { ear = false; break; } + } + if (!ear) continue; + tris.push_back({ i0, i1, i2 }); + idx.erase(idx.begin() + i); + clipped = true; + break; + } + if (!clipped) break; // degenerate input: bail rather than spin + } + if (idx.size() == 3) tris.push_back({ idx[0], idx[1], idx[2] }); + return tris; +} +} // namespace + +// Triangulate a closed boundary polygon and render it as a (blended) filled face. +void DesignSketchTool::draw_fill(GLModel& model, const std::vector& poly, const ColorRGBA& color) +{ + if (poly.size() < 3) return; + const auto tris = ear_clip(poly); + if (tris.empty()) return; + GLModel::Geometry g; + g.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + for (const Vec2d& p : poly) + g.add_vertex((Vec3f)m_plane.to_world(p).cast()); + for (const auto& t : tris) + g.add_triangle(t[0], t[1], t[2]); + model.reset(); + model.init_from(std::move(g)); + model.set_color(color); + model.render(); +} + +void DesignSketchTool::draw_vertices(GLModel& model, const std::vector& pts, const ColorRGBA& color, + double half_size) +{ + if (pts.empty()) + return; + + const double hs = half_size; + GLModel::Geometry g; + g.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + unsigned int base = 0; + for (const Vec2d& p : pts) { + g.add_vertex((Vec3f)m_plane.to_world(p + Vec2d(-hs, -hs)).cast()); + g.add_vertex((Vec3f)m_plane.to_world(p + Vec2d( hs, -hs)).cast()); + g.add_vertex((Vec3f)m_plane.to_world(p + Vec2d( hs, hs)).cast()); + g.add_vertex((Vec3f)m_plane.to_world(p + Vec2d(-hs, hs)).cast()); + g.add_triangle(base, base + 1, base + 2); + g.add_triangle(base, base + 2, base + 3); + base += 4; + } + + model.reset(); + model.init_from(std::move(g)); + model.set_color(color); + model.render(); +} + +// Independent thick-line segments batched into one immediate-mode draw (quote lines, +// extension lines, arrowheads, glyph strokes). Mirrors draw_quad_strip's lift-to-world. +void DesignSketchTool::draw_strokes(GLModel& model, const std::vector>& segs, + double hw, const ColorRGBA& color) +{ + GLModel::Geometry g; + g.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + unsigned int base = 0; + for (const auto& s : segs) { + const Vec2d a = s.first, b = s.second; + const Vec2d d = b - a; + const double len = d.norm(); + if (len < 1e-6) continue; + const Vec2d n(-d.y() / len, d.x() / len); + const Vec2d o = n * hw; + g.add_vertex((Vec3f)m_plane.to_world(a + o).cast()); + g.add_vertex((Vec3f)m_plane.to_world(b + o).cast()); + g.add_vertex((Vec3f)m_plane.to_world(b - o).cast()); + g.add_vertex((Vec3f)m_plane.to_world(a - o).cast()); + g.add_triangle(base, base + 1, base + 2); + g.add_triangle(base, base + 2, base + 3); + base += 4; + } + if (base > 0) { + model.reset(); + model.init_from(std::move(g)); + model.set_color(color); + model.render(); + } +} + +namespace { +// Smooth single-stroke (Hershey-style) vector font for dimension labels. Glyphs +// live in a 0..0.6 (x) by 0..1 (y) cell, baseline at y=0, cap height y=1; curved +// digits are sampled as short segments so they read as rounded shapes, not blocks. +// `advance` is the pen step after the glyph. +constexpr double kPi = 3.14159265358979323846; +inline double rad(double deg) { return deg * kPi / 180.0; } + +// Connect a list of points as a polyline. +void poly(std::vector>& out, std::initializer_list p) +{ + auto it = p.begin(); + if (it == p.end()) return; + Vec2d prev = *it++; + for (; it != p.end(); ++it) { out.emplace_back(prev, *it); prev = *it; } +} +// Sample an elliptical arc (centre cx,cy; radii rx,ry) from angle a0..a1. +void arc(std::vector>& out, double cx, double cy, double rx, double ry, + double a0, double a1, int n = 14) +{ + Vec2d prev(cx + rx * std::cos(a0), cy + ry * std::sin(a0)); + for (int i = 1; i <= n; ++i) { + const double t = a0 + (a1 - a0) * (double)i / n; + const Vec2d cur(cx + rx * std::cos(t), cy + ry * std::sin(t)); + out.emplace_back(prev, cur); + prev = cur; + } +} + +void glyph_strokes(char c, std::vector>& out, double& advance) +{ + advance = 0.72; + switch (c) { + case '0': + arc(out, 0.30, 0.50, 0.25, 0.48, 0.0, 2.0 * kPi); + break; + case '1': + poly(out, {Vec2d(0.13, 0.76), Vec2d(0.33, 1.0), Vec2d(0.33, 0.0)}); + poly(out, {Vec2d(0.13, 0.0), Vec2d(0.53, 0.0)}); + advance = 0.52; + break; + case '2': + arc(out, 0.30, 0.72, 0.25, 0.25, rad(170), rad(-45)); + poly(out, {Vec2d(0.477, 0.543), Vec2d(0.06, 0.0), Vec2d(0.56, 0.0)}); + break; + case '3': + arc(out, 0.30, 0.74, 0.24, 0.24, rad(160), rad(-90)); + arc(out, 0.30, 0.26, 0.26, 0.26, rad(90), rad(-160)); + break; + case '4': + poly(out, {Vec2d(0.42, 1.0), Vec2d(0.04, 0.32), Vec2d(0.58, 0.32)}); + poly(out, {Vec2d(0.42, 1.0), Vec2d(0.42, 0.0)}); + break; + case '5': + poly(out, {Vec2d(0.54, 1.0), Vec2d(0.12, 1.0), Vec2d(0.11, 0.52)}); + arc(out, 0.27, 0.30, 0.27, 0.27, rad(130), rad(-120)); + break; + case '6': + arc(out, 0.30, 0.28, 0.26, 0.26, 0.0, 2.0 * kPi); + arc(out, 0.30, 0.55, 0.30, 0.45, rad(90), rad(190)); + break; + case '7': + poly(out, {Vec2d(0.05, 1.0), Vec2d(0.57, 1.0), Vec2d(0.22, 0.0)}); + break; + case '8': + arc(out, 0.30, 0.73, 0.22, 0.25, 0.0, 2.0 * kPi); + arc(out, 0.30, 0.26, 0.26, 0.26, 0.0, 2.0 * kPi); + break; + case '9': + arc(out, 0.30, 0.70, 0.26, 0.26, 0.0, 2.0 * kPi); + arc(out, 0.28, 0.55, 0.28, 0.55, rad(15), rad(-90)); + break; + case '.': + case ',': // locale (LC_NUMERIC) may format the decimal separator as a comma + // small solid dot: crossed short strokes so the quads fill a visible disk + poly(out, {Vec2d(0.10, 0.08), Vec2d(0.24, 0.08)}); + poly(out, {Vec2d(0.17, 0.02), Vec2d(0.17, 0.15)}); + advance = 0.30; + break; + case '-': + poly(out, {Vec2d(0.10, 0.5), Vec2d(0.50, 0.5)}); + advance = 0.62; + break; + case 'R': + poly(out, {Vec2d(0.08, 0.0), Vec2d(0.08, 1.0), Vec2d(0.38, 1.0)}); + arc(out, 0.38, 0.75, 0.17, 0.25, rad(90), rad(-90)); + poly(out, {Vec2d(0.38, 0.50), Vec2d(0.08, 0.50)}); + poly(out, {Vec2d(0.30, 0.50), Vec2d(0.58, 0.0)}); + advance = 0.80; + break; + case ' ': + advance = 0.5; + break; + default: + advance = 0.5; + break; + } +} +} // namespace + +void DesignSketchTool::draw_text(GLModel& model, const std::string& s, const Vec2d& center, + double height, const ColorRGBA& color) +{ + std::vector>> glyphs; + std::vector advs; + double total = 0.0; + for (size_t i = 0; i < s.size(); ++i) { + std::vector> gs; + double adv = 0.5; + if ((unsigned char)s[i] == 0xC3 && i + 1 < s.size() && (unsigned char)s[i + 1] == 0x98) { + glyph_strokes('0', gs, adv); // 'Ø' = '0' + slash + gs.emplace_back(Vec2d(0.0, 0.0), Vec2d(0.6, 1.0)); + ++i; + } else if ((unsigned char)s[i] == 0xC2 && i + 1 < s.size() && (unsigned char)s[i + 1] == 0xB0) { + // '°' degree sign: a small open ring high in the cell, approximated by a + // short polyline loop (the stroke font has no curves primitive here). + const Vec2d c(0.18, 0.85); const double r = 0.16; + const int N = 8; Vec2d prev = c + Vec2d(r, 0); + for (int k = 1; k <= N; ++k) { + const double t = 2.0 * 3.14159265358979 * k / N; + const Vec2d cur = c + Vec2d(r * std::cos(t), r * std::sin(t)); + gs.emplace_back(prev, cur); prev = cur; + } + adv = 0.42; + ++i; + } else { + glyph_strokes(s[i], gs, adv); + } + glyphs.push_back(std::move(gs)); + advs.push_back(adv); + total += adv; + } + const Vec2d origin = center - Vec2d(total * height * 0.5, height * 0.5); + std::vector> world; + double pen = 0.0; + for (size_t g = 0; g < glyphs.size(); ++g) { + for (const auto& seg : glyphs[g]) { + const Vec2d a = origin + Vec2d((seg.first.x() + pen) * height, seg.first.y() * height); + const Vec2d b = origin + Vec2d((seg.second.x() + pen) * height, seg.second.y() * height); + world.emplace_back(a, b); + } + pen += advs[g]; + } + draw_strokes(model, world, std::max(height * 0.08, 0.02), color); +} + +// Draw every placed dimension: extension lines, the offset dimension line, arrowheads +// and the numeric label. Geometry is recomputed from the (solved) entities each frame +// so the quote tracks the sketch. +// Draw one dimension's quote (extension/dimension lines, arrowheads, numeric label). +// Geometry is recomputed from the (solved) entities so the quote tracks the sketch. +// Returns the label centre in out_label; false if the annot references missing/degenerate +// geometry. Shared by placed (render_dimensions) and live (render_live_quotes) quotes. +bool DesignSketchTool::draw_dim_quote(const DimAnnot& a, double th, const ColorRGBA& dimcol, + Vec2d& out_label) +{ + std::vector> segs; + if (a.kind == DimType::Length || a.kind == DimType::Distance) { + Vec2d pa, pb; + if (a.kind == DimType::Length) { + if (a.ea < 0 || a.ea >= int(m_entities.size())) return false; + pa = m_entities[a.ea].p0; pb = m_entities[a.ea].p1; + } else if (!point_at(a.ea, a.ra, pa) || !point_at(a.eb, a.rb, pb)) { + return false; + } + const Vec2d d = pb - pa; + const double L = d.norm(); + if (L < 1e-6) return false; + const Vec2d u = d / L; + const Vec2d nrm(-u.y(), u.x()); + const double side = (a.side != 0.0) ? a.side : 1.0; + const double off = side * std::max(L * 0.18, 8.0); + const Vec2d A2 = pa + nrm * off, B2 = pb + nrm * off; + const Vec2d ext = nrm * (off + side * 2.0); + segs.emplace_back(pa, pa + ext); // extension lines + segs.emplace_back(pb, pb + ext); + segs.emplace_back(A2, B2); // dimension line + const double as = std::max(L * 0.04, 2.0); + auto arrow = [&](const Vec2d& tip, const Vec2d& dir) { + const Vec2d back = tip + dir * as; + segs.emplace_back(tip, back + nrm * (as * 0.5)); + segs.emplace_back(tip, back - nrm * (as * 0.5)); + }; + arrow(A2, u); arrow(B2, -u); + out_label = (A2 + B2) * 0.5 + nrm * (side * (th * 0.7 + 1.5)); + } else if (a.kind == DimType::Diameter || a.kind == DimType::Radius) { + if (a.ea < 0 || a.ea >= int(m_entities.size())) return false; + const SketchEntity& e = m_entities[a.ea]; + const Vec2d c = e.center; + const double r = e.radius; + if (r < 1e-6) return false; + const Vec2d u(1.0, 0.0); + const double as = std::max(r * 0.12, 2.0); + if (a.kind == DimType::Diameter) { + const Vec2d p1 = c - u * r, p2 = c + u * r; + segs.emplace_back(p1, p2); + segs.emplace_back(p1, p1 + u * as + Vec2d(0, 1) * (as * 0.5)); + segs.emplace_back(p1, p1 + u * as - Vec2d(0, 1) * (as * 0.5)); + segs.emplace_back(p2, p2 - u * as + Vec2d(0, 1) * (as * 0.5)); + segs.emplace_back(p2, p2 - u * as - Vec2d(0, 1) * (as * 0.5)); + out_label = c + Vec2d(0, 1) * (th * 0.8); + } else { + const Vec2d p2 = c + u * r; + segs.emplace_back(c, p2); + segs.emplace_back(p2, p2 - u * as + Vec2d(0, 1) * (as * 0.5)); + segs.emplace_back(p2, p2 - u * as - Vec2d(0, 1) * (as * 0.5)); + out_label = (c + p2) * 0.5 + Vec2d(0, 1) * (th * 0.8); + } + } else if (a.kind == DimType::DistanceToLine) { + Vec2d pa; + if (!point_at(a.ea, a.ra, pa) || a.eb < 0 || a.eb >= int(m_entities.size())) return false; + const SketchEntity& Ln = m_entities[a.eb]; + const Vec2d ld = Ln.p1 - Ln.p0; + const double n = ld.norm(); + if (n < 1e-9) return false; + const Vec2d u = ld / n; + const double t = (pa - Ln.p0).dot(u); + const Vec2d foot = Ln.p0 + u * t; // perpendicular foot on the line + segs.emplace_back(pa, foot); + out_label = (pa + foot) * 0.5 + u * (th * 0.7 + 1.5); + } else if (a.kind == DimType::Angle) { + if (a.ea < 0 || a.ea >= int(m_entities.size())) return false; + const SketchEntity& e = m_entities[a.ea]; + if (e.type != SketchEntity::Type::Line) return false; + const Vec2d d = e.p1 - e.p0; + const double L = d.norm(); + if (L < 1e-6) return false; + double ang = std::atan2(d.y(), d.x()); // signed, matches measure_dim sweep + const double rr = std::max(std::min(L * 0.35, 40.0), th * 1.6); // arc radius + segs.emplace_back(e.p0, e.p0 + Vec2d(rr * 1.15, 0.0)); // horizontal reference leg + const int N = 20; // arc 0 -> ang about p0 + Vec2d prev = e.p0 + Vec2d(rr, 0.0); + for (int i = 1; i <= N; ++i) { + const double t = ang * double(i) / N; + const Vec2d cur = e.p0 + Vec2d(rr * std::cos(t), rr * std::sin(t)); + segs.emplace_back(prev, cur); prev = cur; + } + const double mid = ang * 0.5; + out_label = e.p0 + Vec2d(std::cos(mid), std::sin(mid)) * (rr + th * 1.1); + } else { + return false; + } + draw_strokes(m_highlight_model, segs, 0.6, dimcol); + draw_text(m_line_model, dim_text(a), out_label, th, dimcol); + return true; +} + +// Draw every placed (driving) dimension; cache each label centre for picking. +void DesignSketchTool::render_dimensions(double unit_per_px) +{ + if (m_dimensions.empty()) return; + const ColorRGBA dimcol(0.30f, 0.88f, 0.66f, 1.0f); // teal-green CAD quote + // Label text is a CONSTANT screen size (like real CAD), not scaled to geometry, + // so a long line doesn't get huge text. ~15 px tall in plane units at this zoom. + const double th = std::max(15.0 * unit_per_px, 1e-4); + for (size_t di = 0; di < m_dimensions.size(); ++di) { + Vec2d label; + if (draw_dim_quote(m_dimensions[di], th, dimcol, label)) + m_dimensions[di].label_pos = label; + } +} + +// Per-entity-type characteristic dimensions, drawn as live non-driving quotes for the +// entity being edited (point/handle drag, or a lone selection). This is the Onshape +// pattern that scales to every tool: each kind reports its defining dimension(s); each +// is clickable (m_live_quotes) to promote to a driving dim + open the inline editor. +// A dim already driven on the entity is skipped (render_dimensions draws that one). +void DesignSketchTool::render_live_quotes(double unit_per_px) +{ + m_live_quotes.clear(); + m_live_poly_fi = -1; + m_live_poly_side_label = m_live_poly_angle_label = Vec2d(1e18, 1e18); + m_live_arc_ei = -1; + m_live_arc_angle_label = Vec2d(1e18, 1e18); + m_live_ellipse_ei = -1; + m_live_ellipse_major_label = m_live_ellipse_minor_label = Vec2d(1e18, 1e18); + m_live_ellipsearc_sweep_label = Vec2d(1e18, 1e18); + m_live_obrect_fi = -1; + m_live_obrect_angle_label = Vec2d(1e18, 1e18); + m_live_rrect_fi = -1; + m_live_rrect_w_label = m_live_rrect_h_label = m_live_rrect_r_label = Vec2d(1e18, 1e18); + m_live_aslot_fi = -1; + m_live_aslot_r_label = m_live_aslot_w_label = Vec2d(1e18, 1e18); + // Edit-op tools (Fillet/Chamfer/Offset/Mirror) put their picks in m_selection for the + // highlight, but their own arrow/label gizmo is the value affordance — don't also draw + // the picked entity's characteristic quotes (Length/Angle/…) or the view gets cluttered. + if (is_edit_op_mode() || is_transform_mode()) return; + int ei = -1; + if (m_dragging_point && m_drag_ei >= 0) ei = m_drag_ei; + else if (m_dragging_handle) ei = m_drag_handle.ei; + else if (m_selection.size() == 1) ei = m_selection[0]; + if (ei < 0 || ei >= int(m_entities.size())) return; + const SketchEntity& e = m_entities[ei]; + + std::vector protos; + auto add_len = [&](int line_ei, double side) { + if (line_ei < 0 || line_ei >= int(m_entities.size())) return; + if (m_entities[line_ei].type != SketchEntity::Type::Line) return; + DimAnnot a; a.kind = DimType::Length; a.ea = line_ei; a.side = side; protos.push_back(a); + }; + + // A grouped gesture (rect/slot/polygon decomposes into raw lines/arcs) exposes its + // DERIVED characteristic dims off the Feature span, regardless of which member edge + // was picked. Rect: Width = first edge length, Height = second edge length (the two + // axes of the 4-line loop pushed by push_closed_lines: edge0 horizontal, edge1 + // vertical). Quotes offset to opposite sides so they don't overlap. + const int fi = feature_of(ei); + if (fi >= 0) { + const Feature& f = m_features[fi]; + switch (f.kind) { + case FeatureKind::CornerRect: + case FeatureKind::CenterRect: { + add_len(f.begin + 0, 1.0); // Width + add_len(f.begin + 1, -1.0); // Height + // OBLIQUE rect (drawn off-axis, also a CornerRect feature): expose its orientation + // too. Gated to genuinely-tilted edges so an axis-aligned Corner/Center rect never + // gets an angle quote (and its verified W/H behaviour is untouched). + if (f.begin >= 0 && f.begin < int(m_entities.size())) { + const SketchEntity& e0 = m_entities[f.begin]; + Vec2d d0 = e0.p1 - e0.p0; + if (d0.squaredNorm() > 1e-12) { + double deg = std::atan2(d0.y(), d0.x()) * 180.0 / M_PI; + const double off = std::fmod(std::fmod(deg, 90.0) + 90.0, 90.0); // dist to axis + if (off > 2.0 && off < 88.0) { + const ColorRGBA dc(0.30f, 0.88f, 0.66f, 1.0f); + const double th = std::max(15.0 * unit_per_px, 1e-4); + double adeg = deg; if (adeg < 0.0) adeg += 360.0; + const Vec2d mid = 0.5 * (e0.p0 + e0.p1); + Vec2d nrm(-d0.y(), d0.x()); if (nrm.squaredNorm() > 1e-12) nrm.normalize(); + m_live_obrect_angle_label = mid + nrm * (th * 1.4); + DimAnnot at; at.kind = DimType::Angle; at.value = adeg; + draw_text(m_line_model, dim_text(at), m_live_obrect_angle_label, th, dc); + m_live_obrect_fi = fi; + } + } + } + break; + } + case FeatureKind::Slot: { + // make_slot order: [top line, cap@c1, bottom line, cap@c0]. Centre-distance = + // Distance between the two cap-arc centres; Width = cap Radius (half-width). + const int cap_c1 = f.begin + 1, cap_c0 = f.begin + 3; + if (cap_c0 < int(m_entities.size()) && cap_c1 < int(m_entities.size())) { + DimAnnot dst; dst.kind = DimType::Distance; + dst.ea = cap_c0; dst.ra = SketchPointRole::Center; + dst.eb = cap_c1; dst.rb = SketchPointRole::Center; + // Push the centre-distance label clear ABOVE the slot (past the cap + // half-width) so it sits outside the fillable face — otherwise clicking + // it would hit the interior and trigger face-select. a.side scales the + // quote offset (draw_dim_quote: off = side * max(L*0.18, 8)). + const double Lc = (f.c1 - f.c0).norm(); + const double unit = std::max(Lc * 0.18, 8.0); + const double th = std::max(15.0 * unit_per_px, 1e-4); + dst.side = (f.param + th * 2.5) / unit; // clear cap + label height + protos.push_back(dst); + DimAnnot rad; rad.kind = DimType::Radius; rad.ea = cap_c1; // width + protos.push_back(rad); + } + break; + } + case FeatureKind::Polygon: { + // A regular polygon is N raw lines (no centre entity). Its natural editable + // dims are the SIDE length and the ORIENTATION — NOT a circumradius (a polygon + // is not a circle). Both edit the whole loop geometrically: side scales it + // uniformly, angle rotates it. Drawn off edge0 with draw_dim_quote (Length + + // Angle); the side quote is offset OUTWARD so its label clears the face. + if (f.begin < int(m_entities.size()) && + m_entities[f.begin].type == SketchEntity::Type::Line) { + const ColorRGBA dc(0.30f, 0.88f, 0.66f, 1.0f); + const double th = std::max(15.0 * unit_per_px, 1e-4); + const SketchEntity& e0 = m_entities[f.begin]; + const Vec2d m0 = 0.5 * (e0.p0 + e0.p1); + Vec2d u0 = e0.p1 - e0.p0; + if (u0.squaredNorm() > 1e-12) u0.normalize(); + const Vec2d n0(-u0.y(), u0.x()); + const double outsign = ((m0 + n0) - f.c0).norm() >= (m0 - f.c0).norm() ? 1.0 : -1.0; + DimAnnot side; side.kind = DimType::Length; side.ea = f.begin; side.side = outsign; + side.value = measure_dim(side); + Vec2d slbl; + if (draw_dim_quote(side, th, dc, slbl)) m_live_poly_side_label = slbl; + + // Orientation = the angle of the centre->vertex0 spoke from +X (the + // intuitive "which way does the polygon point"), NOT the edge direction. + // Drawn as a wedge OUTSIDE the polygon (radius just past the circumradius) + // so its arc/label clear the fillable face. + const Vec2d sp = m_entities[f.begin].p0 - f.c0; // centre -> vertex0 + const double R = sp.norm(); + if (R > 1e-6) { + double av = std::atan2(sp.y(), sp.x()); + double avdeg = av * 180.0 / M_PI; if (avdeg < 0.0) avdeg += 360.0; + const double rr = R + th * 2.5; // wedge just outside the loop + std::vector> asegs; + asegs.emplace_back(f.c0, f.c0 + Vec2d(rr, 0.0)); // +X leg + asegs.emplace_back(f.c0, f.c0 + Vec2d(std::cos(av), std::sin(av)) * rr); // spoke leg + const int N = 20; Vec2d prev = f.c0 + Vec2d(rr, 0.0); + for (int i = 1; i <= N; ++i) { + const double t = av * double(i) / N; + const Vec2d cur = f.c0 + Vec2d(rr * std::cos(t), rr * std::sin(t)); + asegs.emplace_back(prev, cur); prev = cur; + } + const double mid = av * 0.5; + const Vec2d albl = f.c0 + Vec2d(std::cos(mid), std::sin(mid)) * (rr + th * 1.2); + DimAnnot at; at.kind = DimType::Angle; at.value = avdeg; // "NN.N°" + draw_strokes(m_highlight_model, asegs, 0.6, dc); + draw_text(m_line_model, dim_text(at), albl, th, dc); + m_live_poly_angle_label = albl; + } + m_live_poly_fi = fi; + } + break; + } + case FeatureKind::RoundedRect: { + // Width + Height (box bounds) + fillet Radius, drawn as clickable quotes that + // rebuild the box geometrically (set_rounded_rect). c0=min corner, c1=max, param=r. + const ColorRGBA dc(0.30f, 0.88f, 0.66f, 1.0f); + const double th = std::max(15.0 * unit_per_px, 1e-4); + const double xmin = std::min(f.c0.x(), f.c1.x()), xmax = std::max(f.c0.x(), f.c1.x()); + const double ymin = std::min(f.c0.y(), f.c1.y()), ymax = std::max(f.c0.y(), f.c1.y()); + const double w = xmax - xmin, h = ymax - ymin, r = f.param; + const double off = th * 2.0; + std::vector> segs; + // Width quote below the box. + const double yb = ymin - off; + segs.emplace_back(Vec2d(xmin, ymin), Vec2d(xmin, yb)); + segs.emplace_back(Vec2d(xmax, ymin), Vec2d(xmax, yb)); + segs.emplace_back(Vec2d(xmin, yb), Vec2d(xmax, yb)); + // Height quote left of the box. + const double xl = xmin - off; + segs.emplace_back(Vec2d(xmin, ymin), Vec2d(xl, ymin)); + segs.emplace_back(Vec2d(xmin, ymax), Vec2d(xl, ymax)); + segs.emplace_back(Vec2d(xl, ymin), Vec2d(xl, ymax)); + // Fillet-radius leader from the TR arc centre out to the corner. + const Vec2d rc(xmax - r, ymax - r); + segs.emplace_back(rc, rc + Vec2d(r, r).normalized() * r); + draw_strokes(m_highlight_model, segs, 0.6, dc); + DimAnnot wa; wa.kind = DimType::Length; wa.value = w; + DimAnnot ha; ha.kind = DimType::Length; ha.value = h; + DimAnnot ra; ra.kind = DimType::Radius; ra.value = r; + m_live_rrect_w_label = Vec2d((xmin + xmax) * 0.5, yb - th * 0.8); + m_live_rrect_h_label = Vec2d(xl - th * 0.8, (ymin + ymax) * 0.5); + m_live_rrect_r_label = rc + Vec2d(r, r).normalized() * (r + th * 1.2); + draw_text(m_line_model, dim_text(wa), m_live_rrect_w_label, th, dc); + draw_text(m_line_model, dim_text(ha), m_live_rrect_h_label, th, dc); + draw_text(m_line_model, dim_text(ra), m_live_rrect_r_label, th, dc); + m_live_rrect_fi = fi; + break; + } + case FeatureKind::ArcSlot: { + // Centreline Radius + slot Width quotes. centre=f.c0, centreline start=f.c1, + // half-width=f.param; end direction from the cap@E arc centre (begin+1). + if (f.begin + 1 < int(m_entities.size())) { + const ColorRGBA dc(0.30f, 0.88f, 0.66f, 1.0f); + const double th = std::max(15.0 * unit_per_px, 1e-4); + const Vec2d center = f.c0; + const double Rc = (f.c1 - center).norm(); + const double w = f.param; + Vec2d dirS = (f.c1 - center); + Vec2d dirE = (m_entities[f.begin + 1].center - center); + if (dirS.squaredNorm() > 1e-12 && dirE.squaredNorm() > 1e-12 && Rc > 1e-6) { + dirS.normalize(); dirE.normalize(); + const double aS = std::atan2(dirS.y(), dirS.x()); + double sweep = std::atan2(dirE.y(), dirE.x()) - aS; + while (sweep < 0) sweep += 2.0 * M_PI; + const double aMid = aS + sweep * 0.5; + const Vec2d uMid(std::cos(aMid), std::sin(aMid)); + // Centreline-radius leader: centre -> centreline midpoint. + std::vector> segs; + segs.emplace_back(center, center + uMid * Rc); + // Width tick across the slot at the start cap (outer<->inner). + segs.emplace_back(center + dirS * (Rc + w), center + dirS * (Rc - w)); + draw_strokes(m_highlight_model, segs, 0.6, dc); + DimAnnot ra; ra.kind = DimType::Radius; ra.value = Rc; + DimAnnot wa; wa.kind = DimType::Length; wa.value = 2.0 * w; + m_live_aslot_r_label = center + uMid * (Rc * 0.5) + Vec2d(0, th); + m_live_aslot_w_label = center + dirS * (Rc + w) + dirS * (th * 1.2); + draw_text(m_line_model, dim_text(ra), m_live_aslot_r_label, th, dc); + draw_text(m_line_model, dim_text(wa), m_live_aslot_w_label, th, dc); + m_live_aslot_fi = fi; + } + } + break; + } + default: break; // other features: later chunks + } + } + + if (protos.empty() && m_live_poly_fi < 0 && m_live_rrect_fi < 0 && + m_live_aslot_fi < 0) { // ungrouped single entity + switch (e.type) { + case SketchEntity::Type::Line: { + DimAnnot len; len.kind = DimType::Length; len.ea = ei; len.side = 1.0; protos.push_back(len); + DimAnnot ang; ang.kind = DimType::Angle; ang.ea = ei; ang.eb = -1; protos.push_back(ang); + break; // segment length + angle-to-horizontal + } + case SketchEntity::Type::Circle: { + DimAnnot a; a.kind = DimType::Radius; a.ea = ei; protos.push_back(a); break; + } + case SketchEntity::Type::Arc: { + // Arc radius is its single defining dimension (sweep angles edit via the end + // handles). radius lives in the same .radius field measure_dim/constraint_for + // read, so the Radius promotion path is identical to Circle. + DimAnnot a; a.kind = DimType::Radius; a.ea = ei; protos.push_back(a); break; + } + default: break; // ellipse/bspline: later + } + } + + // Arc sweep-angle wedge: drawn inline (like the polygon orientation) because it is a + // GEOMETRIC edit (SLVS angle constraints are line-to-line). A wedge spans the arc's + // start->end angles just OUTSIDE the radius; its label shows the included angle and is + // clickable to type a new sweep. The radius quote is still emitted via `protos`. + if (m_live_poly_fi < 0 && m_live_rrect_fi < 0 && m_live_aslot_fi < 0 && + e.type == SketchEntity::Type::Arc && e.radius > 1e-6) { + const ColorRGBA dc(0.30f, 0.88f, 0.66f, 1.0f); + const double th = std::max(15.0 * unit_per_px, 1e-4); + const Vec2d c = e.center; + const double a0 = e.start_angle, a1 = e.end_angle; + const double sweep = a1 - a0; // signed (CCW>0); |sweep| shown + double swdeg = std::abs(sweep) * 180.0 / M_PI; + const double rr = e.radius + th * 2.5; // wedge just outside the arc + std::vector> asegs; + asegs.emplace_back(c, c + Vec2d(std::cos(a0), std::sin(a0)) * rr); // start leg + asegs.emplace_back(c, c + Vec2d(std::cos(a1), std::sin(a1)) * rr); // end leg + const int N = 24; Vec2d prev = c + Vec2d(std::cos(a0), std::sin(a0)) * rr; + for (int i = 1; i <= N; ++i) { + const double t = a0 + sweep * double(i) / N; + const Vec2d cur = c + Vec2d(rr * std::cos(t), rr * std::sin(t)); + asegs.emplace_back(prev, cur); prev = cur; + } + const double mid = a0 + sweep * 0.5; + const Vec2d albl = c + Vec2d(std::cos(mid), std::sin(mid)) * (rr + th * 1.2); + DimAnnot at; at.kind = DimType::Angle; at.value = swdeg; // "NN.N°" + draw_strokes(m_highlight_model, asegs, 0.6, dc); + draw_text(m_line_model, dim_text(at), albl, th, dc); + m_live_arc_angle_label = albl; + m_live_arc_ei = ei; + } + + // Ellipse: two clickable axis quotes — semi-major (a) along the major direction and + // semi-minor (b) along the minor. Both edit geometrically (a=e.radius, b=e.rminor); + // phi (orientation) is changed by dragging the major grip, not via a label. + if (m_live_poly_fi < 0 && m_live_rrect_fi < 0 && + (e.type == SketchEntity::Type::Ellipse || e.type == SketchEntity::Type::EllipseArc) && + e.radius > 1e-6 && e.rminor > 1e-6) { + const ColorRGBA dc(0.30f, 0.88f, 0.66f, 1.0f); + const double th = std::max(15.0 * unit_per_px, 1e-4); + const Vec2d c = e.center; + const Vec2d um(std::cos(e.rotation), std::sin(e.rotation)); // major dir + const Vec2d un(-um.y(), um.x()); // minor dir + std::vector> segs; + const Vec2d majEnd = c + um * e.radius, minEnd = c + un * e.rminor; + segs.emplace_back(c, majEnd); + segs.emplace_back(c, minEnd); + draw_strokes(m_highlight_model, segs, 0.6, dc); + DimAnnot ma; ma.kind = DimType::Length; ma.value = e.radius; // plain "NN.N" + DimAnnot mi; mi.kind = DimType::Length; mi.value = e.rminor; + const Vec2d majLbl = c + um * (e.radius * 0.5) + un * (th * 1.0); + const Vec2d minLbl = c + un * (e.rminor * 0.5) + um * (th * 1.0); + draw_text(m_line_model, dim_text(ma), majLbl, th, dc); + draw_text(m_line_model, dim_text(mi), minLbl, th, dc); + m_live_ellipse_major_label = majLbl; + m_live_ellipse_minor_label = minLbl; + m_live_ellipse_ei = ei; + // Elliptical arc also has a SWEEP (included parametric angle), drawn outside the arc + // midpoint; the full ellipse skips this (closed). + if (e.type == SketchEntity::Type::EllipseArc) { + const double midp = 0.5 * (e.start_angle + e.end_angle); + const Vec2d mp = ellipse_point(c, e.radius, e.rminor, e.rotation, midp); + Vec2d outw = mp - c; if (outw.squaredNorm() > 1e-12) outw.normalize(); + m_live_ellipsearc_sweep_label = mp + outw * (th * 1.5); + DimAnnot sw; sw.kind = DimType::Angle; + sw.value = std::abs(e.end_angle - e.start_angle) * 180.0 / M_PI; + draw_text(m_line_model, dim_text(sw), m_live_ellipsearc_sweep_label, th, dc); + } + } + + if (protos.empty()) return; + + const ColorRGBA dimcol(0.30f, 0.88f, 0.66f, 1.0f); + const double th = std::max(15.0 * unit_per_px, 1e-4); + for (DimAnnot a : protos) { + bool driven = false; // skip if already a driving dim of this kind + for (const DimAnnot& d : m_dimensions) + if (d.ea == a.ea && d.kind == a.kind) { driven = true; break; } + if (driven) continue; + a.value = measure_dim(a); + Vec2d label; + if (draw_dim_quote(a, th, dimcol, label)) { + a.label_pos = label; + m_live_quotes.push_back(a); // remember for click-to-promote + } + } +} + +// Iconic constraint badges drawn near each constraint's primary entity (C3.4b). +// Each glyph is authored in a unit cell [-0.5,0.5]^2 then scaled to a constant +// on-screen size and translated to the anchor; badges on the same entity stack +// upward so multiple constraints stay legible. +void DesignSketchTool::build_constraint_glyphs(double unit_per_px, + std::vector>& out) const +{ + if (m_constrain_cons.empty() || m_entities.empty()) return; + using T = SketchConstraintType; + const double s = std::max(11.0 * unit_per_px, 1e-4); // glyph cell size in plane units + const double step = s * 1.5; // vertical stacking step + + // Representative anchor point on an entity (line midpoint, round-entity centre). + auto anchor_of = [&](int ei) -> Vec2d { + if (ei < 0 || ei >= int(m_entities.size())) return Vec2d(0, 0); + const SketchEntity& e = m_entities[ei]; + switch (e.type) { + case SketchEntity::Type::Line: return 0.5 * (e.p0 + e.p1); + case SketchEntity::Type::Circle: + case SketchEntity::Type::Ellipse: + case SketchEntity::Type::Arc: + case SketchEntity::Type::EllipseArc: return e.center; + case SketchEntity::Type::BSpline: return 0.5 * (e.p0 + e.p1); + case SketchEntity::Type::Point: return e.p0; + } + return e.p0; + }; + + // Unit-cell stroke authoring helpers (cell centred on origin). + auto seg = [&](std::vector>& v, Vec2d a, Vec2d b) { v.emplace_back(a, b); }; + auto circ = [&](std::vector>& v, Vec2d c, double r) { + const int n = 12; Vec2d prev(c.x() + r, c.y()); + for (int i = 1; i <= n; ++i) { + const double t = 2.0 * 3.14159265358979 * i / n; + Vec2d cur(c.x() + r * std::cos(t), c.y() + r * std::sin(t)); + v.emplace_back(prev, cur); prev = cur; + } + }; + // Author one glyph type into a unit-cell stroke list. + auto unit_glyph = [&](T type, std::vector>& v) { + switch (type) { + case T::Horizontal: seg(v, {-0.5, 0}, {0.5, 0}); break; + case T::Vertical: seg(v, {0, -0.5}, {0, 0.5}); break; + case T::Parallel: seg(v, {-0.35, -0.5}, {0.0, 0.5}); seg(v, {0.05, -0.5}, {0.4, 0.5}); break; + case T::Perpendicular: seg(v, {-0.4, 0.5}, {-0.4, -0.4}); seg(v, {-0.4, -0.4}, {0.5, -0.4}); break; + case T::Coincident: circ(v, {0, 0}, 0.42); break; + case T::Concentric: circ(v, {0, 0}, 0.5); circ(v, {0, 0}, 0.24); break; + case T::EqualLength:seg(v, {-0.4, 0.16}, {0.4, 0.16}); seg(v, {-0.4, -0.16}, {0.4, -0.16}); break; + case T::Tangent: circ(v, {0, -0.1}, 0.35); seg(v, {-0.5, 0.42}, {0.5, 0.42}); break; + case T::Midpoint: seg(v, {-0.4, 0}, {0.4, 0}); seg(v, {0, -0.18}, {0, 0.18}); break; + case T::Symmetric: seg(v, {0, -0.5}, {0, 0.5}); + seg(v, {-0.5, 0.4}, {-0.15, 0}); seg(v, {-0.5, -0.4}, {-0.15, 0}); + seg(v, {0.5, 0.4}, {0.15, 0}); seg(v, {0.5, -0.4}, {0.15, 0}); break; + case T::Fix: seg(v, {-0.4, -0.4}, {0.4, -0.4}); seg(v, {0.4, -0.4}, {0.4, 0.4}); + seg(v, {0.4, 0.4}, {-0.4, 0.4}); seg(v, {-0.4, 0.4}, {-0.4, -0.4}); break; + case T::Angle: seg(v, {-0.4, -0.4}, {0.4, -0.4}); seg(v, {-0.4, -0.4}, {0.3, 0.4}); break; + case T::Radius: circ(v, {0, 0}, 0.45); seg(v, {0, 0}, {0.45, 0}); break; + case T::Diameter: circ(v, {0, 0}, 0.45); seg(v, {-0.45, 0}, {0.45, 0}); break; + case T::PointOnLine: + case T::PointOnObject: seg(v, {-0.5, -0.3}, {0.5, -0.3}); circ(v, {0, 0.05}, 0.16); break; + case T::Distance: + case T::LockX: + case T::LockY: seg(v, {-0.4, 0}, {0.4, 0}); break; // generic tick + } + }; + + // Stack count per entity so successive badges step upward. + std::vector stack(m_entities.size(), 0); + const Vec2d up(0.0, 1.0); // plane-space up; offset so badge sits off the geometry + for (const SketchEntityConstraintDef& d : m_constrain_cons) { + if (d.ea < 0 || d.ea >= int(m_entities.size())) continue; + const int k = stack[d.ea]++; + const Vec2d center = anchor_of(d.ea) + up * (step * (1.0 + k)); + std::vector> cell; + unit_glyph(d.type, cell); + for (auto& sgp : cell) + out.emplace_back(center + sgp.first * s, center + sgp.second * s); + } +} + +void DesignSketchTool::draw_entities_preview(const std::vector& ents, const ColorRGBA& color) +{ + for (const SketchEntity& e : ents) { + if (e.type == SketchEntity::Type::Point) continue; + bool closed = false; + std::vector poly = entity_polyline(e, closed); + draw_quad_strip(m_highlight_model, poly, closed, color); + } +} + +// ---- In-canvas edit-op gizmo (Fillet/Chamfer/Offset/Mirror toolbar tools) ---------- +// These tools replace the docked numeric card. They operate on the LIVE session's +// m_entities/m_constraints, so they work both while drawing and after begin_edit re-opens +// a committed sketch. The SketchEngine op (context-free) is reused verbatim; the +// constraint binding is ported from DesignPanel::apply_entity_constraint into m_constraints +// via try_add_constraints (append→solve→keep / rollback). + +void DesignSketchTool::reset_op() +{ + m_op_a = m_op_b = -1; + m_op_value = 0.0; + m_op_anchor = Vec2d(0, 0); + m_op_dir = Vec2d(0, 0); + m_op_label = Vec2d(1e18, 1e18); + m_op_ghost.clear(); + m_op_dragging_arrow = false; + m_mirror_targets.clear(); +} + +// ---- Imported-art bounding-box transform gizmo (Mode::TransformArt) ---- + +void DesignSketchTool::reset_xform() +{ + m_xform_base.clear(); + m_xform_feat = -1; + m_xform_handle = -1; + m_xform_offset = Vec2d(0, 0); + m_xform_sx = m_xform_sy = 1.0; + m_xform_min = m_xform_max = Vec2d(0, 0); +} + +void DesignSketchTool::begin_imported_transform( + int feat, const std::vector>>& base_regions, + const SketchPlane& plane, const Vec2d& offset, double sx, double sy) +{ + cancel(); // drop any prior session, clears state + m_plane = plane; + m_mode = Mode::TransformArt; + m_xform_base = base_regions; + m_xform_feat = feat; + m_xform_offset = offset; + m_xform_sx = (std::abs(sx) > 1e-6) ? sx : 1.0; + m_xform_sy = (std::abs(sy) > 1e-6) ? sy : 1.0; + m_xform_handle = -1; + Vec2d mn(1e30, 1e30), mx(-1e30, -1e30); + for (const auto& region : m_xform_base) + for (const auto& contour : region) + for (const Vec2d& p : contour) { + mn.x() = std::min(mn.x(), p.x()); mn.y() = std::min(mn.y(), p.y()); + mx.x() = std::max(mx.x(), p.x()); mx.y() = std::max(mx.y(), p.y()); + } + if (mx.x() < mn.x()) { mn = Vec2d(0, 0); mx = Vec2d(0, 0); } + m_xform_min = mn; m_xform_max = mx; + m_active = true; + m_has_cursor = false; +} + +// 4 bbox corners in plane coords: 0=min/min, 1=max/min, 2=max/max, 3=min/max. The art +// transform is world = base*scale + offset (CadFeature import convention). +void DesignSketchTool::xform_world_corners(Vec2d out[4]) const +{ + const double x0 = m_xform_min.x() * m_xform_sx + m_xform_offset.x(); + const double x1 = m_xform_max.x() * m_xform_sx + m_xform_offset.x(); + const double y0 = m_xform_min.y() * m_xform_sy + m_xform_offset.y(); + const double y1 = m_xform_max.y() * m_xform_sy + m_xform_offset.y(); + out[0] = Vec2d(x0, y0); out[1] = Vec2d(x1, y0); + out[2] = Vec2d(x1, y1); out[3] = Vec2d(x0, y1); +} + +int DesignSketchTool::hit_test_xform_handle(const Vec2d& p, double tol) const +{ + Vec2d c[4]; xform_world_corners(c); + int best = -1; double bd = tol; + for (int i = 0; i < 4; ++i) { const double d = (c[i] - p).norm(); if (d < bd) { bd = d; best = i; } } + if (best >= 0) return best; + if ((0.5 * (c[0] + c[2]) - p).norm() <= tol) return 4; // centre move-handle + return -1; +} + +void DesignSketchTool::drag_xform_handle(const Vec2d& target) +{ + if (m_xform_handle < 0) return; + if (m_xform_handle == 4) { // centre move: translate by cursor delta + m_xform_offset += (target - m_xform_anchor); + m_xform_anchor = target; + emit_xform(); + return; + } + // Corner scale: hold the opposite corner (fixed world anchor O), send the grabbed + // corner to the cursor. base coords of grabbed (bg) and opposite (ba) corners. + auto base_corner = [&](int i) { + return Vec2d((i == 1 || i == 2) ? m_xform_max.x() : m_xform_min.x(), + (i == 2 || i == 3) ? m_xform_max.y() : m_xform_min.y()); + }; + const int h = m_xform_handle; + const Vec2d bg = base_corner(h); + const Vec2d ba = base_corner((h + 2) % 4); + const Vec2d O = m_xform_anchor; + const double dbx = bg.x() - ba.x(), dby = bg.y() - ba.y(); + if (std::abs(dbx) > 1e-9) { + double nsx = (target.x() - O.x()) / dbx; + if (std::abs(nsx) < 1e-4) nsx = (nsx < 0 ? -1e-4 : 1e-4); + m_xform_sx = nsx; + m_xform_offset.x() = O.x() - ba.x() * nsx; + } + if (std::abs(dby) > 1e-9) { + double nsy = (target.y() - O.y()) / dby; + if (std::abs(nsy) < 1e-4) nsy = (nsy < 0 ? -1e-4 : 1e-4); + m_xform_sy = nsy; + m_xform_offset.y() = O.y() - ba.y() * nsy; + } + emit_xform(); +} + +void DesignSketchTool::emit_xform() +{ + if (on_imported_transform) + on_imported_transform(m_xform_feat, m_xform_offset, m_xform_sx, m_xform_sy); +} + +void DesignSketchTool::render_xform_gizmo() +{ + if (m_mode != Mode::TransformArt) return; + Vec2d c[4]; xform_world_corners(c); + const ColorRGBA box(0.30f, 0.88f, 0.66f, 1.0f); + std::vector> segs; + for (int i = 0; i < 4; ++i) segs.emplace_back(c[i], c[(i + 1) % 4]); + draw_strokes(m_highlight_model, segs, 0.6, box); + const Camera& cam = wxGetApp().plater()->get_camera(); + const double hs = 7.0 / std::max(cam.get_zoom(), 1e-6); // screen-constant half-size + const ColorRGBA hcol(0.30f, 0.88f, 0.66f, 1.0f); + const ColorRGBA hhot(1.0f, 0.85f, 0.2f, 1.0f); + auto square = [&](const Vec2d& q, const ColorRGBA& col) { + const std::vector sq = { q + Vec2d(-hs, -hs), q + Vec2d(hs, -hs), + q + Vec2d(hs, hs), q + Vec2d(-hs, hs) }; + draw_fill(m_fill_model, sq, col); + }; + for (int i = 0; i < 4; ++i) square(c[i], m_xform_handle == i ? hhot : hcol); + square(0.5 * (c[0] + c[2]), m_xform_handle == 4 ? hhot : hcol); +} + +bool DesignSketchTool::op_ready() const +{ + switch (m_mode) { + case Mode::Fillet: + case Mode::Chamfer: return m_op_a >= 0 && m_op_b >= 0; + case Mode::Offset: return m_op_a >= 0; + case Mode::Mirror: return m_op_a >= 0 && !m_mirror_targets.empty(); + default: return false; + } +} + +// Corner vertex of two lines + the inward angle bisector (unit), pointing from the vertex +// into the fillet/chamfer interior. Mirrors SketchEngine::fillet_lines's geometry so the +// arrow tracks the op exactly. +bool DesignSketchTool::op_corner(int a, int b, Vec2d& C, Vec2d& bis, double& theta) const +{ + if (a < 0 || b < 0 || a >= int(m_entities.size()) || b >= int(m_entities.size())) return false; + const SketchEntity& ea = m_entities[a]; + const SketchEntity& eb = m_entities[b]; + if (ea.type != SketchEntity::Type::Line || eb.type != SketchEntity::Type::Line) return false; + const Vec2d da = ea.p1 - ea.p0, db = eb.p1 - eb.p0; + const double denom = da.x() * db.y() - da.y() * db.x(); + if (std::abs(denom) < 1e-12) return false; // parallel + const Vec2d diff = eb.p0 - ea.p0; + const double s = (diff.x() * db.y() - diff.y() * db.x()) / denom; + C = ea.p0 + s * da; + Vec2d ua = ((ea.p0 - C).norm() <= (ea.p1 - C).norm()) ? (ea.p1 - C) : (ea.p0 - C); + Vec2d ub = ((eb.p0 - C).norm() <= (eb.p1 - C).norm()) ? (eb.p1 - C) : (eb.p0 - C); + if (ua.norm() < 1e-12 || ub.norm() < 1e-12) return false; + ua.normalize(); ub.normalize(); + theta = std::acos(std::max(-1.0, std::min(1.0, ua.dot(ub)))); + bis = ua + ub; + if (bis.norm() < 1e-12) return false; // 180° corner + bis.normalize(); + return true; +} + +void DesignSketchTool::recompute_op_ghost() +{ + m_op_ghost.clear(); + if (m_mode == Mode::Fillet || m_mode == Mode::Chamfer) { + if (m_op_a < 0 || m_op_b < 0) return; + Vec2d C, bis; double theta; + if (op_corner(m_op_a, m_op_b, C, bis, theta)) { m_op_anchor = C; m_op_dir = bis; } + SketchEntity a_out, b_out, extra; + const bool ok = (m_mode == Mode::Fillet) + ? SketchEngine::fillet_lines(m_entities[m_op_a], m_entities[m_op_b], m_op_value, a_out, b_out, extra) + : SketchEngine::chamfer_lines(m_entities[m_op_a], m_entities[m_op_b], m_op_value, a_out, b_out, extra); + if (ok) m_op_ghost = { a_out, b_out, extra }; + } else if (m_mode == Mode::Offset) { + if (m_op_a < 0) return; + const SketchEntity& e = m_entities[m_op_a]; + if (e.type == SketchEntity::Type::Line) { + m_op_anchor = 0.5 * (e.p0 + e.p1); + Vec2d u = e.p1 - e.p0; if (u.norm() > 1e-12) u.normalize(); + m_op_dir = Vec2d(-u.y(), u.x()); // left normal = +distance side + } else if (e.type == SketchEntity::Type::Circle || e.type == SketchEntity::Type::Arc) { + m_op_anchor = e.center + Vec2d(e.radius, 0.0); + m_op_dir = Vec2d(1, 0); + } + m_op_ghost = SketchEngine::offset_entities({ e }, m_op_value); + } else if (m_mode == Mode::Mirror) { + if (m_op_a < 0 || m_mirror_targets.empty()) return; + const SketchEntity& axis = m_entities[m_op_a]; + std::vector src; + for (int ti : m_mirror_targets) + if (ti >= 0 && ti < int(m_entities.size())) src.push_back(m_entities[ti]); + m_op_ghost = SketchEngine::mirror_entities(src, axis.p0, axis.p1); + } +} + +// Route an entity pick to the active op; sets an initial value + ghost once enough +// entities are picked. Highlights the running picks via m_selection. +void DesignSketchTool::op_pick(int ei) +{ + if (ei < 0 || ei >= int(m_entities.size())) return; + const SketchEntity::Type t = m_entities[ei].type; + switch (m_mode) { + case Mode::Fillet: + case Mode::Chamfer: + if (t != SketchEntity::Type::Line) return; // corner ops need two lines + if (m_op_a < 0) m_op_a = ei; + else if (ei != m_op_a) { + m_op_b = ei; + const double la = (m_entities[m_op_a].p1 - m_entities[m_op_a].p0).norm(); + const double lb = (m_entities[m_op_b].p1 - m_entities[m_op_b].p0).norm(); + m_op_value = std::max(0.001, 0.2 * std::min(la, lb)); // a sensible starting size + recompute_op_ghost(); + } + break; + case Mode::Offset: { + m_op_a = ei; + const SketchEntity& e = m_entities[ei]; + const double sz = (e.type == SketchEntity::Type::Line) ? (e.p1 - e.p0).norm() + : std::max(e.radius * 2.0, 1.0); + m_op_value = std::max(0.001, 0.1 * sz); + recompute_op_ghost(); + break; + } + case Mode::Mirror: + if (m_op_a < 0) { + if (t != SketchEntity::Type::Line) return; // axis must be a line + m_op_a = ei; + } else if (ei != m_op_a) { + auto it = std::find(m_mirror_targets.begin(), m_mirror_targets.end(), ei); + if (it == m_mirror_targets.end()) m_mirror_targets.push_back(ei); + else m_mirror_targets.erase(it); + recompute_op_ghost(); + } + break; + default: break; + } + // Mirror the picks into m_selection so the existing highlight shows them. + m_selection.clear(); + if (m_op_a >= 0) m_selection.push_back(m_op_a); + if (m_op_b >= 0) m_selection.push_back(m_op_b); + for (int ti : m_mirror_targets) m_selection.push_back(ti); + if (on_selection_changed) on_selection_changed(int(m_selection.size())); +} + +bool DesignSketchTool::hit_test_op_arrow(const Vec2d& p, double tol) const +{ + if (!op_ready() || m_mode == Mode::Mirror) return false; + const Vec2d tip = m_op_anchor + m_op_dir * m_op_value; + return point_segment_dist(p, m_op_anchor, tip) <= tol * 1.5; +} + +void DesignSketchTool::drag_op_arrow(const Vec2d& target) +{ + const double v = (target - m_op_anchor).dot(m_op_dir); // project onto the arrow axis + if (m_mode == Mode::Offset) m_op_value = v; // signed: chooses the side + else m_op_value = std::max(0.001, v);// fillet/chamfer: positive + recompute_op_ghost(); +} + +void DesignSketchTool::open_op_editor() +{ + if (!on_inline_edit || !op_ready() || m_mode == Mode::Mirror) return; + const double sign = (m_mode == Mode::Offset && m_op_value < 0) ? -1.0 : 1.0; + const wxPoint px(m_last_mouse_x, m_last_mouse_y); + on_inline_edit(px, std::abs(m_op_value), + [this, sign](double v) { + m_op_value = (m_mode == Mode::Offset) ? sign * std::abs(v) : std::max(0.001, v); + recompute_op_ghost(); + }, + []() {}); +} + +void DesignSketchTool::render_op_gizmo(double unit_per_px) +{ + m_op_label = Vec2d(1e18, 1e18); + if (!op_ready()) return; + const ColorRGBA ghostc(0.30f, 0.88f, 0.66f, 0.55f); + draw_entities_preview(m_op_ghost, ghostc); + if (m_mode == Mode::Mirror) return; // pick-only, no arrow/label + const ColorRGBA dc(0.30f, 0.88f, 0.66f, 1.0f); + const double th = std::max(15.0 * unit_per_px, 1e-4); + const Vec2d dir = (m_op_value >= 0 ? m_op_dir : -m_op_dir); + const Vec2d tip = m_op_anchor + m_op_dir * m_op_value; // signed length picks the side + std::vector> segs; + segs.emplace_back(m_op_anchor, tip); + const double as = std::max(std::abs(m_op_value) * 0.18, th * 0.8); // arrowhead size + const Vec2d nrm(-dir.y(), dir.x()); + const Vec2d back = tip - dir * as; + segs.emplace_back(tip, back + nrm * (as * 0.5)); + segs.emplace_back(tip, back - nrm * (as * 0.5)); + draw_strokes(m_highlight_model, segs, 0.6, dc); + DimAnnot a; + a.kind = (m_mode == Mode::Fillet) ? DimType::Radius : DimType::Distance; + a.value = std::abs(m_op_value); + m_op_label = tip + dir * (th * 1.2); + draw_text(m_line_model, dim_text(a), m_op_label, th, dc); +} + +void DesignSketchTool::confirm_op() +{ + if (!op_ready()) return; + using R = SketchPointRole; + using CT = SketchConstraintType; + + if (m_mode == Mode::Fillet || m_mode == Mode::Chamfer) { + const bool fillet = (m_mode == Mode::Fillet); + SketchEntity a_out, b_out, extra; + const bool ok = fillet + ? SketchEngine::fillet_lines(m_entities[m_op_a], m_entities[m_op_b], m_op_value, a_out, b_out, extra) + : SketchEngine::chamfer_lines(m_entities[m_op_a], m_entities[m_op_b], m_op_value, a_out, b_out, extra); + if (!ok) { reset_op(); return; } + const int a = m_op_a, b = m_op_b; + m_entities[a] = a_out; m_entities[b] = b_out; + const int xi = int(m_entities.size()); + m_entities.push_back(extra); // fillet arc / chamfer segment + auto role_near = [](const SketchEntity& ln, const Vec2d& q) -> R { + return ((ln.p0 - q).squaredNorm() <= (ln.p1 - q).squaredNorm()) ? R::P0 : R::P1; }; + const R ra = role_near(m_entities[a], extra.p0); + const R rb = role_near(m_entities[b], extra.p1); + // Drop the now-stale corner Coincident + each line's own length Distance (the op + // trimmed both legs back), then bind the new entity onto the trimmed endpoints. + auto refs = [](const SketchEntityConstraintDef& d, int e, R r) { + return (d.ea == e && d.ra == r) || (d.eb == e && d.rb == r); }; + auto self_len = [](const SketchEntityConstraintDef& d, int e) { + return d.type == CT::Distance && d.ea == e && d.eb == e; }; + auto& cs = m_constraints; + cs.erase(std::remove_if(cs.begin(), cs.end(), [&](const SketchEntityConstraintDef& d) { + return (d.type == CT::Coincident && refs(d, a, ra) && refs(d, b, rb)) + || self_len(d, a) || self_len(d, b); + }), cs.end()); + auto coin = [&](R xr, int ln, R lr) { + SketchEntityConstraintDef d; d.type = CT::Coincident; d.ea = xi; d.ra = xr; d.eb = ln; d.rb = lr; return d; }; + if (fillet) { + auto tang = [&](int ln) { + SketchEntityConstraintDef d; d.type = CT::Tangent; d.ea = xi; d.eb = ln; return d; }; + const std::vector> ladder = { + { coin(R::P0, a, ra), coin(R::P1, b, rb), tang(a), tang(b) }, + { coin(R::P0, a, ra), coin(R::P1, b, rb), tang(a) }, + { coin(R::P0, a, ra), coin(R::P1, b, rb) }, + }; + for (const auto& set : ladder) if (try_add_constraints(set)) break; + } else { + try_add_constraints({ coin(R::P0, a, ra), coin(R::P1, b, rb) }); + } + } else if (m_mode == Mode::Offset) { + const int a = m_op_a; + auto out = SketchEngine::offset_entities({ m_entities[a] }, m_op_value); + if (out.empty()) { reset_op(); return; } + const int ni = int(m_entities.size()); + for (auto& o : out) m_entities.push_back(o); + const SketchEntity::Type st = m_entities[a].type; + SketchEntityConstraintDef d; d.ea = a; d.eb = ni; + bool emit = true; + if (st == SketchEntity::Type::Line) d.type = CT::Parallel; + else if (st == SketchEntity::Type::Arc || st == SketchEntity::Type::Circle) d.type = CT::Concentric; + else emit = false; + if (emit) try_add_constraints({ d }); + } else if (m_mode == Mode::Mirror) { + const SketchEntity axis = m_entities[m_op_a]; // by value (m_entities grows below) + for (int ti : m_mirror_targets) { + if (ti < 0 || ti >= int(m_entities.size())) continue; + auto out = SketchEngine::mirror_entities({ m_entities[ti] }, axis.p0, axis.p1); + if (out.empty()) continue; + const int mi = int(m_entities.size()); + for (auto& m : out) m_entities.push_back(m); + SketchEntityConstraintDef d; d.type = CT::Symmetric; d.ea = ti; d.eb = mi; d.ec = m_op_a; + const SketchEntity::Type st = m_entities[ti].type; + std::vector cand; + if (st == SketchEntity::Type::Line) { + d.ra = R::P0; d.rb = R::P0; cand.push_back(d); + d.ra = R::P1; d.rb = R::P1; cand.push_back(d); + } else if (st == SketchEntity::Type::Arc || st == SketchEntity::Type::Circle) { + d.ra = R::Center; d.rb = R::Center; cand.push_back(d); + } else if (st == SketchEntity::Type::Point) { + d.ra = R::P0; d.rb = R::P0; cand.push_back(d); + } + if (!cand.empty()) try_add_constraints(cand); + } + } + reset_op(); + m_selection.clear(); + resolve_live(); + if (on_selection_changed) on_selection_changed(0); +} + +// ---- In-canvas transform gizmo (Move/Rotate/Scale/Array/PolarArray) ---- +// These replace the docked numeric cards: pick subject entities in-canvas, then a single +// draggable handle drives the continuous parameter (Move/Array offset, Rotate/Polar angle, +// Scale factor) and an editable value label sets it exactly; Array/PolarArray expose a +// second label for the copy count. A live translucent ghost previews the result. Confirm +// applies the geometry and emits the per-op constraint web (mutating ops drop the classes +// the map invalidates; additive ops bind each copy to its source) into m_constraints. + +void DesignSketchTool::reset_tf() +{ + m_tf_targets.clear(); + m_tf_pivot = Vec2d(0, 0); + m_tf_delta = Vec2d(0, 0); + m_tf_angle = 0.0; + m_tf_scale = 1.0; + m_tf_count = 3; + m_tf_handle_r = 1.0; + m_tf_ghost.clear(); + m_tf_handle = -1; + m_tf_dragging = false; + m_tf_label_a = Vec2d(1e18, 1e18); + m_tf_label_b = Vec2d(1e18, 1e18); +} + +bool DesignSketchTool::tf_ready() const { return !m_tf_targets.empty(); } + +// Centroid of the picked subject set (the rotate/scale/polar pivot, and the array origin), +// plus a reference radius (max distance from the pivot to any subject extremum) used to +// size the rotate/polar handle ring and the scale handle's unit position. +void DesignSketchTool::compute_tf_pivot() +{ + using T = SketchEntity::Type; + auto cen = [](const SketchEntity& e) -> Vec2d { + switch (e.type) { + case T::Line: return 0.5 * (e.p0 + e.p1); + case T::Arc: case T::Circle: case T::Ellipse: case T::EllipseArc: return e.center; + case T::BSpline: + if (!e.ctrl.empty()) { Vec2d s(0, 0); for (const auto& p : e.ctrl) s += p; return s / double(e.ctrl.size()); } + return 0.5 * (e.p0 + e.p1); + default: return e.p0; + } + }; + Vec2d c(0, 0); int n = 0; + for (int ti : m_tf_targets) + if (ti >= 0 && ti < int(m_entities.size())) { c += cen(m_entities[ti]); ++n; } + if (n == 0) { m_tf_pivot = Vec2d(0, 0); m_tf_handle_r = 1.0; return; } + m_tf_pivot = c / double(n); + double r = 0.0; + for (int ti : m_tf_targets) { + if (ti < 0 || ti >= int(m_entities.size())) continue; + const SketchEntity& e = m_entities[ti]; + auto upd = [&](const Vec2d& p) { r = std::max(r, (p - m_tf_pivot).norm()); }; + switch (e.type) { + case T::Line: upd(e.p0); upd(e.p1); break; + case T::Arc: case T::Circle: case T::Ellipse: case T::EllipseArc: + upd(e.center + Vec2d(e.radius, 0)); upd(e.center - Vec2d(e.radius, 0)); break; + case T::BSpline: for (const auto& p : e.ctrl) upd(p); break; + default: upd(e.p0); break; + } + } + m_tf_handle_r = std::max(r, 1.0); +} + +void DesignSketchTool::tf_pick(int ei) +{ + if (ei < 0 || ei >= int(m_entities.size())) return; + auto it = std::find(m_tf_targets.begin(), m_tf_targets.end(), ei); + if (it == m_tf_targets.end()) m_tf_targets.push_back(ei); // toggle-select like Mirror + else m_tf_targets.erase(it); + compute_tf_pivot(); + // Seed sensible starting parameters (mirrors the retired card defaults so the ghost is + // immediately visible). Only seed while still at the neutral value, so re-picking more + // targets keeps a value the user already dialled in. + if (!m_tf_targets.empty()) { + const double step = std::max(m_tf_handle_r * 1.5, 1.0); + switch (m_mode) { + case Mode::Move: + if (m_tf_delta.norm() < 1e-9) m_tf_delta = Vec2d(step, 0.0); + break; + case Mode::Array: + if (m_tf_delta.norm() < 1e-9) { + Vec2d d(step, 0.0); // default: perpendicular to a single line, else +X + if (m_tf_targets.size() == 1) { + const SketchEntity& e = m_entities[m_tf_targets[0]]; + if (e.type == SketchEntity::Type::Line) { + Vec2d t = e.p1 - e.p0; + if (t.norm() > 1e-9) { t.normalize(); d = Vec2d(-t.y(), t.x()) * step; } + } + } + m_tf_delta = d; + } + break; + case Mode::Rotate: if (std::abs(m_tf_angle) < 1e-9) m_tf_angle = M_PI / 4.0; break; // 45° + case Mode::PolarArray: if (std::abs(m_tf_angle) < 1e-9) m_tf_angle = 2.0 * M_PI; break; // 360° + case Mode::Scale: if (std::abs(m_tf_scale - 1.0) < 1e-9) m_tf_scale = 2.0; break; + default: break; + } + } + recompute_tf_ghost(); + m_selection = m_tf_targets; // reuse the existing selection highlight + if (on_selection_changed) on_selection_changed(int(m_selection.size())); +} + +void DesignSketchTool::recompute_tf_ghost() +{ + m_tf_ghost.clear(); + if (m_tf_targets.empty()) return; + std::vector src; + for (int ti : m_tf_targets) + if (ti >= 0 && ti < int(m_entities.size())) src.push_back(m_entities[ti]); + if (src.empty()) return; + const int count = std::max(2, m_tf_count); + switch (m_mode) { + case Mode::Move: + m_tf_ghost = SketchEngine::transform_entities(src, m_tf_delta, 0.0, 1.0, Vec2d(0, 0)); + break; + case Mode::Rotate: + m_tf_ghost = SketchEngine::transform_entities(src, Vec2d(0, 0), m_tf_angle, 1.0, m_tf_pivot); + break; + case Mode::Scale: + m_tf_ghost = SketchEngine::transform_entities(src, Vec2d(0, 0), 0.0, m_tf_scale, m_tf_pivot); + break; + case Mode::Array: + m_tf_ghost = SketchEngine::array_entities(src, count, m_tf_delta, 0.0, m_tf_pivot); + break; + case Mode::PolarArray: + m_tf_ghost = SketchEngine::array_entities(src, count, Vec2d(0, 0), m_tf_angle / double(count), m_tf_pivot); + break; + default: break; + } +} + +// World position of the single drag handle: at the translated/spacing tip for the linear +// ops, on the pivot-centred ring at the current angle for the rotational ops, and at the +// scaled unit position along +X for Scale. +Vec2d DesignSketchTool::tf_handle_pos() const +{ + switch (m_mode) { + case Mode::Move: + case Mode::Array: return m_tf_pivot + m_tf_delta; + case Mode::Rotate: + case Mode::PolarArray: return m_tf_pivot + m_tf_handle_r * Vec2d(std::cos(m_tf_angle), std::sin(m_tf_angle)); + case Mode::Scale: return m_tf_pivot + Vec2d(m_tf_scale * m_tf_handle_r, 0.0); + default: return m_tf_pivot; + } +} + +bool DesignSketchTool::hit_test_tf_handle(const Vec2d& p, double tol) const +{ + if (!tf_ready()) return false; + return (tf_handle_pos() - p).norm() <= tol * 2.5; +} + +void DesignSketchTool::drag_tf_handle(const Vec2d& target) +{ + switch (m_mode) { + case Mode::Move: + case Mode::Array: + m_tf_delta = target - m_tf_pivot; + break; + case Mode::Rotate: + case Mode::PolarArray: { + const Vec2d d = target - m_tf_pivot; + if (d.norm() > 1e-9) m_tf_angle = std::atan2(d.y(), d.x()); + break; + } + case Mode::Scale: { + const double r = (target - m_tf_pivot).norm(); + m_tf_scale = std::max(1e-3, r / std::max(m_tf_handle_r, 1e-9)); + break; + } + default: break; + } + recompute_tf_ghost(); +} + +void DesignSketchTool::open_tf_editor_a() +{ + if (!on_inline_edit || !tf_ready()) return; + const wxPoint px(m_last_mouse_x, m_last_mouse_y); + double cur; + if (m_mode == Mode::Rotate || m_mode == Mode::PolarArray) cur = std::abs(m_tf_angle) * 180.0 / M_PI; + else if (m_mode == Mode::Scale) cur = m_tf_scale; + else cur = m_tf_delta.norm(); + Vec2d dir = m_tf_delta; if (dir.norm() > 1e-9) dir.normalize(); else dir = Vec2d(1, 0); + const double sgn = (m_tf_angle < 0) ? -1.0 : 1.0; + on_inline_edit(px, cur, + [this, dir, sgn](double v) { + switch (m_mode) { + case Mode::Move: case Mode::Array: m_tf_delta = dir * v; break; + case Mode::Rotate: case Mode::PolarArray: m_tf_angle = sgn * std::abs(v) * M_PI / 180.0; break; + case Mode::Scale: m_tf_scale = std::max(1e-3, v); break; + default: break; + } + recompute_tf_ghost(); + }, + []() {}); +} + +void DesignSketchTool::open_tf_editor_count() +{ + if (!on_inline_edit || !tf_ready()) return; + if (m_mode != Mode::Array && m_mode != Mode::PolarArray) return; + const wxPoint px(m_last_mouse_x, m_last_mouse_y); + on_inline_edit(px, double(std::max(2, m_tf_count)), + [this](double v) { m_tf_count = std::max(2, int(v + 0.5)); recompute_tf_ghost(); }, + []() {}); +} + +void DesignSketchTool::render_tf_gizmo(double unit_per_px) +{ + m_tf_label_a = Vec2d(1e18, 1e18); + m_tf_label_b = Vec2d(1e18, 1e18); + if (!tf_ready()) return; + const ColorRGBA ghostc(0.30f, 0.88f, 0.66f, 0.55f); + draw_entities_preview(m_tf_ghost, ghostc); + + const ColorRGBA dc(0.30f, 0.88f, 0.66f, 1.0f); + const ColorRGBA hot(1.0f, 0.85f, 0.2f, 1.0f); + const double th = std::max(15.0 * unit_per_px, 1e-4); + const Vec2d handle = tf_handle_pos(); + + // spoke from the pivot to the handle (+ arrowhead for the linear ops). + std::vector> segs; + segs.emplace_back(m_tf_pivot, handle); + if (m_mode == Mode::Move || m_mode == Mode::Array || m_mode == Mode::Scale) { + Vec2d dir = handle - m_tf_pivot; const double L = dir.norm(); + if (L > 1e-9) { + dir /= L; + const double as = std::max(L * 0.15, th * 0.8); + const Vec2d nrm(-dir.y(), dir.x()); + const Vec2d back = handle - dir * as; + segs.emplace_back(handle, back + nrm * (as * 0.5)); + segs.emplace_back(handle, back - nrm * (as * 0.5)); + } + } + draw_strokes(m_highlight_model, segs, 0.6, dc); + + const double hs = 6.0 * unit_per_px; // screen-constant handle marker + const std::vector sq = { handle + Vec2d(-hs, -hs), handle + Vec2d(hs, -hs), + handle + Vec2d(hs, hs), handle + Vec2d(-hs, hs) }; + draw_fill(m_fill_model, sq, m_tf_dragging ? hot : dc); + draw_vertices(m_vertex_model, { m_tf_pivot }, dc, std::max(3.0 * unit_per_px, 1e-4)); + + // primary parameter label at the handle. + std::string txt_a; + if (m_mode == Mode::Rotate || m_mode == Mode::PolarArray) { + DimAnnot a; a.kind = DimType::Angle; a.value = std::abs(m_tf_angle) * 180.0 / M_PI; + txt_a = dim_text(a); + } else if (m_mode == Mode::Scale) { + char b[24]; std::snprintf(b, sizeof(b), "x%.2f", m_tf_scale); + for (char& ch : b) if (ch == ',') ch = '.'; + txt_a = b; + } else { + DimAnnot a; a.kind = DimType::Distance; a.value = m_tf_delta.norm(); + txt_a = dim_text(a); + } + Vec2d outw = handle - m_tf_pivot; if (outw.norm() > 1e-9) outw.normalize(); else outw = Vec2d(1, 0); + m_tf_label_a = handle + outw * (th * 1.2); + draw_text(m_line_model, txt_a, m_tf_label_a, th, dc); + + if (m_mode == Mode::Array || m_mode == Mode::PolarArray) { + char cb[16]; std::snprintf(cb, sizeof(cb), "x%d", std::max(2, m_tf_count)); + m_tf_label_b = m_tf_pivot + Vec2d(th * 1.5, th * 1.5); + draw_text(m_line_model, cb, m_tf_label_b, th, dc); + } +} + +void DesignSketchTool::confirm_transform() +{ + if (!tf_ready()) { reset_tf(); return; } + using CT = SketchConstraintType; + const std::vector targets = m_tf_targets; // snapshot by value (m_entities grows) + auto is_target = [&](int e) { + return e >= 0 && std::find(targets.begin(), targets.end(), e) != targets.end(); + }; + + if (m_mode == Mode::Move || m_mode == Mode::Rotate || m_mode == Mode::Scale) { + // MUTATING: map every subject in place, then drop the constraint classes the map + // invalidates for any constraint touching a subject. Surviving classes are + // satisfied by construction; the re-solve folds in the new placement. + const Mode mode = m_mode; + for (int ti : targets) { + if (ti < 0 || ti >= int(m_entities.size())) continue; + std::vector out; + if (mode == Mode::Move) + out = SketchEngine::transform_entities({ m_entities[ti] }, m_tf_delta, 0.0, 1.0, Vec2d(0, 0)); + else if (mode == Mode::Rotate) + out = SketchEngine::transform_entities({ m_entities[ti] }, Vec2d(0, 0), m_tf_angle, 1.0, m_tf_pivot); + else + out = SketchEngine::transform_entities({ m_entities[ti] }, Vec2d(0, 0), 0.0, m_tf_scale, m_tf_pivot); + if (!out.empty()) m_entities[ti] = out[0]; + } + auto& cs = m_constraints; + cs.erase(std::remove_if(cs.begin(), cs.end(), [&](const SketchEntityConstraintDef& d) { + if (!(is_target(d.ea) || is_target(d.eb) || is_target(d.ec))) return false; + const bool self = (d.ea == d.eb); // self-length Distance survives translate/rotate + if (mode == Mode::Move) { + switch (d.type) { + case CT::Coincident: case CT::PointOnLine: case CT::PointOnObject: + case CT::Concentric: case CT::Symmetric: case CT::Midpoint: + case CT::Fix: case CT::LockX: case CT::LockY: return true; + case CT::Distance: return !self; + default: return false; // orientation/length preserved by translation + } + } else if (mode == Mode::Rotate) { + switch (d.type) { + case CT::EqualLength: case CT::Radius: case CT::Diameter: return false; + case CT::Distance: return !self; + default: return true; // orientation + position broken by rotation + } + } else { // Scale (uniform / conformal) + switch (d.type) { + case CT::Horizontal: case CT::Vertical: case CT::Parallel: + case CT::Perpendicular: case CT::Angle: return false; + default: return true; // size + position broken + } + } + }), cs.end()); + } else if (m_mode == Mode::Array || m_mode == Mode::PolarArray) { + // ADDITIVE: append copies of each subject, then bind each copy to its source. Lines + // get Parallel+EqualLength (linear) or EqualLength only (polar — rotation breaks + // Parallel); arc/circle copies get a per-copy Radius (equal-radius under any rigid + // map) plus Concentric when the polar pivot is the source's own centre. Emit each + // web as a degrade ladder (try_add_constraints keeps the first set the solver + // accepts, else the geometry stays unconstrained). + const bool polar = (m_mode == Mode::PolarArray); + const int count = std::max(2, m_tf_count); + for (int ti : targets) { + if (ti < 0 || ti >= int(m_entities.size())) continue; + const SketchEntity src = m_entities[ti]; // by value (m_entities grows below) + std::vector copies = polar + ? SketchEngine::array_entities({ src }, count, Vec2d(0, 0), m_tf_angle / double(count), m_tf_pivot) + : SketchEngine::array_entities({ src }, count, m_tf_delta, 0.0, m_tf_pivot); + if (copies.empty()) continue; + const int base = int(m_entities.size()); + for (auto& c : copies) m_entities.push_back(c); + const int nc = int(copies.size()); + const SketchEntity::Type st = src.type; + std::vector> ladder; + if (st == SketchEntity::Type::Line) { + auto mk = [&](bool eq, bool par) { + std::vector w; + for (int k = 0; k < nc; ++k) { + const int ci = base + k; + if (par) { SketchEntityConstraintDef dp; dp.type = CT::Parallel; dp.ea = ti; dp.eb = ci; w.push_back(dp); } + if (eq) { SketchEntityConstraintDef de; de.type = CT::EqualLength; de.ea = ti; de.eb = ci; w.push_back(de); } + } + return w; + }; + if (polar) ladder = { mk(true, false) }; + else ladder = { mk(true, true), mk(false, true) }; + } else if (st == SketchEntity::Type::Arc || st == SketchEntity::Type::Circle) { + const bool can_conc = polar && (m_tf_pivot - src.center).norm() < 1e-6; + auto mk = [&](bool conc) { + std::vector w; + for (int k = 0; k < nc; ++k) { + const int ci = base + k; + SketchEntityConstraintDef dr; dr.type = CT::Radius; dr.ea = ci; dr.value = src.radius; w.push_back(dr); + if (conc) { + SketchEntityConstraintDef dco; dco.type = CT::Concentric; + dco.ea = ti; dco.ra = SketchPointRole::Center; dco.eb = ci; dco.rb = SketchPointRole::Center; + w.push_back(dco); + } + } + return w; + }; + ladder = can_conc ? std::vector>{ mk(true), mk(false) } + : std::vector>{ mk(false) }; + } + for (auto& w : ladder) if (!w.empty() && try_add_constraints(w)) break; + } + } + reset_tf(); + m_selection.clear(); + resolve_live(); + if (on_selection_changed) on_selection_changed(0); +} + +void DesignSketchTool::render(GLCanvas3D& canvas) +{ + (void)canvas; + if (!has_display()) { + if (on_readout) on_readout(std::string()); // nothing to show -> hide HUD + return; + } + if (m_active && m_mode != Mode::Constrain && m_entities.empty() && m_points.empty() + && m_display_sketches.empty()) { + if (on_readout) on_readout(std::string()); + return; + } + + // Draw-then-edit: a creation tool that just committed a new entity/feature (gesture now + // idle) gets its result auto-selected — so render_live_quotes below computes its quotes — + // and the primary value editor armed (opened after those quotes exist, see service block). + if (m_active && is_creation_autoedit_mode() && m_points.empty() && + m_open_feature < 0 && !m_awaiting_length) { + const int n = int(m_entities.size()); + if (m_autoedit_seen >= 0 && n > m_autoedit_seen && n > 0) { + m_selection.clear(); + m_selection.push_back(n - 1); // feature_of(last) groups rect/slot/poly/ellipse + m_autoedit_pending = true; + } + m_autoedit_seen = n; + } + + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + glsafe(::glDisable(GL_DEPTH_TEST)); + glsafe(::glDisable(GL_CULL_FACE)); + shader->start_using(); + const Camera& camera = wxGetApp().plater()->get_camera(); + shader->set_uniform("view_model_matrix", camera.get_view_matrix()); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + + // Persistent committed sketches (e.g. an un-consumed sketch left visible after its + // extrude is removed): faces translucent, outlines orange. Each uses its own plane. + if (!m_display_sketches.empty()) { + const SketchPlane saved_plane = m_plane; + const ColorRGBA dface(0.30f, 0.60f, 1.0f, 0.16f); // normal translucent face + const ColorRGBA sface(0.30f, 0.80f, 1.0f, 0.34f); // click-selected loop: brighter cyan + const ColorRGBA dwire(1.0f, 0.55f, 0.1f, 1.0f); // normal orange outline + const ColorRGBA swire(0.30f, 0.85f, 1.0f, 1.0f); // click-selected loop: cyan outline + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + for (const DisplaySketch& ds : m_display_sketches) { + m_plane = ds.plane; + const std::vector loops = region_loops(ds.entities); + for (int r = 0; r < int(loops.size()); ++r) { + const bool sel = (ds.feature == m_display_pick && r == m_display_pick_region); + draw_fill(m_fill_model, loops[r].poly, sel ? sface : dface); + } + } + glsafe(::glDisable(GL_BLEND)); + for (const DisplaySketch& ds : m_display_sketches) { + m_plane = ds.plane; + // Entities forming the selected loop (highlighted cyan); the rest stay orange. + std::vector sel_ent(ds.entities.size(), 0); + if (ds.feature == m_display_pick && m_display_pick_region >= 0) { + const std::vector loops = region_loops(ds.entities); + if (m_display_pick_region < int(loops.size())) + for (int ei : loops[m_display_pick_region].ents) + if (ei >= 0 && ei < int(sel_ent.size())) sel_ent[ei] = 1; + } + for (int i = 0; i < int(ds.entities.size()); ++i) { + const SketchEntity& e = ds.entities[i]; + if (e.type == SketchEntity::Type::Point) continue; + bool closed = false; + std::vector poly = entity_polyline(e, closed); + draw_quad_strip(m_line_model, poly, closed, sel_ent[i] ? swire : dwire); + } + } + m_plane = saved_plane; + } + + // Nothing else to draw when no live sketch session is active — except the solid + // face/edge highlight overlay (whole-solid tint is handled by set_body_highlight). + if (!m_active) { + render_datum_planes(); + render_solid_highlight(); + if (m_ex_active) render_extrude_gizmo(); + if (m_mv_active) render_move_gizmo(); + if (m_fl_active) render_fillet_gizmo(); + if (m_hl_active) render_hole_gizmo(); + if (m_th_active) render_thread_gizmo(); + if (m_sh_active) render_shell_gizmo(); + if (m_rv_active) render_revolve_gizmo(); + if (m_pt_active) render_pattern_gizmo(); + shader->stop_using(); + glsafe(::glEnable(GL_CULL_FACE)); + glsafe(::glEnable(GL_DEPTH_TEST)); + return; + } + + // Imported-art transform: only the bbox + handles over the (display-overlay) art. + if (m_mode == Mode::TransformArt) { + render_xform_gizmo(); + shader->stop_using(); + glsafe(::glEnable(GL_CULL_FACE)); + glsafe(::glEnable(GL_DEPTH_TEST)); + return; + } + + const ColorRGBA orange(1.0f, 0.55f, 0.1f, 1.0f); + const ColorRGBA yellow(1.0f, 0.85f, 0.2f, 1.0f); + const ColorRGBA grey(0.55f, 0.55f, 0.60f, 1.0f); + + if (m_mode == Mode::Constrain) { + const ColorRGBA cyan(0.30f, 0.80f, 1.0f, 1.0f); + const ColorRGBA red(1.0f, 0.25f, 0.25f, 1.0f); + if (m_constrain_entities) { + // Draw all entities cyan; picked Line entities highlighted red. + std::vector markers; + for (size_t i = 0; i < m_entities.size(); ++i) { + const SketchEntity& e = m_entities[i]; + const bool sel = (int(i) == m_pick0 || int(i) == m_pick1 || int(i) == m_pick2); + // Constraint-manager highlight: the entities a selected constraint + // references glow yellow (picked entities still win as red). + const bool hl = !sel && std::find(m_constraint_hl.begin(), m_constraint_hl.end(), + int(i)) != m_constraint_hl.end(); + const ColorRGBA col = sel ? red : (hl ? yellow : cyan); + if (e.type == SketchEntity::Type::Point) { markers.push_back(e.p0); continue; } + bool closed = false; + std::vector poly = entity_polyline(e, closed); + draw_quad_strip((sel || hl) ? m_highlight_model : m_line_model, poly, closed, col); + } + if (!markers.empty()) + draw_vertices(m_vertex_model, markers, cyan); + // Constraint badges (C3.4b): iconic glyphs near each constraint's entity. + { + const double upp = 1.0 / std::max(camera.get_zoom(), 1e-6); + std::vector> glyphs; + build_constraint_glyphs(upp, glyphs); + if (!glyphs.empty()) { + const ColorRGBA badge(0.45f, 0.95f, 0.70f, 1.0f); // CAD teal-green + draw_strokes(m_fill_model, glyphs, std::max(0.9 * upp, 1e-4), badge); + } + } + shader->stop_using(); + glsafe(::glEnable(GL_CULL_FACE)); + glsafe(::glEnable(GL_DEPTH_TEST)); + return; + } + draw_quad_strip(m_line_model, m_points, true, cyan); + draw_vertices(m_vertex_model, m_points, cyan); + if (m_sel_a >= 0 && m_sel_b >= 0 && + m_sel_a < int(m_points.size()) && m_sel_b < int(m_points.size())) { + std::vector seg = { m_points[m_sel_a], m_points[m_sel_b] }; + draw_quad_strip(m_highlight_model, seg, false, red); + } + shader->stop_using(); + glsafe(::glEnable(GL_CULL_FACE)); + glsafe(::glEnable(GL_DEPTH_TEST)); + return; + } + + // Closed loops fill as translucent faces (the "closed loop = selectable face" + // affordance). Drawn first so the entity outlines paint over the fill. + { + const std::vector> regions = closed_regions(); + if (!regions.empty()) { + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + const ColorRGBA face(0.30f, 0.60f, 1.0f, 0.22f); + for (const std::vector& r : regions) + draw_fill(m_fill_model, r, face); + glsafe(::glDisable(GL_BLEND)); + } + } + + // Committed entities of this session. DoF feedback (P3): a fully-constrained + // sketch (dof==0, consistent) paints green; entities touched by a conflicting + // constraint paint red; otherwise the under-constrained default (orange / grey + // construction). Selected entities always override to white. + const ColorRGBA white(1.0f, 1.0f, 1.0f, 1.0f); + const ColorRGBA green(0.30f, 0.85f, 0.42f, 1.0f); + const ColorRGBA conflict(1.0f, 0.22f, 0.22f, 1.0f); + const ColorRGBA editing(1.0f, 0.78f, 0.10f, 1.0f); // amber: entity whose dim is being typed + const bool fully = (m_dof == 0 && m_solve_ok); + // While an auto-edit value field is open, the active step names the entities its dimension + // drives — light them up so it's obvious WHICH feature the number (e.g. a circle's radius) + // changes. + const std::vector* edit_hi = + (m_autoedit_dim_idx >= 0 && m_autoedit_dim_idx < int(m_autoedit_dims.size())) + ? &m_autoedit_dims[m_autoedit_dim_idx].hi : nullptr; + std::vector point_markers, sel_point_markers; + for (size_t i = 0; i < m_entities.size(); ++i) { + const SketchEntity& e = m_entities[i]; + const bool selected = + std::find(m_selection.begin(), m_selection.end(), int(i)) != m_selection.end(); + const bool editing_this = + edit_hi && std::find(edit_hi->begin(), edit_hi->end(), int(i)) != edit_hi->end(); + const bool bad = i < m_entity_conflict.size() && m_entity_conflict[i]; + ColorRGBA col; + if (editing_this) col = editing; + else if (selected) col = white; + else if (bad) col = conflict; + else if (e.construction) col = grey; + else col = fully ? green : orange; + if (e.type == SketchEntity::Type::Point) { + (selected ? sel_point_markers : point_markers).push_back(e.p0); + continue; + } + bool closed = false; + std::vector poly = entity_polyline(e, closed); + draw_quad_strip((selected || editing_this) ? m_highlight_model : m_line_model, poly, closed, col); + } + if (!point_markers.empty()) + draw_vertices(m_vertex_model, point_markers, yellow); + if (!sel_point_markers.empty()) + draw_vertices(m_highlight_model, sel_point_markers, white); + + // Endpoint / centre handles so individual points are visible and pickable in the + // Select and Dimension tools (a line = a segment + 2 points). Selected ones white. + m_show_handles = (m_mode == Mode::Select || m_mode == Mode::Dimension); + if (m_show_handles) { + std::vector handles, sel_handles; + auto add_h = [&](int ei, SketchPointRole r, const Vec2d& q) { + const bool s = std::find(m_point_sel.begin(), m_point_sel.end(), + std::make_pair(ei, r)) != m_point_sel.end(); + (s ? sel_handles : handles).push_back(q); + }; + for (size_t i = 0; i < m_entities.size(); ++i) { + const SketchEntity& e = m_entities[i]; + switch (e.type) { + case SketchEntity::Type::Line: + add_h(int(i), SketchPointRole::P0, e.p0); + add_h(int(i), SketchPointRole::P1, e.p1); + break; + case SketchEntity::Type::Arc: + case SketchEntity::Type::EllipseArc: + add_h(int(i), SketchPointRole::P0, e.p0); + add_h(int(i), SketchPointRole::P1, e.p1); + add_h(int(i), SketchPointRole::Center, e.center); + break; + case SketchEntity::Type::Circle: + case SketchEntity::Type::Ellipse: + add_h(int(i), SketchPointRole::Center, e.center); + break; + case SketchEntity::Type::BSpline: + add_h(int(i), SketchPointRole::P0, e.p0); + add_h(int(i), SketchPointRole::P1, e.p1); + break; + case SketchEntity::Type::Point: + break; // its own marker is drawn above + } + } + if (!handles.empty()) draw_vertices(m_vertex_model, handles, ColorRGBA(0.65f, 0.65f, 0.30f, 1.0f)); + if (!sel_handles.empty()) draw_vertices(m_highlight_model, sel_handles, white); + + // Derived feature handles (A3): the circle RadiusHandle is not a SketchPointRole, + // so the per-point pass above doesn't draw it. Render it (cyan) + the hovered + // handle (white, larger) at a screen-constant size so they stay grabbable at any + // zoom. A4 makes these draggable; later phases add slot/rect/polygon handles. + const double upp = 1.0 / std::max(camera.get_zoom(), 1e-6); + std::vector radius_h; + for (const Handle& h : build_handles()) + if (h.role == HandleRole::RadiusHandle || h.role == HandleRole::MajorAxis || + h.role == HandleRole::MinorAxis || h.role == HandleRole::BSplineCtrl) + radius_h.push_back(h.pos); + if (!radius_h.empty()) + draw_vertices(m_vertex_model, radius_h, ColorRGBA(0.30f, 0.75f, 0.95f, 1.0f), + std::max(4.0 * upp, 1e-4)); + if (m_has_hover_handle) + draw_vertices(m_highlight_model, { m_hover_handle.pos }, white, + std::max(5.5 * upp, 1e-4)); + } + + // Placed dimension quotes (drawn in every mode so they persist while sketching). + // Pass plane-units-per-pixel so labels keep a constant on-screen size. + render_dimensions(1.0 / std::max(camera.get_zoom(), 1e-6)); + render_live_quotes(1.0 / std::max(camera.get_zoom(), 1e-6)); + // Draw-then-edit: the selection's live quotes now exist. Open the primary value editor on + // the next event-loop tick (NOT here inside the paint) so the floating field grabs focus + // cleanly — the same context the Line path opens from. m_live_quotes persists until the + // next render_live_quotes(), so the deferred open still sees this frame's values. + if (m_autoedit_pending) { + m_autoedit_pending = false; + wxGetApp().CallAfter([this] { open_primary_autoedit(); }); + } + if (is_edit_op_mode()) + render_op_gizmo(1.0 / std::max(camera.get_zoom(), 1e-6)); + if (is_transform_mode()) + render_tf_gizmo(1.0 / std::max(camera.get_zoom(), 1e-6)); + + // In-progress entity preview for the active tool. + const ColorRGBA preview = m_construction ? grey : orange; + switch (m_mode) { + + case Mode::Select: + case Mode::Dimension: + break; // selection highlight / placed quotes are drawn above; no rubber-band + + case Mode::Polyline: { + std::vector pts = m_points; + if (m_has_cursor) + pts.push_back(m_cursor); + draw_quad_strip(m_highlight_model, pts, false, preview); + draw_vertices(m_vertex_model, m_points, yellow); + // Teal rubber-band = the new segment is locked to an inference angle. + if (m_cursor_locked && m_has_cursor && !m_points.empty()) { + const ColorRGBA lock(0.10f, 0.85f, 0.80f, 1.0f); + std::vector seg = { m_points.back(), m_cursor }; + draw_quad_strip(m_line_model, seg, false, lock); + } + break; + } + + case Mode::Line: { + std::vector pts = m_points; + if (m_has_cursor && m_points.size() == 1) + pts.push_back(m_cursor); + if (pts.size() >= 2) { + const ColorRGBA col = (m_cursor_locked && m_points.size() == 1) + ? ColorRGBA(0.10f, 0.85f, 0.80f, 1.0f) : preview; + draw_quad_strip(m_highlight_model, pts, false, col); + } + draw_vertices(m_vertex_model, m_points, yellow); + break; + } + + case Mode::CornerRect: { + if (m_points.size() == 1 && m_has_cursor) { + const Vec2d A = m_points[0]; + const Vec2d B = m_cursor; + std::vector corners = { A, Vec2d(B.x(), A.y()), B, Vec2d(A.x(), B.y()) }; + draw_quad_strip(m_highlight_model, corners, true, preview); + } + break; + } + + case Mode::CenterRect: { + if (m_points.size() == 1 && m_has_cursor) { + const Vec2d C = m_points[0]; + const Vec2d P = m_cursor; + const double hx = std::abs(P.x() - C.x()); + const double hy = std::abs(P.y() - C.y()); + std::vector corners = { + Vec2d(C.x() - hx, C.y() - hy), Vec2d(C.x() + hx, C.y() - hy), + Vec2d(C.x() + hx, C.y() + hy), Vec2d(C.x() - hx, C.y() + hy) }; + draw_quad_strip(m_highlight_model, corners, true, preview); + draw_vertices(m_vertex_model, { C }, yellow); + } + break; + } + + case Mode::ObliqueRect: { + draw_vertices(m_vertex_model, m_points, yellow); + if (m_points.size() == 2 && m_has_cursor) { + const Vec2d A = m_points[0], B = m_points[1]; + Vec2d u = B - A; + if (u.squaredNorm() > 1e-12) { + u.normalize(); + const Vec2d n(-u.y(), u.x()); + const double w = n.dot(m_cursor - A); + draw_quad_strip(m_highlight_model, { A, B, B + n * w, A + n * w }, true, preview); + } + } + break; + } + + case Mode::RoundedRect: { + draw_vertices(m_vertex_model, m_points, yellow); + if (m_points.size() == 1 && m_has_cursor) { + const Vec2d A = m_points[0], B = m_cursor; // box not yet fixed: plain rect + draw_quad_strip(m_highlight_model, { A, Vec2d(B.x(), A.y()), B, Vec2d(A.x(), B.y()) }, true, preview); + } else if (m_points.size() == 2 && m_has_cursor) { + draw_entities_preview(make_rounded_rect(m_points[0], m_points[1], m_cursor), preview); + } + break; + } + + case Mode::CenterCircle: { + if (m_points.size() == 1 && m_has_cursor) { + const Vec2d C = m_points[0]; + const double r = (m_cursor - C).norm(); + draw_quad_strip(m_highlight_model, circle_polygon(C, r), true, preview); + draw_vertices(m_vertex_model, { C }, yellow); + } + break; + } + + case Mode::TwoPointCircle: { + draw_vertices(m_vertex_model, m_points, yellow); + if (m_points.size() == 1 && m_has_cursor) { + const Vec2d C = (m_points[0] + m_cursor) * 0.5; + const double r = (m_cursor - m_points[0]).norm() * 0.5; + draw_quad_strip(m_highlight_model, circle_polygon(C, r), true, preview); + } + break; + } + + case Mode::ThreePointCircle: { + draw_vertices(m_vertex_model, m_points, yellow); + if (m_points.size() == 2 && m_has_cursor) + draw_entities_preview(make_three_point_circle(m_points[0], m_points[1], m_cursor), preview); + break; + } + + case Mode::ThreePointArc: { + draw_vertices(m_vertex_model, m_points, yellow); + if (m_points.size() == 2 && m_has_cursor) + draw_entities_preview(make_three_point_arc(m_points[0], m_points[1], m_cursor), preview); + break; + } + + case Mode::TangentArc: { + draw_vertices(m_vertex_model, m_points, yellow); + if (m_points.size() == 1 && m_has_cursor) + draw_entities_preview(make_tangent_arc(m_points[0], m_cursor), preview); + break; + } + + case Mode::CenterArc: { + draw_vertices(m_vertex_model, m_points, yellow); + if (m_points.size() == 1 && m_has_cursor) { + // center placed: show the radius rubber-band as a faint guide circle + SketchEntity g; g.type = SketchEntity::Type::Circle; g.center = m_points[0]; + g.p0 = m_points[0]; g.radius = (m_cursor - m_points[0]).norm(); g.construction = true; + draw_entities_preview({ g }, preview); + } else if (m_points.size() == 2 && m_has_cursor) { + draw_entities_preview(make_center_arc(m_points[0], m_points[1], m_cursor), preview); + } + break; + } + + case Mode::Slot: { + draw_vertices(m_vertex_model, m_points, yellow); + if (m_points.size() == 1 && m_has_cursor) { + draw_quad_strip(m_highlight_model, { m_points[0], m_cursor }, false, grey); + } else if (m_points.size() == 2 && m_has_cursor) { + Vec2d u = m_points[1] - m_points[0]; + if (u.squaredNorm() > 1e-12) { + u.normalize(); + const Vec2d n(-u.y(), u.x()); + const double w = std::abs(n.dot(m_cursor - m_points[0])); + draw_entities_preview(make_slot(m_points[0], m_points[1], w), preview); + } + } + break; + } + + case Mode::ArcSlot: { + draw_vertices(m_vertex_model, m_points, yellow); + if (m_points.size() == 1 && m_has_cursor) { + // center placed: faint guide circle for the centerline radius + SketchEntity g; g.type = SketchEntity::Type::Circle; g.center = m_points[0]; + g.p0 = m_points[0]; g.radius = (m_cursor - m_points[0]).norm(); g.construction = true; + draw_entities_preview({ g }, preview); + } else if (m_points.size() == 2 && m_has_cursor) { + draw_entities_preview(make_center_arc(m_points[0], m_points[1], m_cursor), preview); // centerline arc + } else if (m_points.size() == 3 && m_has_cursor) { + const double Rc = (m_points[1] - m_points[0]).norm(); + const double w = std::abs((m_cursor - m_points[0]).norm() - Rc); + draw_entities_preview(make_arc_slot(m_points[0], m_points[1], m_points[2], w), preview); + } + break; + } + + case Mode::Polygon: { + draw_vertices(m_vertex_model, m_points, yellow); + if (m_points.size() == 1 && m_has_cursor) + draw_entities_preview(make_polygon(m_points[0], m_cursor, m_polygon_sides), preview); + break; + } + + case Mode::Ellipse: { + draw_vertices(m_vertex_model, m_points, yellow); + if (m_points.size() == 1 && m_has_cursor) { + SketchEntity g; g.type = SketchEntity::Type::Line; + g.p0 = m_points[0]; g.p1 = m_cursor; g.construction = true; // major-axis rubber band + draw_entities_preview({ g }, preview); + } else if (m_points.size() == 2 && m_has_cursor) { + draw_entities_preview(make_ellipse(m_points[0], m_points[1], m_cursor), preview); + } + break; + } + + case Mode::EllipseArc: { + draw_vertices(m_vertex_model, m_points, yellow); + if (m_points.size() == 1 && m_has_cursor) { + SketchEntity g; g.type = SketchEntity::Type::Line; + g.p0 = m_points[0]; g.p1 = m_cursor; g.construction = true; + draw_entities_preview({ g }, preview); + } else if (m_points.size() == 2 && m_has_cursor) { + draw_entities_preview(make_ellipse(m_points[0], m_points[1], m_cursor), preview); + } else if (m_points.size() == 3 && m_has_cursor) { + std::vector full = make_ellipse(m_points[0], m_points[1], m_points[2]); + for (auto& e : full) e.construction = true; // faint full ellipse + draw_entities_preview(full, preview); + } else if (m_points.size() == 4 && m_has_cursor) { + draw_entities_preview(make_ellipse_arc(m_points[0], m_points[1], m_points[2], + m_points[3], m_cursor), preview); + } + break; + } + + case Mode::BSpline: { + std::vector poles = m_points; + if (m_has_cursor) poles.push_back(m_cursor); + // Faint control polygon as a placement guide. + if (poles.size() >= 2) { + std::vector guide; + for (size_t i = 1; i < poles.size(); ++i) { + SketchEntity g; g.type = SketchEntity::Type::Line; + g.p0 = poles[i - 1]; g.p1 = poles[i]; g.construction = true; + guide.push_back(g); + } + draw_entities_preview(guide, grey); + } + // The spline curve itself. + if (poles.size() >= 2) + draw_quad_strip(m_highlight_model, bspline_polyline(poles), false, preview); + draw_vertices(m_vertex_model, m_points, yellow); + break; + } + + case Mode::Trim: + case Mode::Extend: { + // Hover preview: paint the exact sub-portion a click would cut (Trim) / add (Extend) + // in red, recomputed from the cursor every frame (never persisted). The pick tolerance + // matches on_mouse: ~8 px projected to plane units, x3 (here via zoom -> units/px). + if (m_has_cursor) { + const double upp = 1.0 / std::max(camera.get_zoom(), 1e-6); + int subj = -1; + std::vector removed; + const ColorRGBA cut(1.0f, 0.2f, 0.2f, 1.0f); + if (compute_trim_preview(m_cursor, 24.0 * upp, m_mode == Mode::Extend, subj, removed)) + draw_quad_strip(m_highlight_model, removed, false, cut); + } + break; + } + + case Mode::Point: + case Mode::Constrain: + break; + } + + // Inference hint: highlight the snapped target under the cursor (C1.3). Colour + // encodes what the placed point will be Coincident/PointOnObject/Fixed onto. + if (m_has_cursor && m_mode != Mode::Constrain && m_cursor_snap.snapped()) { + ColorRGBA hint(1.0f, 0.55f, 0.1f, 1.0f); // endpoint/midpoint: orange + switch (m_cursor_snap.kind) { + case InferenceSnap::Kind::Center: hint = ColorRGBA(0.30f, 0.80f, 1.0f, 1.0f); break; // cyan + case InferenceSnap::Kind::Origin: hint = ColorRGBA(1.0f, 0.30f, 0.85f, 1.0f); break; // magenta + case InferenceSnap::Kind::OnEdge: hint = ColorRGBA(0.45f, 0.70f, 1.0f, 1.0f); break; // blue + default: break; + } + draw_vertices(m_highlight_model, { m_cursor_snap.point }, hint); + } + + shader->stop_using(); + glsafe(::glEnable(GL_CULL_FACE)); + glsafe(::glEnable(GL_DEPTH_TEST)); + + if (on_readout) on_readout(build_readout()); // bottom-right viewport HUD +} + +// Compact "current values" for the bottom-right HUD: the live segment being drawn (length +// + bearing) takes priority; otherwise the selected entity's characteristic quotes (the +// same values render_live_quotes just drew on the geometry). +std::string DesignSketchTool::build_readout() const +{ + auto en = [](char* b) { for (char* c = b; *c; ++c) if (*c == ',') *c = '.'; }; + if (m_active && (m_mode == Mode::Line || m_mode == Mode::Polyline) && + !m_points.empty() && m_has_cursor) { + const Vec2d d = m_cursor - m_points.back(); + double ang = std::atan2(d.y(), d.x()) * 180.0 / M_PI; if (ang < 0.0) ang += 360.0; + char b[80]; std::snprintf(b, sizeof(b), "L %.2f mm %.1f\xC2\xB0", d.norm(), ang); + en(b); + std::string out = b; + // Tell the user how to end a polyline chain — there's no other affordance for it. + if (m_mode == Mode::Polyline && m_points.size() >= 2) + out += " right-click or double-click to finish, click start to close"; + return out; + } + if (!m_active) return std::string(); + std::string out; + for (const DimAnnot& q : m_live_quotes) { // selection's Length/Radius/Width/… quotes + if (!out.empty()) out += " "; + out += dim_text(q); + } + return out; +} + +// Distance from point p to the segment [a,b] in plane (2D) coordinates. +static double point_segment_dist(const Vec2d& p, const Vec2d& a, const Vec2d& b) +{ + const Vec2d ab = b - a; + const double len2 = ab.squaredNorm(); + if (len2 < 1e-12) + return (p - a).norm(); + double t = (p - a).dot(ab) / len2; + t = std::max(0.0, std::min(1.0, t)); + return (p - (a + t * ab)).norm(); +} + +// Even-odd point-in-polygon test (plane coords), for picking a closed-loop interior. +static bool point_in_poly(const Vec2d& q, const std::vector& poly) +{ + if (poly.size() < 3) return false; + bool in = false; + for (size_t i = 0, j = poly.size() - 1; i < poly.size(); j = i++) { + const Vec2d& a = poly[i]; + const Vec2d& b = poly[j]; + if (((a.y() > q.y()) != (b.y() > q.y())) && + (q.x() < (b.x() - a.x()) * (q.y() - a.y()) / (b.y() - a.y()) + a.x())) + in = !in; + } + return in; +} + +// Möller–Trumbore ray/triangle intersection in 3D world space. ro=ray origin, rd=ray dir +// (not necessarily unit). Returns true + the ray parameter t (>0) of the hit. +static bool ray_triangle(const Vec3d& ro, const Vec3d& rd, + const Vec3d& v0, const Vec3d& v1, const Vec3d& v2, double& t) +{ + const Vec3d e1 = v1 - v0, e2 = v2 - v0; + const Vec3d p = rd.cross(e2); + const double det = e1.dot(p); + if (std::abs(det) < 1e-12) return false; // parallel + const double inv = 1.0 / det; + const Vec3d s = ro - v0; + const double u = s.dot(p) * inv; + if (u < -1e-9 || u > 1.0 + 1e-9) return false; + const Vec3d q = s.cross(e1); + const double v = rd.dot(q) * inv; + if (v < -1e-9 || u + v > 1.0 + 1e-9) return false; + t = e2.dot(q) * inv; + return t > 1e-9; +} + +// Shortest distance between an infinite ray (ro+rd) and a 3D segment [a,b]. +static double ray_segment_dist3(const Vec3d& ro, const Vec3d& rd, const Vec3d& a, const Vec3d& b) +{ + const Vec3d d1 = rd, d2 = b - a, r = ro - a; + const double A = d1.dot(d1), B = d1.dot(d2), C = d2.dot(d2), D = d1.dot(r), E = d2.dot(r); + const double denom = A * C - B * B; + double s = (std::abs(denom) > 1e-12) ? (A * E - B * D) / denom : 0.0; // param on segment + s = std::max(0.0, std::min(1.0, s)); + double tt = (B * s - D) / std::max(A, 1e-12); // param on ray + tt = std::max(0.0, tt); // ray is forward-only + const Vec3d pr = ro + tt * d1, ps = a + s * d2; + return (pr - ps).norm(); +} + +// Screen-plane distance from p to a sketch entity, for click picking in Constrain +// mode. Circles/arcs measure distance to the ring; points to their position. +static double entity_pick_dist(const Vec2d& p, const SketchEntity& e) +{ + switch (e.type) { + case SketchEntity::Type::Line: return point_segment_dist(p, e.p0, e.p1); + case SketchEntity::Type::Point: return (p - e.p0).norm(); + case SketchEntity::Type::Circle: + case SketchEntity::Type::Arc: return std::abs((p - e.center).norm() - e.radius); + case SketchEntity::Type::Ellipse: + case SketchEntity::Type::EllipseArc: { + // Accurate edge pick: sample the true ellipse outline as a polyline and take the + // min segment distance. The crude mean-radius circle mis-picks eccentric ellipses + // (the outline at the major/minor extremes is far from that circle), which made the + // face-fill swallow edge clicks. Full ellipse sweeps 0..2pi; an arc its param range. + const double cu = std::cos(e.rotation), su = std::sin(e.rotation); + const bool full = (e.type == SketchEntity::Type::Ellipse); + const double t0 = full ? 0.0 : e.start_angle; + const double t1 = full ? 2.0 * M_PI : e.end_angle; + const int n = 48; + double best = 1e30; Vec2d prev; + for (int i = 0; i <= n; ++i) { + const double t = t0 + (t1 - t0) * double(i) / n; + const double lx = e.radius * std::cos(t), ly = e.rminor * std::sin(t); // local + const Vec2d q(e.center.x() + lx * cu - ly * su, + e.center.y() + lx * su + ly * cu); // world + if (i > 0) best = std::min(best, point_segment_dist(p, prev, q)); + prev = q; + } + return best; + } + case SketchEntity::Type::BSpline: { + const std::vector poly = bspline_polyline(e.ctrl); + double best = 1e30; + for (size_t i = 1; i < poly.size(); ++i) + best = std::min(best, point_segment_dist(p, poly[i - 1], poly[i])); + return best; + } + } + return 1e30; +} + +// Adjacency endpoints for loop walking (Circles/Points have none -> stand-alone). +static void entity_endpoints(const SketchEntity& e, std::vector& out) +{ + out.clear(); + if (e.type == SketchEntity::Type::Line) { + out.push_back(e.p0); + out.push_back(e.p1); + } else if (e.type == SketchEntity::Type::Arc) { + out.push_back(e.center + e.radius * Vec2d(std::cos(e.start_angle), std::sin(e.start_angle))); + out.push_back(e.center + e.radius * Vec2d(std::cos(e.end_angle), std::sin(e.end_angle))); + } +} + +// Reference point for positioning ops: a Point's position, or a Circle/Arc centre. +// Lines have no single reference point (return false). +static bool entity_ref_point(const SketchEntity& e, Vec2d& out) +{ + switch (e.type) { + case SketchEntity::Type::Point: out = e.p0; return true; + case SketchEntity::Type::Circle: + case SketchEntity::Type::Arc: + case SketchEntity::Type::Ellipse: + case SketchEntity::Type::EllipseArc: out = e.center; return true; + default: return false; // Line + } +} + +// Rigidly translate a whole entity (keeps size/shape). +static void translate_entity(SketchEntity& e, const Vec2d& d) +{ + e.p0 += d; + e.p1 += d; + e.center += d; + for (auto& cp : e.ctrl) cp += d; // BSpline poles +} + +int DesignSketchTool::hit_test(const Vec2d& p, double tol) const +{ + double best = tol; + int bi = -1; + for (size_t i = 0; i < m_entities.size(); ++i) { + const double d = entity_pick_dist(p, m_entities[i]); + if (d < best) { best = d; bi = int(i); } + } + return bi; +} + +std::vector DesignSketchTool::connected_loop(int seed) const +{ + std::vector out; + if (seed < 0 || seed >= int(m_entities.size())) return out; + const double eps2 = 1e-6; + std::vector vis(m_entities.size(), false); + std::vector stack = { seed }; + vis[seed] = true; + while (!stack.empty()) { + const int cur = stack.back(); + stack.pop_back(); + out.push_back(cur); + std::vector ce; + entity_endpoints(m_entities[cur], ce); + if (ce.empty()) continue; // circle/point: not part of a chain + for (size_t j = 0; j < m_entities.size(); ++j) { + if (vis[j]) continue; + std::vector je; + entity_endpoints(m_entities[j], je); + if (je.empty()) continue; + bool adj = false; + for (const Vec2d& a : ce) + for (const Vec2d& b : je) + if ((a - b).squaredNorm() < eps2) { adj = true; break; } + if (adj) { vis[j] = true; stack.push_back(int(j)); } + } + } + return out; +} + +bool DesignSketchTool::on_mouse(wxMouseEvent& evt, GLCanvas3D& canvas) +{ + // Track the cursor in canvas client px so the in-canvas value editor can open right + // where the user clicked (Onshape places the field at the click, not via a camera + // projection — the design canvas's viewport isn't valid outside its own paint). + m_last_mouse_x = evt.GetX(); + m_last_mouse_y = evt.GetY(); + + // Line draw-then-edit: while the length editor is open right after the second click, + // freeze the canvas so a stray move/click can't push a third point or rubber-band a + // segment under the floating field. The editor's Enter/Esc resolves it + // (apply_segment_length / keep_segment_as_drawn, the latter clears this flag). + if (m_awaiting_length) { + // Polyline terminators must work even with a per-segment field open: right-click or + // double-click accepts the current segment as drawn (close the field) and falls through + // so the Polyline handler ends the chain. Without this the freeze ate every terminator. + if (m_mode == Mode::Polyline && (evt.RightDown() || evt.LeftDClick()) && on_inline_dismiss) + on_inline_dismiss(); // -> set_inline_busy(false), m_awaiting_length=false + else + return true; + } + + // No live session, but committed sketches are shown as overlays on the plate: a left + // click on a loop (its edge OR its closed interior) selects that Sketch feature. This + // is the ONLY interaction in display-only mode; everything else (drag/move/wheel/right) + // falls through (return false) so the camera can still orbit the plate. + if (!m_active) { + // Double-click anywhere fits the view (the bottom navigator orb does orientation; this + // is the fit shortcut). Handled before gizmo/pick so it always works on the idle plate. + if (evt.LeftDClick()) { canvas.zoom_to_volumes(); return true; } + // Visual Extrude gizmo (C5b): while the Extrude card is open the depth arrow is + // grabbable — drag changes the depth live; a click (no drag) on the arrow opens the + // inline depth editor. Intercept before the early no-LeftDown bailout so Dragging/ + // LeftUp reach us; a LeftDown that misses the arrow falls through to solid/loop pick. + // Move-body gizmo (M5): three world-axis arrows on the selected body. Drag an arrow to + // translate live; a stationary click on it opens the inline offset editor; a right click + // exits move mode. A LeftDown that misses the arrows falls through to solid re-pick. + if (m_mv_active) { + if (m_mv_drag >= 0 && evt.Dragging() && evt.LeftIsDown()) { + if (m_mv_drag < 3) drag_move_arrow(canvas, evt, m_mv_drag); + else drag_move_arc(canvas, evt, m_mv_drag - 3); + return true; + } + if (evt.LeftUp() && m_mv_drag >= 0) { + const int d = m_mv_drag; m_mv_drag = -1; + const bool moved = std::abs(evt.GetX() - m_mv_press_x) + + std::abs(evt.GetY() - m_mv_press_y) > 3; + if (!moved && d < 3) open_move_editor(d); // stationary click on an arrow = edit offset + return true; + } + if (evt.RightDown()) { clear_move_gizmo(); canvas.set_as_dirty(); if (on_move_exit) on_move_exit(); return true; } + if (evt.LeftDown()) { + int axis = -1; + if (hit_test_move_arrow(canvas, evt, axis)) { // translate arrows win over rings + m_mv_drag = axis; m_mv_press_x = evt.GetX(); m_mv_press_y = evt.GetY(); + return true; + } + if (hit_test_move_arc(canvas, evt, axis)) { + m_mv_drag = 3 + axis; m_mv_press_x = evt.GetX(); m_mv_press_y = evt.GetY(); + m_mv_rot_start = m_mv_rot; + double a0; if (arc_mouse_angle(canvas, evt, axis, a0)) m_mv_arc_a0 = a0; + return true; + } + } + } + if (m_ex_active) { + if (m_ex_drag >= 0 && evt.Dragging() && evt.LeftIsDown()) { + drag_extrude_arrow(canvas, evt, m_ex_drag); + return true; + } + if (evt.LeftUp() && m_ex_drag >= 0) { + const int which = m_ex_drag; m_ex_drag = -1; + const bool moved = std::abs(evt.GetX() - m_ex_press_x) + + std::abs(evt.GetY() - m_ex_press_y) > 3; + if (!moved) open_extrude_editor(which); // treat a stationary click as edit + return true; + } + if (evt.LeftDown()) { + int which = -1; + if (hit_test_extrude_arrow(canvas, evt, which)) { + m_ex_drag = which; m_ex_press_x = evt.GetX(); m_ex_press_y = evt.GetY(); + return true; + } + } + } + // Revolve angle-arc gizmo: drag the arc tip to sweep the angle; a stationary click on the + // handle opens the inline editor. A LeftDown that misses falls through to normal picking. + if (m_rv_active) { + if (m_rv_drag && evt.Dragging() && evt.LeftIsDown()) { + drag_revolve_arc(canvas, evt); + return true; + } + if (evt.LeftUp() && m_rv_drag) { + m_rv_drag = false; + const bool moved = std::abs(evt.GetX() - m_rv_press_x) + + std::abs(evt.GetY() - m_rv_press_y) > 3; + if (!moved) open_revolve_editor(); + return true; + } + if (evt.LeftDown() && hit_test_revolve_handle(canvas, evt)) { + m_rv_drag = true; m_rv_press_x = evt.GetX(); m_rv_press_y = evt.GetY(); + return true; + } + } + // Pattern gizmo: drag the diamond/arc handle to set the spacing (linear) or angle (circular); + // a stationary click opens the inline editor. A LeftDown that misses falls through to picking. + if (m_pt_active) { + if (m_pt_drag && evt.Dragging() && evt.LeftIsDown()) { + drag_pattern_handle(canvas, evt); + return true; + } + if (evt.LeftUp() && m_pt_drag) { + m_pt_drag = false; + const bool moved = std::abs(evt.GetX() - m_pt_press_x) + + std::abs(evt.GetY() - m_pt_press_y) > 3; + if (!moved) open_pattern_editor(); + return true; + } + if (evt.LeftDown() && hit_test_pattern_handle(canvas, evt)) { + m_pt_drag = true; m_pt_press_x = evt.GetX(); m_pt_press_y = evt.GetY(); + return true; + } + } + // Fillet/Chamfer radius gizmo: drag the edge-anchored arrow to set the radius live; a + // stationary click opens the inline editor. A LeftDown that misses falls through to pick. + if (m_fl_active) { + if (m_fl_drag && evt.Dragging() && evt.LeftIsDown()) { + drag_fillet_arrow(canvas, evt); + return true; + } + if (evt.LeftUp() && m_fl_drag) { + m_fl_drag = false; + const bool moved = std::abs(evt.GetX() - m_fl_press_x) + + std::abs(evt.GetY() - m_fl_press_y) > 3; + if (!moved) open_fillet_editor(); // stationary click = edit + return true; + } + if (evt.LeftDown() && hit_test_fillet_arrow(canvas, evt)) { + start_fillet_drag(canvas, evt); + return true; + } + } + // Hole gizmo: drag the centre to reposition / the diameter or depth arrow to resize; a + // stationary click on an arrow opens its inline editor. A LeftDown that misses any handle + // falls through to solid/loop picking. + if (m_hl_active) { + if (m_hl_drag >= 0 && evt.Dragging() && evt.LeftIsDown()) { + drag_hole_handle(canvas, evt); + return true; + } + if (evt.LeftUp() && m_hl_drag >= 0) { + const int which = m_hl_drag; m_hl_drag = -1; + const bool moved = std::abs(evt.GetX() - m_hl_press_x) + + std::abs(evt.GetY() - m_hl_press_y) > 3; + if (!moved) open_hole_editor(which); // stationary click = edit + return true; + } + if (evt.LeftDown()) { + const int which = hit_test_hole_handle(canvas, evt); + if (which >= 0) { start_hole_drag(canvas, evt, which); return true; } + } + } + // Thread gizmo: same interaction as the hole gizmo (centre / radius / length handles). + if (m_th_active) { + if (m_th_drag >= 0 && evt.Dragging() && evt.LeftIsDown()) { + drag_thread_handle(canvas, evt); + return true; + } + if (evt.LeftUp() && m_th_drag >= 0) { + const int which = m_th_drag; m_th_drag = -1; + const bool moved = std::abs(evt.GetX() - m_th_press_x) + + std::abs(evt.GetY() - m_th_press_y) > 3; + if (!moved) open_thread_editor(which); + return true; + } + if (evt.LeftDown()) { + const int which = hit_test_thread_handle(canvas, evt); + if (which >= 0) { start_thread_drag(canvas, evt, which); return true; } + } + } + // Shell gizmo: drag the inward thickness arrow; stationary click opens the inline editor. + if (m_sh_active) { + if (m_sh_drag && evt.Dragging() && evt.LeftIsDown()) { + drag_shell_arrow(canvas, evt); + return true; + } + if (evt.LeftUp() && m_sh_drag) { + m_sh_drag = false; + const bool moved = std::abs(evt.GetX() - m_sh_press_x) + + std::abs(evt.GetY() - m_sh_press_y) > 3; + if (!moved) open_shell_editor(); + return true; + } + if (evt.LeftDown() && hit_test_shell_arrow(canvas, evt)) { + start_shell_drag(canvas, evt); + return true; + } + } + if (!evt.LeftDown()) return false; + // Committed-sketch loop pick is computed FIRST. A click that lands on a loop's + // STROKE (edge) selects that loop even when it lies on a solid face — so a sketch + // drawn ON a face can be selected and extruded/cut (Onshape engraving workflow). + // Open-face area (no loop stroke under the cursor) falls through to the solid + // whole/face/edge cycle; an interior hit with no solid behind it is the last resort. + Point pos(evt.GetX(), evt.GetY()); + const Linef3 ray = canvas.mouse_ray(pos); + const Linef3 ray8 = canvas.mouse_ray(Point(evt.GetX() + 8, evt.GetY())); + int edge_feat = -1, edge_reg = -1; double edge_d = 1e30; // nearest loop stroke (wins) + int face_feat = -1, face_reg = -1; // interior (fallback) + for (const DisplaySketch& d : m_display_sketches) { + const Vec2d p = d.plane.project(ray.a, ray.vector()); + const Vec2d p8 = d.plane.project(ray8.a, ray8.vector()); + const double tol = std::max(1e-3, (p8 - p).norm()); + const std::vector loops = region_loops(d.entities); + for (int r = 0; r < int(loops.size()); ++r) { + for (int ei : loops[r].ents) { + if (ei < 0 || ei >= int(d.entities.size())) continue; + const double ed = entity_pick_dist(p, d.entities[ei]); + if (ed <= tol * 3.0 && ed < edge_d) { edge_d = ed; edge_feat = d.feature; edge_reg = r; } + } + if (face_feat < 0 && point_in_poly(p, loops[r].poly)) { face_feat = d.feature; face_reg = r; } + } + } + // A precise hit on a loop outline wins over the solid face beneath it. + if (edge_feat >= 0) { + m_display_pick = edge_feat; m_display_pick_region = edge_reg; + if (on_display_sketch_selected) on_display_sketch_selected(edge_feat, edge_reg); + return true; + } + // No loop stroke under the cursor: the solid is the foreground (whole/face/edge cycle). + if (handle_solid_click(canvas, evt)) return true; + // Interior of a committed loop with no solid behind it. + if (face_feat >= 0) { + m_display_pick = face_feat; m_display_pick_region = face_reg; + if (on_display_sketch_selected) on_display_sketch_selected(face_feat, face_reg); + return true; + } + m_display_pick = -1; m_display_pick_region = -1; // clicked bare plate -> drop highlight + return false; // let the stock canvas orbit / deselect + } + + // In-canvas edit-op tools (Fillet/Chamfer/Offset/Mirror): pick entities, then a + // draggable arrow + editable value label (Mirror: a two-phase pick) drives a live + // ghost. A click on empty space confirms; right-click/Esc cancels the gesture. + // Standalone Trim / Extend scissors: click a segment to cut it back to (Trim) or out to + // (Extend) its nearest intersection with the other live entities. One cut per click; the + // tool stays active for more cuts; right-click exits. Drag falls through so the camera can + // still orbit. Operates directly on the live sketch — no Constrain mode. + if (m_mode == Mode::Trim || m_mode == Mode::Extend) { + if (evt.Moving()) { screen_to_plane(canvas, evt, m_cursor); m_has_cursor = true; return true; } + if (evt.LeftDown()) { + Vec2d p; screen_to_plane(canvas, evt, p); + const Linef3 r2 = canvas.mouse_ray(Point(evt.GetX() + 8, evt.GetY())); + const double tol = std::max(1e-3, (m_plane.project(r2.a, r2.vector()) - p).norm()); + if (apply_live_trim(p, tol * 3.0, m_mode == Mode::Extend)) resolve_live(); + return true; + } + if (evt.RightDown()) { request_exit(); return true; } + return false; // let move/drag orbit the camera + } + + if (is_edit_op_mode()) { + if (evt.Moving()) { + screen_to_plane(canvas, evt, m_cursor); + m_has_cursor = true; + return true; + } + if (m_op_dragging_arrow && evt.Dragging() && evt.LeftIsDown()) { + Vec2d p; screen_to_plane(canvas, evt, p); + drag_op_arrow(p); + return true; + } + if (evt.LeftUp()) { + if (m_op_dragging_arrow) { m_op_dragging_arrow = false; return true; } + return false; + } + if (evt.LeftDown()) { + Vec2d p; screen_to_plane(canvas, evt, p); + const Linef3 r2 = canvas.mouse_ray(Point(evt.GetX() + 8, evt.GetY())); + const double tol = std::max(1e-3, (m_plane.project(r2.a, r2.vector()) - p).norm()); + // 1) live gizmo: click the value label to type, or grab the arrow to drag. + if (op_ready() && m_mode != Mode::Mirror) { + const Linef3 rl = canvas.mouse_ray(Point(evt.GetX() + 24, evt.GetY())); + const double ltol = std::max(tol, (m_plane.project(rl.a, rl.vector()) - p).norm()); + if ((m_op_label - p).norm() <= ltol) { open_op_editor(); return true; } + if (hit_test_op_arrow(p, tol)) { m_op_dragging_arrow = true; return true; } + } + // 2) entity pick (close enough to an entity edge). + double best = 1e30; int bi = -1; + for (size_t i = 0; i < m_entities.size(); ++i) { + const double d = entity_pick_dist(p, m_entities[i]); + if (d < best) { best = d; bi = int(i); } + } + if (bi >= 0 && best <= tol * 3.0) { op_pick(bi); return true; } + // 3) empty click confirms a ready gesture. + if (op_ready()) confirm_op(); + return true; + } + if (evt.RightDown()) { + if (m_op_a >= 0 || !m_mirror_targets.empty()) { + reset_op(); + m_selection.clear(); + if (on_selection_changed) on_selection_changed(0); + } else { + request_exit(); + } + return true; + } + return false; + } + + // In-canvas transform tools (Move/Rotate/Scale/Array/PolarArray): pick subject + // entities, then a single draggable handle + editable value label(s) drive a live + // ghost. A click on empty space confirms; right-click drops the gesture / exits. + if (is_transform_mode()) { + if (evt.Moving()) { + screen_to_plane(canvas, evt, m_cursor); + m_has_cursor = true; + return true; + } + if (m_tf_dragging && evt.Dragging() && evt.LeftIsDown()) { + Vec2d p; screen_to_plane(canvas, evt, p); + drag_tf_handle(p); + return true; + } + if (evt.LeftUp()) { + if (m_tf_dragging) { m_tf_dragging = false; return true; } + return false; + } + if (evt.LeftDown()) { + Vec2d p; screen_to_plane(canvas, evt, p); + const Linef3 r2 = canvas.mouse_ray(Point(evt.GetX() + 8, evt.GetY())); + const double tol = std::max(1e-3, (m_plane.project(r2.a, r2.vector()) - p).norm()); + // 1) live gizmo: click a value label to type, or grab the handle to drag. + if (tf_ready()) { + const Linef3 rl = canvas.mouse_ray(Point(evt.GetX() + 24, evt.GetY())); + const double ltol = std::max(tol, (m_plane.project(rl.a, rl.vector()) - p).norm()); + if ((m_tf_label_a - p).norm() <= ltol) { open_tf_editor_a(); return true; } + if ((m_tf_label_b - p).norm() <= ltol) { open_tf_editor_count(); return true; } + if (hit_test_tf_handle(p, tol)) { m_tf_dragging = true; return true; } + } + // 2) entity pick (close enough to an entity edge). + double best = 1e30; int bi = -1; + for (size_t i = 0; i < m_entities.size(); ++i) { + const double d = entity_pick_dist(p, m_entities[i]); + if (d < best) { best = d; bi = int(i); } + } + if (bi >= 0 && best <= tol * 3.0) { tf_pick(bi); return true; } + // 3) empty click confirms a ready gesture. + if (tf_ready()) confirm_transform(); + return true; + } + if (evt.RightDown()) { + if (!m_tf_targets.empty()) { + reset_tf(); + m_selection.clear(); + if (on_selection_changed) on_selection_changed(0); + } else { + request_exit(); + } + return true; + } + return false; + } + + // Imported-art bbox transform: drag a corner to scale, the centre to move. Right-click + // ends the session. Values stream live to the host via on_imported_transform. + if (m_mode == Mode::TransformArt) { + if (evt.Moving()) { + screen_to_plane(canvas, evt, m_cursor); + m_has_cursor = true; + return true; + } + if (evt.LeftDown()) { + Vec2d p; screen_to_plane(canvas, evt, p); + const Linef3 r2 = canvas.mouse_ray(Point(evt.GetX() + 10, evt.GetY())); + const double tol = std::max(1e-3, (m_plane.project(r2.a, r2.vector()) - p).norm()); + const int h = hit_test_xform_handle(p, tol); + if (h >= 0) { + m_xform_handle = h; + if (h == 4) m_xform_anchor = p; // centre move: track the delta + else { Vec2d c[4]; xform_world_corners(c); m_xform_anchor = c[(h + 2) % 4]; } + } + return true; // swallow (no camera orbit while placing) + } + if (m_xform_handle >= 0 && evt.Dragging() && evt.LeftIsDown()) { + Vec2d p; screen_to_plane(canvas, evt, p); + drag_xform_handle(p); + return true; + } + if (evt.LeftUp()) { m_xform_handle = -1; return true; } + if (evt.RightDown()) { if (on_exit) on_exit(); else cancel(); return true; } + return false; + } + + // Constrain mode: pick a segment on click; let move/drag fall through so the + // camera can still orbit while inspecting the sketch. + if (m_mode == Mode::Constrain) { + if (m_constrain_entities) { + // Pick any entity (line/circle/arc/point): rolling two-slot selection. + if (evt.LeftDown()) { + Vec2d p; + screen_to_plane(canvas, evt, p); + double best = 1e30; + int bi = -1; + for (size_t i = 0; i < m_entities.size(); ++i) { + const double d = entity_pick_dist(p, m_entities[i]); + if (d < best) { best = d; bi = int(i); } + } + if (bi >= 0) { + // Rolling three-slot selection: slots 0/1 feed all 2-entity + // constraints; slot 2 is the Symmetric axis (only filled once + // 0 and 1 are set). A click past slot 2 restarts the cycle. + if (m_pick0 < 0) { m_pick0 = bi; m_pick0_pt = p; } + else if (m_pick1 < 0 && bi != m_pick0) m_pick1 = bi; + else if (m_pick1 >= 0 && m_pick2 < 0 && bi != m_pick0 && bi != m_pick1) m_pick2 = bi; + else { m_pick0 = bi; m_pick1 = m_pick2 = -1; m_pick0_pt = p; } + } + return true; + } + if (evt.RightDown()) { cancel(); return true; } + return false; + } + if (evt.LeftDown()) { + if (m_points.size() < 2) + return false; + Vec2d p; + screen_to_plane(canvas, evt, p); + const size_t n = m_points.size(); + double best = 1e30; + int bi = -1; + for (size_t i = 0; i < n; ++i) { + const double d = point_segment_dist(p, m_points[i], m_points[(i + 1) % n]); + if (d < best) { best = d; bi = int(i); } + } + if (bi >= 0) { + m_sel_a = bi; + m_sel_b = int((bi + 1) % n); + } + return true; + } + if (evt.RightDown()) { + cancel(); + return true; + } + return false; + } + + // Selection mode: click to pick an entity, Shift/Ctrl to extend, double-click + // to grab the whole connected loop. Drag falls through so the camera can orbit. + if (m_mode == Mode::Select) { + if (update_hover(canvas, evt)) return true; // repaint when the hovered handle changes + const bool extend = evt.ShiftDown() || evt.ControlDown(); + + // Live point drag: once an endpoint/centre was grabbed on LeftDown, dragging + // moves it (re-solving constraints live) until the button is released. When + // nothing is grabbed, drag falls through so the camera can still orbit. + if (m_dragging_point && evt.Dragging() && evt.LeftIsDown()) { + Vec2d p; + screen_to_plane(canvas, evt, p); + if (m_drag_poly_fi >= 0) + drag_polygon_vertex(m_drag_poly_fi, m_drag_ei, m_drag_role, p); // keep regular + else if (m_drag_rect_fi >= 0) + drag_rect_corner(m_drag_rect_fi, p); // resize axis-aligned box + else if (m_drag_slot_fi >= 0) + drag_slot_handle(m_drag_slot_fi, p); // move a slot end + else if (m_drag_ei >= 0 && m_drag_ei < int(m_entities.size()) && + m_entities[m_drag_ei].type == SketchEntity::Type::Arc) + drag_arc_handle(m_drag_ei, m_drag_role, p); // center/radius/angle grips + else if (m_drag_ei >= 0 && m_drag_ei < int(m_entities.size()) && + m_entities[m_drag_ei].type == SketchEntity::Type::EllipseArc) + drag_ellipsearc_handle(m_drag_ei, m_drag_role, p); // center/sweep endpoints + else { + set_point(m_drag_ei, m_drag_role, p); + resolve_live_drag(m_drag_ei, m_drag_role); + } + return true; + } + // Derived-handle drag (A4): the circle RadiusHandle (and later slot/rect/etc.) + // isn't an entity point, so it rides set_handle, which applies the role-specific + // geometry edit + re-solve. + if (m_dragging_handle && evt.Dragging() && evt.LeftIsDown()) { + Vec2d p; + screen_to_plane(canvas, evt, p); + set_handle(m_drag_handle, p); + return true; + } + if (evt.LeftUp()) { + if (m_dragging_point) { + Vec2d p; + screen_to_plane(canvas, evt, p); + if (m_drag_poly_fi >= 0) + drag_polygon_vertex(m_drag_poly_fi, m_drag_ei, m_drag_role, p); + else if (m_drag_rect_fi >= 0) + drag_rect_corner(m_drag_rect_fi, p); + else if (m_drag_slot_fi >= 0) + drag_slot_handle(m_drag_slot_fi, p); + else if (m_drag_ei >= 0 && m_drag_ei < int(m_entities.size()) && + m_entities[m_drag_ei].type == SketchEntity::Type::Arc) + drag_arc_handle(m_drag_ei, m_drag_role, p); + else if (m_drag_ei >= 0 && m_drag_ei < int(m_entities.size()) && + m_entities[m_drag_ei].type == SketchEntity::Type::EllipseArc) + drag_ellipsearc_handle(m_drag_ei, m_drag_role, p); + else { + set_point(m_drag_ei, m_drag_role, p); + resolve_live_drag(m_drag_ei, m_drag_role); + } + m_dragging_point = false; + m_drag_ei = -1; + m_drag_poly_fi = -1; + m_drag_rect_fi = -1; + m_drag_slot_fi = -1; + return true; + } + if (m_dragging_handle) { + Vec2d p; + screen_to_plane(canvas, evt, p); + set_handle(m_drag_handle, p); + m_dragging_handle = false; + return true; + } + return false; + } + + if (evt.LeftDown() || evt.LeftDClick()) { + m_dragging_point = false; // a fresh press disarms any stale grab + m_dragging_handle = false; + m_drag_poly_fi = -1; + m_drag_rect_fi = -1; + m_drag_slot_fi = -1; + Vec2d p; + screen_to_plane(canvas, evt, p); + if (evt.LeftDClick()) { // double-click a quote label -> edit it + const Linef3 rd = canvas.mouse_ray(Point(evt.GetX() + 28, evt.GetY())); + const double dtol = std::max(2.0, (m_plane.project(rd.a, rd.vector()) - p).norm()); + const int di = hit_test_dimension(p, dtol); + if (di >= 0) { edit_dimension(di); return true; } + } + // Zoom-aware tolerance: project a point 8 px away and measure in plane units. + const Linef3 r2 = canvas.mouse_ray(Point(evt.GetX() + 8, evt.GetY())); + const Vec2d p2 = m_plane.project(r2.a, r2.vector()); + const double tol = std::max(1e-3, (p2 - p).norm()); + + // Single-click a dimension quote label -> edit its value in place. A placed + // driving quote reopens its editor; a live (non-driving) characteristic quote + // is first promoted to a driving dimension (place_dimension), then its editor + // opens — so typing a value sets the precise dimension. Generous label + // tolerance (~24 px) since text labels are wider than a point grip. + if (evt.LeftDown()) { + const Linef3 rdl = canvas.mouse_ray(Point(evt.GetX() + 24, evt.GetY())); + const double ltol = std::max(tol, (m_plane.project(rdl.a, rdl.vector()) - p).norm()); + const int di = hit_test_dimension(p, ltol); + if (di >= 0) { open_value_editor(di); return true; } + for (const DimAnnot& q : m_live_quotes) { + if ((q.label_pos - p).norm() <= ltol) { + if (q.kind == DimType::Angle) + open_angle_editor(q.ea); // geometric rotate to a typed angle + else + place_dimension(q); // promote -> driving dim + open editor + return true; + } + } + if (m_live_poly_fi >= 0) { + if ((m_live_poly_side_label - p).norm() <= ltol) { + open_polygon_side_editor(m_live_poly_fi); // geometric uniform scale + return true; + } + if ((m_live_poly_angle_label - p).norm() <= ltol) { + open_polygon_angle_editor(m_live_poly_fi); // geometric rotate + return true; + } + } + if (m_live_arc_ei >= 0 && (m_live_arc_angle_label - p).norm() <= ltol) { + open_arc_angle_editor(m_live_arc_ei); // geometric sweep change + return true; + } + if (m_live_ellipse_ei >= 0) { + if ((m_live_ellipse_major_label - p).norm() <= ltol) { + open_ellipse_axis_editor(m_live_ellipse_ei, true); // semi-major + return true; + } + if ((m_live_ellipse_minor_label - p).norm() <= ltol) { + open_ellipse_axis_editor(m_live_ellipse_ei, false); // semi-minor + return true; + } + } + if (m_live_rrect_fi >= 0) { + if ((m_live_rrect_w_label - p).norm() <= ltol) { open_rounded_rect_editor(m_live_rrect_fi, 0); return true; } + if ((m_live_rrect_h_label - p).norm() <= ltol) { open_rounded_rect_editor(m_live_rrect_fi, 1); return true; } + if ((m_live_rrect_r_label - p).norm() <= ltol) { open_rounded_rect_editor(m_live_rrect_fi, 2); return true; } + } + if (m_live_aslot_fi >= 0) { + if ((m_live_aslot_r_label - p).norm() <= ltol) { open_arc_slot_editor(m_live_aslot_fi, true); return true; } + if ((m_live_aslot_w_label - p).norm() <= ltol) { open_arc_slot_editor(m_live_aslot_fi, false); return true; } + } + } + + // A derived handle (the circle RadiusHandle — not an entity point, so + // hit_test_point can't grab it) arms a handle drag that resizes on motion. + // Checked before the point/entity hit-tests so the radius grip wins near + // the circle edge. + if (evt.LeftDown()) { + Handle hh; + if (hit_test_handle(p, tol, hh) && + (hh.role == HandleRole::RadiusHandle || hh.role == HandleRole::MajorAxis || + hh.role == HandleRole::MinorAxis || hh.role == HandleRole::BSplineCtrl)) { + m_dragging_handle = true; + m_drag_handle = hh; + m_selection.clear(); + m_point_sel.clear(); + m_selection.push_back(hh.ei); // highlight the circle being resized + if (on_selection_changed) + on_selection_changed(int(m_selection.size())); + return true; + } + } + + // A nearby endpoint/centre selects that POINT (a line = a segment + 2 + // points); a click on the bare segment selects the whole entity. + if (evt.LeftDown()) { + int pe; SketchPointRole pr; + if (hit_test_point(p, tol, pe, pr)) { + const auto key = std::make_pair(pe, pr); + auto it = std::find(m_point_sel.begin(), m_point_sel.end(), key); + if (extend) { + if (it == m_point_sel.end()) m_point_sel.push_back(key); + else m_point_sel.erase(it); + } else { + m_selection.clear(); + m_point_sel.clear(); + m_point_sel.push_back(key); + } + // Arm the drag so the grabbed point follows the cursor. + m_dragging_point = true; + m_drag_ei = pe; + m_drag_role = pr; + // If the grabbed point is a polygon vertex, the drag must keep the + // polygon REGULAR — it adjusts the circumradius + orientation (the + // vertex follows the cursor) instead of moving one point freely. + const int pf = feature_of(pe); + m_drag_poly_fi = (pf >= 0 && m_features[pf].kind == FeatureKind::Polygon) ? pf : -1; + m_drag_rect_fi = -1; m_drag_slot_fi = -1; + if (pf >= 0 && m_drag_poly_fi < 0) { + const Feature& ft = m_features[pf]; + if (ft.kind == FeatureKind::CornerRect || ft.kind == FeatureKind::CenterRect) { + // Only axis-aligned boxes resize by corner (oblique rects fall + // back to free point move). Capture the fixed opposite corner. + const SketchEntity& e0 = m_entities[ft.begin]; + const Vec2d d0 = e0.p1 - e0.p0; + const bool aa = std::abs(d0.x()) < 1e-6 || std::abs(d0.y()) < 1e-6; + Vec2d gp; + if (aa && point_at(pe, pr, gp)) { + Vec2d opp; + opp.x() = (std::abs(gp.x() - ft.c0.x()) < std::abs(gp.x() - ft.c1.x())) ? ft.c1.x() : ft.c0.x(); + opp.y() = (std::abs(gp.y() - ft.c0.y()) < std::abs(gp.y() - ft.c1.y())) ? ft.c1.y() : ft.c0.y(); + m_drag_rect_fi = pf; m_drag_rect_anchor = opp; + } + } else if (ft.kind == FeatureKind::Slot && + pr == SketchPointRole::Center && + (pe == ft.begin + 1 || pe == ft.begin + 3)) { + m_drag_slot_fi = pf; // cap@c1 = begin+1, cap@c0 = begin+3 + m_drag_slot_c1 = (pe == ft.begin + 1); + } + } + if (on_selection_changed) + on_selection_changed(int(m_selection.size() + m_point_sel.size())); + return true; + } + } + + const int hit = hit_test(p, tol); + + if (evt.LeftDClick() && hit >= 0) { + if (!extend) m_selection.clear(); + for (int idx : connected_loop(hit)) + if (std::find(m_selection.begin(), m_selection.end(), idx) == m_selection.end()) + m_selection.push_back(idx); + } else if (hit >= 0) { + auto it = std::find(m_selection.begin(), m_selection.end(), hit); + if (extend) { + if (it == m_selection.end()) m_selection.push_back(hit); + else m_selection.erase(it); // toggle off + } else { + m_selection.clear(); + m_point_sel.clear(); + m_selection.push_back(hit); + } + } else if (!extend) { + // Inside a closed loop (not on an edge/point) → select it as a face + // and hand off to the panel, which commits the sketch and extrudes. + if (evt.LeftDown() && on_face_selected && region_at(p) >= 0) { + m_selection.clear(); + m_point_sel.clear(); + on_face_selected(); + return true; + } + m_selection.clear(); // clicked empty space + m_point_sel.clear(); + } + if (on_selection_changed) + on_selection_changed(int(m_selection.size() + m_point_sel.size())); + return true; + } + if (evt.RightDown()) { + clear_selection(); + return true; + } + return false; // let drag orbit the camera + } + + // Dimension mode: click entities directly. 2 points -> Distance, a line -> Length, + // a circle -> Diameter, an arc -> Radius, a point then a line -> DistanceToLine. + // Each resolved pick places a driving quote and pops the value card. + if (m_mode == Mode::Dimension) { + if (update_hover(canvas, evt)) return true; // repaint when the hovered handle changes + if (evt.LeftDClick()) { // double-click a quote label -> edit it + Vec2d p; + screen_to_plane(canvas, evt, p); + const Linef3 r2 = canvas.mouse_ray(Point(evt.GetX() + 28, evt.GetY())); + const Vec2d p2 = m_plane.project(r2.a, r2.vector()); + const double di_tol = std::max(2.0, (p2 - p).norm()); + const int di = hit_test_dimension(p, di_tol); + if (di >= 0) edit_dimension(di); + m_dim_has0 = false; + return true; + } + if (evt.LeftDown()) { + Vec2d p; + screen_to_plane(canvas, evt, p); + const Linef3 r2 = canvas.mouse_ray(Point(evt.GetX() + 8, evt.GetY())); + const Vec2d p2 = m_plane.project(r2.a, r2.vector()); + const double tol = std::max(1e-3, (p2 - p).norm()); + int pe; SketchPointRole pr; + const bool got_pt = hit_test_point(p, tol, pe, pr); + const int he = hit_test(p, tol); + if (!m_dim_has0) { + if (got_pt) { // first point picked: await a second + m_dim_e0 = pe; m_dim_r0 = pr; m_dim_has0 = true; + } else if (he >= 0) { // whole-entity dimension + DimAnnot a; a.ea = he; + const SketchEntity::Type t = m_entities[he].type; + if (t == SketchEntity::Type::Line) { a.kind = DimType::Length; place_dimension(a); } + else if (t == SketchEntity::Type::Circle) { a.kind = DimType::Diameter; place_dimension(a); } + else if (t == SketchEntity::Type::Arc) { a.kind = DimType::Radius; place_dimension(a); } + } + } else { + if (got_pt && !(pe == m_dim_e0 && pr == m_dim_r0)) { + DimAnnot a; a.kind = DimType::Distance; + a.ea = m_dim_e0; a.ra = m_dim_r0; a.eb = pe; a.rb = pr; + place_dimension(a); + } else if (he >= 0 && m_entities[he].type == SketchEntity::Type::Line) { + DimAnnot a; a.kind = DimType::DistanceToLine; + a.ea = m_dim_e0; a.ra = m_dim_r0; a.eb = he; + place_dimension(a); + } + m_dim_has0 = false; // reset after the second pick + } + return true; + } + if (evt.RightDown()) { m_dim_has0 = false; return true; } + return false; // let drag orbit the camera + } + + if (evt.Moving()) { + m_snap_off = evt.ShiftDown(); + screen_to_plane(canvas, evt, m_cursor); + m_has_cursor = true; + m_cursor_locked = false; + bool vsnap = false; + m_cursor = snap_vertex(canvas, evt, m_cursor, vsnap); // preview-snap to endpoints + const bool line_like = (m_mode == Mode::Polyline || m_mode == Mode::Line); + if (line_like && !m_points.empty() && !vsnap) + m_cursor = snap_dir(m_points.back(), m_cursor, m_cursor_locked); + if (on_cursor_metrics && line_like && !m_points.empty() && !m_awaiting_length) { + const Vec2d d = m_cursor - m_points.back(); + on_cursor_metrics(d.norm(), std::atan2(d.y(), d.x()) * 180.0 / M_PI, m_cursor_locked); + } + return true; + } + + switch (m_mode) { + + case Mode::Polyline: { + if (evt.LeftDown()) { + Vec2d p; + screen_to_plane(canvas, evt, p); + m_snap_off = evt.ShiftDown(); + bool vsnap = false; + p = snap_vertex(canvas, evt, p, vsnap); // snap onto an existing endpoint + if (near_first(p)) { // closing the current chain back to its start + const int base = int(m_entities.size()); + push_closed_lines(m_points); // close the loop + infer_auto_constraints(base); // loop self-closes via auto Coincident + H/V + m_points.clear(); + return true; + } + if (!m_points.empty() && !vsnap) { + bool lk = false; + p = snap_dir(m_points.back(), p, lk); // lock new segment to inference angle + } + m_points.push_back(p); + arm_polyline_segment_edit(); // refine this segment's Length+Angle, then continue + return true; + } + if (evt.LeftDClick()) { + if (m_points.size() >= 2) { + const int base = int(m_entities.size()); + push_open_chain(m_points); // end as an open chain + infer_auto_constraints(base); + m_points.clear(); + } + return true; + } + if (evt.RightDown()) { + const int base = int(m_entities.size()); + if (m_points.size() >= 3) + push_closed_lines(m_points); + else if (m_points.size() == 2) + push_open_chain(m_points); + infer_auto_constraints(base); + m_points.clear(); + return true; + } + break; + } + + case Mode::Line: { + if (evt.LeftDown()) { + Vec2d p; + screen_to_plane(canvas, evt, p); + m_snap_off = evt.ShiftDown(); + bool vsnap = false; + p = snap_vertex(canvas, evt, p, vsnap); // snap onto an existing endpoint + if (m_points.empty()) { // first click = anchor + m_points.push_back(p); + return true; + } + bool lk = false; + if (!vsnap) p = snap_dir(m_points.back(), p, lk); // vertex snap wins over angle + m_points.push_back(p); // second click completes the segment + // Draw-then-edit: just commit the segment. The generic detect/service path + // (is_creation_autoedit_mode now includes Line) auto-selects it and opens its + // Length THEN Angle fields in sequence, each over its label — same UX as every + // other 2D tool. Enter advances (Length drives a Distance constraint, Angle rotates + // about P0); Esc keeps it as drawn. + keep_segment_as_drawn(); + return true; + } + if (evt.RightDown()) { // abandon the in-progress anchor + m_points.clear(); + m_has_cursor = false; + return true; + } + break; + } + + case Mode::CornerRect: { + if (evt.LeftDown()) { + Vec2d p; + screen_to_plane(canvas, evt, p); + if (m_points.empty()) { + m_points.push_back(p); + } else { + const Vec2d A = m_points[0]; + const Vec2d B = p; + const int base = int(m_entities.size()); + begin_feature(FeatureKind::CornerRect); + push_closed_lines({ A, Vec2d(B.x(), A.y()), B, Vec2d(A.x(), B.y()) }); + infer_auto_constraints(base); // corners Coincident + sides H/V + end_feature(A, B); // group: Width/Height live quotes + m_points.clear(); + } + return true; + } + if (evt.RightDown()) { m_points.clear(); return true; } + break; + } + + case Mode::CenterRect: { + if (evt.LeftDown()) { + Vec2d p; + screen_to_plane(canvas, evt, p); + if (m_points.empty()) { + m_points.push_back(p); + } else { + const Vec2d C = m_points[0]; + const double hx = std::abs(p.x() - C.x()); + const double hy = std::abs(p.y() - C.y()); + const int base = int(m_entities.size()); + begin_feature(FeatureKind::CenterRect); + push_closed_lines({ + Vec2d(C.x() - hx, C.y() - hy), Vec2d(C.x() + hx, C.y() - hy), + Vec2d(C.x() + hx, C.y() + hy), Vec2d(C.x() - hx, C.y() + hy) }); + infer_auto_constraints(base); + end_feature(Vec2d(C.x() - hx, C.y() - hy), Vec2d(C.x() + hx, C.y() + hy)); + m_points.clear(); + } + return true; + } + if (evt.RightDown()) { m_points.clear(); return true; } + break; + } + + case Mode::ObliqueRect: { + if (evt.LeftDown()) { + Vec2d p; screen_to_plane(canvas, evt, p); + bool vsnap = false; + if (m_points.size() < 2) // corners snap; 3rd click is the width + p = snap_vertex(canvas, evt, p, vsnap); + m_points.push_back(p); + if (m_points.size() == 3) { + const Vec2d A = m_points[0], B = m_points[1]; + Vec2d u = B - A; + if (u.squaredNorm() > 1e-12) { + u.normalize(); + const Vec2d n(-u.y(), u.x()); + const double w = n.dot(m_points[2] - A); // signed perpendicular width + const int base = int(m_entities.size()); + begin_feature(FeatureKind::CornerRect); + push_closed_lines({ A, B, B + n * w, A + n * w }); + infer_auto_constraints(base); // corners Coincident + the AB pair parallel + end_feature(A, B + n * w); // group: Width/Height live quotes + } + m_points.clear(); + } + return true; + } + if (evt.RightDown()) { m_points.clear(); return true; } + break; + } + + case Mode::RoundedRect: { + if (evt.LeftDown()) { + Vec2d p; screen_to_plane(canvas, evt, p); + bool vsnap = false; + if (m_points.size() < 2) // the two corners snap; 3rd sets radius + p = snap_vertex(canvas, evt, p, vsnap); + m_points.push_back(p); + if (m_points.size() == 3) { + const int base = int(m_entities.size()); + begin_feature(FeatureKind::RoundedRect); + append_entities(make_rounded_rect(m_points[0], m_points[1], m_points[2])); + infer_auto_constraints(base); + // Group with the actual clamped fillet radius so the W/H/R live quotes + // and rebuild edits can recover the box. (Skip grouping if degenerate.) + const Vec2d a = m_points[0], b = m_points[1]; + const double xmin=std::min(a.x(),b.x()), xmax=std::max(a.x(),b.x()); + const double ymin=std::min(a.y(),b.y()), ymax=std::max(a.y(),b.y()); + const double bw=xmax-xmin, bh=ymax-ymin; + const Vec2d cs[4]={{xmin,ymin},{xmax,ymin},{xmax,ymax},{xmin,ymax}}; + double r=1e18; for(const Vec2d&c:cs) r=std::min(r,(m_points[2]-c).norm()); + r=std::min(r, std::min(bw,bh)*0.5); + end_feature(Vec2d(xmin,ymin), Vec2d(xmax,ymax), r); + m_points.clear(); + } + return true; + } + if (evt.RightDown()) { m_points.clear(); return true; } + break; + } + + case Mode::CenterCircle: { + if (evt.LeftDown()) { + Vec2d p; + screen_to_plane(canvas, evt, p); + if (m_points.empty()) { + m_points.push_back(p); + } else { + const Vec2d C = m_points[0]; + push_circle(C, (p - C).norm()); + m_points.clear(); + } + return true; + } + if (evt.RightDown()) { m_points.clear(); return true; } + break; + } + + case Mode::TwoPointCircle: { + if (evt.LeftDown()) { + Vec2d p; screen_to_plane(canvas, evt, p); + bool vsnap = false; + p = snap_vertex(canvas, evt, p, vsnap); // diameter ends snap onto geometry + m_points.push_back(p); + if (m_points.size() == 2) { + const Vec2d C = (m_points[0] + m_points[1]) * 0.5; + push_circle(C, (m_points[1] - m_points[0]).norm() * 0.5); + m_points.clear(); + } + return true; + } + if (evt.RightDown()) { m_points.clear(); return true; } + break; + } + + case Mode::ThreePointCircle: { + if (evt.LeftDown()) { + Vec2d p; screen_to_plane(canvas, evt, p); + m_points.push_back(p); + if (m_points.size() == 3) { + append_entities(make_three_point_circle(m_points[0], m_points[1], m_points[2])); + m_points.clear(); + } + return true; + } + if (evt.RightDown()) { m_points.clear(); return true; } + break; + } + + case Mode::ThreePointArc: { + if (evt.LeftDown()) { + Vec2d p; screen_to_plane(canvas, evt, p); + bool vsnap = false; + if (m_points.size() < 2) // snap the start/end onto endpoints + p = snap_vertex(canvas, evt, p, vsnap); // (the 3rd click is the through-point) + m_points.push_back(p); + if (m_points.size() == 3) { + // clicks: start, end, point-on-arc + const int base = int(m_entities.size()); + append_entities(make_three_point_arc(m_points[0], m_points[1], m_points[2])); + infer_auto_constraints(base); // arc ends Coincident onto snapped vertices + m_points.clear(); + } + return true; + } + if (evt.RightDown()) { m_points.clear(); return true; } + break; + } + + case Mode::TangentArc: { + if (evt.LeftDown()) { + Vec2d p; screen_to_plane(canvas, evt, p); + bool vsnap = false; + p = snap_vertex(canvas, evt, p, vsnap); // snap both ends onto endpoints + m_points.push_back(p); + if (m_points.size() == 2) { + const int base = int(m_entities.size()); + append_entities(make_tangent_arc(m_points[0], m_points[1])); + infer_auto_constraints(base); // tangent-arc ends Coincident onto vertices + m_points.clear(); + } + return true; + } + if (evt.RightDown()) { m_points.clear(); return true; } + break; + } + + case Mode::CenterArc: { + if (evt.LeftDown()) { + Vec2d p; screen_to_plane(canvas, evt, p); + bool vsnap = false; + // The start (2nd click) snaps onto endpoints; center & end-dir are free. + if (m_points.size() == 1) + p = snap_vertex(canvas, evt, p, vsnap); + m_points.push_back(p); + if (m_points.size() == 3) { + const int base = int(m_entities.size()); + append_entities(make_center_arc(m_points[0], m_points[1], m_points[2])); + infer_auto_constraints(base); // arc start Coincident onto a snapped vertex + m_points.clear(); + } + return true; + } + if (evt.RightDown()) { m_points.clear(); return true; } + break; + } + + case Mode::Slot: { + if (evt.LeftDown()) { + Vec2d p; screen_to_plane(canvas, evt, p); + if (m_points.size() < 2) { + m_points.push_back(p); + } else { + // third click sets the half-width (distance to the centerline) + Vec2d u = m_points[1] - m_points[0]; + if (u.squaredNorm() > 1e-12) { + u.normalize(); + const Vec2d n(-u.y(), u.x()); + const double w = std::abs(n.dot(p - m_points[0])); + const int base = int(m_entities.size()); + begin_feature(FeatureKind::Slot); + append_entities(make_slot(m_points[0], m_points[1], w)); + infer_auto_constraints(base); + end_feature(m_points[0], m_points[1], w); // centres + half-width + } + m_points.clear(); + } + return true; + } + if (evt.RightDown()) { m_points.clear(); return true; } + break; + } + + case Mode::ArcSlot: { + if (evt.LeftDown()) { + Vec2d p; screen_to_plane(canvas, evt, p); + bool vsnap = false; + if (m_points.size() == 1) // start snaps; center & end-dir are free + p = snap_vertex(canvas, evt, p, vsnap); + m_points.push_back(p); + if (m_points.size() == 4) { + // clicks: center, start, end-dir, width + const double Rc = (m_points[1] - m_points[0]).norm(); + const double w = std::abs((m_points[3] - m_points[0]).norm() - Rc); + const int base = int(m_entities.size()); + begin_feature(FeatureKind::ArcSlot); + append_entities(make_arc_slot(m_points[0], m_points[1], m_points[2], w)); + infer_auto_constraints(base); + // c0 = centre, c1 = centreline start point; param = half-width. The end + // direction is recovered from the cap@E arc centre when rebuilding. + end_feature(m_points[0], m_points[1], w); + m_points.clear(); + } + return true; + } + if (evt.RightDown()) { m_points.clear(); return true; } + break; + } + + case Mode::Polygon: { + if (evt.LeftDown()) { + Vec2d p; screen_to_plane(canvas, evt, p); + if (m_points.empty()) { + m_points.push_back(p); + } else { + const int base = int(m_entities.size()); + begin_feature(FeatureKind::Polygon); + append_entities(make_polygon(m_points[0], p, m_polygon_sides)); + infer_auto_constraints(base); + end_feature(m_points[0], p, (p - m_points[0]).norm(), m_polygon_sides); + m_points.clear(); + } + return true; + } + if (evt.RightDown()) { m_points.clear(); return true; } + break; + } + + case Mode::Ellipse: { + if (evt.LeftDown()) { + Vec2d p; screen_to_plane(canvas, evt, p); + bool vsnap = false; + if (m_points.empty()) p = snap_vertex(canvas, evt, p, vsnap); // center can snap + m_points.push_back(p); + if (m_points.size() == 3) { // center, major-end, minor + const int base = int(m_entities.size()); + append_entities(make_ellipse(m_points[0], m_points[1], m_points[2])); + infer_auto_constraints(base); + m_points.clear(); + } + return true; + } + if (evt.RightDown()) { m_points.clear(); return true; } + break; + } + + case Mode::EllipseArc: { + if (evt.LeftDown()) { + Vec2d p; screen_to_plane(canvas, evt, p); + bool vsnap = false; + if (m_points.empty()) p = snap_vertex(canvas, evt, p, vsnap); // center can snap + m_points.push_back(p); + if (m_points.size() == 5) { // center, major, minor, start, end + const int base = int(m_entities.size()); + append_entities(make_ellipse_arc(m_points[0], m_points[1], m_points[2], + m_points[3], m_points[4])); + infer_auto_constraints(base); + m_points.clear(); + } + return true; + } + if (evt.RightDown()) { m_points.clear(); return true; } + break; + } + + case Mode::BSpline: { + // Variable-length: left-click adds a control pole (each can snap onto existing + // geometry); double-click or right-click finishes as an open spline. + if (evt.LeftDown()) { + Vec2d p; screen_to_plane(canvas, evt, p); + m_snap_off = evt.ShiftDown(); + bool vsnap = false; + p = snap_vertex(canvas, evt, p, vsnap); // poles can land on endpoints + m_points.push_back(p); + return true; + } + if (evt.LeftDClick() || evt.RightDown()) { + if (m_points.size() >= 2) { + const int base = int(m_entities.size()); + append_entities(make_bspline(m_points)); + infer_auto_constraints(base); // end poles auto-Coincident -> loops close + } + m_points.clear(); + return true; + } + break; + } + + case Mode::Point: { + if (evt.LeftDown()) { + Vec2d p; + screen_to_plane(canvas, evt, p); + push_point(p); + return true; + } + if (evt.RightDown()) { return true; } + break; + } + + case Mode::Constrain: + break; + } + + if (evt.Dragging()) + return false; + + return false; +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/DesignSketchTool.hpp b/src/slic3r/GUI/DesignSketchTool.hpp new file mode 100644 index 0000000000..dfac473963 --- /dev/null +++ b/src/slic3r/GUI/DesignSketchTool.hpp @@ -0,0 +1,980 @@ +#ifndef slic3r_DesignSketchTool_hpp_ +#define slic3r_DesignSketchTool_hpp_ + +#include "libslic3r/Point.hpp" +#include "libslic3r/SketchEngine.hpp" +#include "libslic3r/CadDocument.hpp" // CadBody for per-body solid picking +#include "libslic3r/SketchInference.hpp" +#include "libslic3r/SketchSolver.hpp" +#include "GLModel.hpp" +#include +#include +#include +#include + +class wxMouseEvent; +class wxPoint; + +namespace Slic3r { + +class TriangleMesh; // fwd (libslic3r) — solid-pick mesh, non-owning pointer + +namespace GUI { + +class GLCanvas3D; + +// Onshape-style sketch session. `begin` enters a session on a plane; the active +// drawing tool (Mode) can be switched mid-session via `set_tool` while entities +// accumulate. `finish` commits the whole entity list as one sketch feature; +// `cancel` aborts. Constrain is a separate legacy mode that operates on a +// committed profile's points (entity constraints land in a later chunk). +class DesignSketchTool { +public: + enum class Mode { Select, Dimension, Polyline, Line, CornerRect, CenterRect, ObliqueRect, + RoundedRect, CenterCircle, TwoPointCircle, Point, + ThreePointCircle, ThreePointArc, TangentArc, CenterArc, Slot, ArcSlot, Polygon, + Ellipse, EllipseArc, BSpline, + // In-canvas edit-op TOOLBAR tools (drag-arrow + label, no numeric card): + Fillet, Chamfer, Offset, Mirror, + // Standalone scissors: click a segment to trim/extend it (immediate, no card): + Trim, Extend, + // In-canvas transform TOOLBAR tools (pick targets + drag handle/label, no card): + Move, Rotate, Scale, Array, PolarArray, + // In-canvas bounding-box transform for imported Text/SVG art: + TransformArt, + Constrain }; + bool is_edit_op_mode() const { return m_mode == Mode::Fillet || m_mode == Mode::Chamfer || + m_mode == Mode::Offset || m_mode == Mode::Mirror; } + bool is_transform_mode() const { return m_mode == Mode::Move || m_mode == Mode::Rotate || + m_mode == Mode::Scale || m_mode == Mode::Array || + m_mode == Mode::PolarArray; } + // Creation tools that get draw-then-edit: on commit the new entity/feature is selected + // and its primary value editor opens. Line is handled inline (its own length field); + // Polyline/BSpline/Point have no single primary value, so they opt out. + bool is_creation_autoedit_mode() const { + switch (m_mode) { + case Mode::Line: + case Mode::CornerRect: case Mode::CenterRect: case Mode::ObliqueRect: + case Mode::RoundedRect: case Mode::CenterCircle: case Mode::TwoPointCircle: + case Mode::ThreePointCircle: case Mode::ThreePointArc: case Mode::TangentArc: + case Mode::CenterArc: case Mode::Slot: case Mode::ArcSlot: case Mode::Polygon: + case Mode::Ellipse: case Mode::EllipseArc: + return true; + default: return false; + } + } + // The host (DesignCanvas) flags the canvas frozen while an inline value editor is open, + // so a stray click/move can't draw under the floating field. Reuses m_awaiting_length + // (Line's existing freeze flag) as the single "inline editor open" gate. + void set_inline_busy(bool b) { m_awaiting_length = b; } + bool inline_busy() const { return m_awaiting_length; } // true while a value field is open + bool constrain_value_anchor(wxPoint& out) const; // screen anchor over the picked constrain geometry + + void begin(const SketchPlane& plane, Mode mode = Mode::Polyline); + // Re-open a committed entity sketch for full in-canvas editing: load its entities + + // driving constraints, re-detect the polygon/rect/slot grouping, and live-solve. The + // caller re-commits via finish() (the panel replaces the feature, see m_edit_index). + void begin_edit(const std::vector& entities, + const std::vector& constraints, + const SketchPlane& plane); + void set_tool(Mode mode); // switch tool, keep accumulated entities + void set_construction(bool c) { m_construction = c; } + void set_polygon_sides(int n) { m_polygon_sides = (n < 3 ? 3 : n); } + void set_polygon_circumscribed(bool c) { m_polygon_circumscribed = c; } + void finish(); // emit accumulated entities, end session + void cancel(); + bool is_active() const { return m_active; } + bool has_entities() const { return !m_entities.empty(); } + bool on_mouse(wxMouseEvent& evt, GLCanvas3D& canvas); + void render(GLCanvas3D& canvas); + + // Persistent committed sketches to draw even when no session is active (e.g. an + // un-consumed sketch left visible after its extrude is removed). Each carries its + // own plane. render() draws these as translucent faces + outlines. + struct DisplaySketch { std::vector entities; SketchPlane plane; int feature{-1}; }; + void set_display_sketches(std::vector ds) { m_display_sketches = std::move(ds); } + bool has_display() const { return m_active || !m_display_sketches.empty() + || (m_solid_bodies != nullptr && !m_solid_bodies->empty()) + || !m_datum_planes.empty() + || m_ex_active || m_mv_active || m_fl_active + || m_hl_active || m_th_active || m_sh_active; } + + // Solid topology selection on the committed bodies: clicking a solid cycles + // whole-solid -> face -> edge (Onshape-style) to target fillet/chamfer/extrude. With + // multiple bodies the pick resolves WHICH body was hit (per-triangle body id). + enum class SolidSel { None, Whole, Face, Edge }; + // Point the tool at the current bodies + their concatenated tessellation (non-owning; + // pass nullptr to clear). Call after each recompute — selection resets (ids invalidate). + // tri_face = per-triangle face id within its body; tri_body = per-triangle body index. + void set_solid_pick(const std::vector* bodies, const TriangleMesh* mesh, + const std::vector* tri_face, const std::vector* tri_body, + const std::vector* visible = nullptr, + const std::vector* xform = nullptr); + void clear_solid_selection(); + // Select a whole body by index (from the Parts list) — Whole-level highlight, no face/edge. + // body < 0 or out of range clears the selection. + void select_body(int body); + + // Move-body gizmo (M5): translate a whole body with three world-axis drag arrows + // (X red / Y green / Z blue) anchored at the body centroid. Display-only — the host + // keeps a per-body Transform3d and re-feeds the moved display/pick meshes; the OCCT + // shape (and thus face/edge global ids) is never touched. Drag fires on_body_move_changed + // live; a stationary click on an arrow opens the inline offset editor for that axis. + void set_move_gizmo(int body, const Vec3d& pivot, const Transform3d& base_xform); + void clear_move_gizmo(); + bool moving_body() const { return m_mv_active; } + int move_body_index() const { return m_mv_body; } + // F key forwarded from the canvas (Prepare's Place on Face): returns true if it acted. + bool request_place_on_face() { return on_place_on_face ? on_place_on_face() : false; } + std::function on_place_on_face; + std::function on_body_move_changed; + // Fired on each cycle change: (level 0=None/1=Whole/2=Face/3=Edge, body index, face id, edge id). + std::function on_solid_selection_changed; + // Click a committed sketch overlay (no live session) -> select that loop: the Sketch + // feature index + the clicked closed-region index within it (-1 = no specific loop). + std::function on_display_sketch_selected; + // Entities forming the currently click-selected loop (for a per-loop extrude); empty + // if no loop is selected. + std::vector selected_loop_entities() const; + // Per closed loop, the indices into `ents` that form it (for hiding already-extruded + // loops from the committed-sketch overlay). + std::vector> region_entity_indices(const std::vector& ents) const; + void clear_display_pick() { m_display_pick = -1; m_display_pick_region = -1; } + + // Visual Extrude gizmo (C5b). The Extrude tool is a DesignPanel docked card, so the + // sketch tool is NOT active during it; the panel feeds the profile plane + a 2D centroid + // (arrow anchor) + the live depths/flags, and the tool renders an in-canvas world-space + // depth arrow along plane.normal with a draggable handle + editable label. TwoSided draws + // a second arrow along -normal driven by depth2. Drag/edit fire on_extrude_depth_changed + // back to the panel, which writes the spin value + refreshes the ghost preview. + void set_extrude_gizmo(const SketchPlane& plane, const Vec2d& centroid, + double depth, double depth2, bool two_sided, bool flip); + void clear_extrude_gizmo(); + // (new_depth, second_side): second_side=false drives the primary depth, true the 2nd side. + std::function on_extrude_depth_changed; + + // Visual Fillet/Chamfer gizmo. The Dressup tool is a DesignPanel docked card, so the sketch + // tool is NOT active during it; when a solid EDGE is picked the panel passes the body centroid + // + current radius and the tool anchors a world-space radius arrow at the picked edge midpoint + // (from m_sel_edge_pts), perpendicular to the edge, pointing outward (away from the centroid). + // Dragging the arrow changes the radius live; a stationary click opens the inline editor; both + // fire on_fillet_radius_changed back to the panel, which writes the spin + refreshes the ghost. + // Returns true if it could anchor (needs a picked edge with >=2 sample points). + bool set_fillet_gizmo(const Vec3d& body_centroid, double radius); + void clear_fillet_gizmo(); + bool filleting() const { return m_fl_active; } + std::function on_fillet_radius_changed; + + // Visual Hole gizmo. Like Dressup, the Hole tool is a DesignPanel docked card, so the sketch + // tool is NOT active during it; the panel passes the hole plane + position + diameter + depth + + // through flag, and the tool draws an on-plane footprint circle plus a radial diameter arrow, + // a normal-axis depth arrow (only when !through), and a draggable centre marker. Dragging the + // centre repositions (plane u/v), the diameter arrow resizes, the depth arrow deepens — all + // live; a stationary click on an arrow opens its inline editor. Every change fires + // on_hole_changed back to the panel, which writes the spins + refreshes the ghost. + void set_hole_gizmo(const SketchPlane& plane, double x, double y, + double diameter, double depth, bool through); + // Provide the face (u,v) bounds so the hole's construction dims read from the face sides. + void set_hole_face_bounds(bool has, double umin, double umax, double vmin, double vmax); + void clear_hole_gizmo(); + bool holing() const { return m_hl_active; } + std::function on_hole_changed; + + // Visual Thread gizmo. Same docked-card story as Hole: the panel feeds the thread plane + + // axis position + nominal radius + length; the tool draws an on-plane footprint circle plus a + // radial radius arrow and a normal-axis length arrow (always shown — a thread has no "through") + // and a draggable centre. Pitch/depth/internal stay in the card. Drag is live; a stationary + // click on an arrow opens its inline editor; every change fires on_thread_changed. + void set_thread_gizmo(const SketchPlane& plane, double x, double y, + double radius, double height); + void clear_thread_gizmo(); + bool threading() const { return m_th_active; } + std::function on_thread_changed; + + // Visual Shell gizmo. The panel passes the picked open-face centroid + an inward direction + // (-outward normal) + the current wall thickness; the tool anchors a single thickness arrow + // there (mirrors the fillet radius arrow). Dragging sets the thickness live; a stationary + // click opens the inline editor; both fire on_shell_thickness_changed. + void set_shell_gizmo(const Vec3d& face_centroid, const Vec3d& inward_dir, double thickness); + void clear_shell_gizmo(); + bool shelling() const { return m_sh_active; } + std::function on_shell_thickness_changed; + + // Datum/reference planes (Plane feature) carry no solid; the panel feeds their resolved + // SketchPlanes so they render as translucent rectangles in feature mode (otherwise a + // Plane feature is invisible in the canvas). + void set_datum_planes(std::vector planes) { m_datum_planes = std::move(planes); } + + // Visual Revolve gizmo. The panel feeds the sketch plane + profile centroid + axis (0=plane X, + // 1=plane Y) + angle + flip while its Revolve card is open; an angle-arc is drawn in the + // revolve plane at the profile radius. Dragging the tip sweeps the angle, a stationary click + // edits it; both fire on_revolve_angle_changed. + void set_revolve_gizmo(const SketchPlane& plane, const Vec2d& centroid, + int axis_sel, double angle, bool flip); + void clear_revolve_gizmo(); + bool revolving() const { return m_rv_active; } + std::function on_revolve_angle_changed; + + // Visual Pattern gizmo. Linear: a 3D arrow along the world axis (plane X/Y per `dir`) of length + // spacing*(count-1) with a tick at each copy; dragging the end sets the spacing. Circular: a + // revolve-style angle-arc about the plane normal through the plane origin sweeping `angle`. + // Both fire on_pattern_changed (spacing for linear, angle for circular). + void set_pattern_gizmo(const SketchPlane& plane, const Vec3d& body_centroid, bool circular, + int count, int dir, double spacing, double angle); + void clear_pattern_gizmo(); + bool patterning() const { return m_pt_active; } + std::function on_pattern_changed; + + // Constrain mode: load an already-committed profile for entity picking + + // constraint application (the geometry is solved in the kernel, not here). + void begin_constrain(const SketchProfile& prof, const SketchPlane& plane); + bool is_constraining() const { return m_active && m_mode == Mode::Constrain; } + // Replace the displayed profile (e.g. after the kernel re-solved it). + void set_profile_points(const std::vector& pts) { m_points = pts; } + // The currently picked segment's endpoint indices into the profile. + bool selected_segment(int& a, int& b) const; + + // Entity-aware Constrain (Fase 4.2): load a committed entity sketch and pick + // Line entities (constraints are solved against entity endpoints in the kernel). + void begin_constrain_entities(const std::vector& ents, const SketchPlane& plane); + bool is_constraining_entities() const { return m_active && m_mode == Mode::Constrain && m_constrain_entities; } + + // In-canvas bounding-box transform of imported Text/SVG art (replaces the Move/Scale + // dialog). `base_regions` are the untransformed region contours; the gizmo shows the + // current bbox with 4 corner scale-handles + a centre move-handle. Dragging fires + // on_imported_transform live with the new offset/scale, which the host writes back to + // the feature. Exiting (Esc/right-click) ends the session. + void begin_imported_transform(int feat, + const std::vector>>& base_regions, + const SketchPlane& plane, const Vec2d& offset, + double scale_x, double scale_y); + std::function on_imported_transform; + // Up to two picked line-entity indices; returns true if at least one is picked. + bool selected_constrain_entities(int& e0, int& e1) const { e0 = m_pick0; e1 = m_pick1; return m_pick0 >= 0; } + // Third pick slot (Symmetric axis): only filled after slots 0 and 1 are set. + int pick2() const { return m_pick2; } + // Plane-coords of the click that filled slot 0 (for pick-point edit ops: trim/extend). + bool pick0_point(Vec2d& out) const { out = m_pick0_pt; return m_pick0 >= 0; } + // Refresh the displayed entities after the kernel re-solved them. + void set_constrain_entities(const std::vector& ents) { m_entities = ents; } + // Constraint manager (C3.4): entity indices the panel asks to highlight (the + // entities a selected constraint references); rendered yellow in Constrain mode. + void set_constraint_highlight(std::vector v) { m_constraint_hl = std::move(v); } + // The committed feature's constraints, supplied so Constrain-mode render can draw + // an iconic glyph badge per constraint near its primary entity (C3.4b). + void set_constraint_glyphs(std::vector v) { m_constrain_cons = std::move(v); } + + // Line tool: after a single segment is placed, the panel pops a length dialog + // (length, angle_deg are the as-drawn values); it then resolves via + // apply_segment_length() (exact length) or keep_segment_as_drawn() (cancel). + std::function on_segment_drawn; + void apply_segment_length(double len); // rescale the pending segment, then commit it + void keep_segment_as_drawn(); // commit the pending segment unchanged + + // Live readout while drawing a Line/Polyline segment (anchor->cursor metrics). + std::function on_cursor_metrics; + + // DoF feedback (P3): solver state after each live solve. dof>0 = under-constrained, + // dof==0 = fully constrained, ok==false = conflicting/inconsistent constraints. + // has_constraints is false while the sketch carries no driving constraints yet. + std::function on_solve_state; + + // Selection (Mode::Select): pick points/lines/arcs/circles of the in-session + // sketch; Shift/Ctrl extends, double-click grabs the whole connected loop. + const std::vector& selection() const { return m_selection; } + void clear_selection(); + void delete_selected(); // erase selected entities + std::function on_selection_changed; + + // Dimension tool: infer a driving dimension from the current selection and set + // it exactly. Sizing: 1 line=Length, 1 circle=Diameter, 1 arc=Radius, + // 2 lines=Angle. Positioning (a value of 0 makes them coincident): + // 2 point-likes (point/circle-centre/arc-centre)=Distance, moving the 2nd onto + // the 1st; a point-like + a line=DistanceToLine, moving the point-like's + // reference point onto/away-from the line (e.g. a circle centre onto an axis). + enum class DimType { None, Length, Diameter, Radius, Angle, Distance, DistanceToLine }; + DimType dimension_kind() const; // what the selection supports (None if invalid) + double dimension_current() const; // current value, to pre-fill the dialog + void apply_dimension(double v); // set it exactly, then clear the selection + + // Onshape-style Dimension tool (Mode::Dimension): with the tool active you click + // directly in the viewport — 2 points -> Distance, a line -> Length, a circle -> + // Diameter, an arc -> Radius, a point then a line -> DistanceToLine. A quote line + // with extension lines, arrowheads and a numeric label is PLACED in the sketch and + // drives the geometry (auto-offset; label editable). on_dimension_pick_complete + // fires when a pick resolves so the panel can pop the value card pre-filled. + std::function on_dimension_pick_complete; + DimType pending_dimension_type() const; // type of the dim awaiting a value, or None + void set_dimension_value(double v); // apply the typed value to the placed dim + void cancel_dimension_value(); // keep the placed dim at its measured value + + // Onshape-style in-canvas value editing: open a floating text editor at the given + // screen pixel, pre-filled with `current`; commit applies the value, cancel keeps + // it. The owner (DesignCanvas) hosts the wxTextCtrl over the GL canvas. This is the + // single numeric-entry path for all sketch dimensions (replaces the modal cards). + std::function commit, + std::function cancel)> on_inline_edit; + // Force-close any open inline field (runs its cancel = keep-as-drawn). Used by the polyline + // terminators (right-click / double-click) to end the chain even mid per-segment edit. + std::function on_inline_dismiss; + + // Bottom-right viewport readout: emitted each frame with the active tool's current + // values (live segment length/angle while drawing a line, or the selected entity's + // characteristic dimensions). Empty string -> hide the HUD. The owner (DesignCanvas) + // shows it as a floating corner label over the GL canvas. + std::function on_readout; + + // Driving dimension constraints accumulated during the session (the Dimension + // tool records a SketchEntityConstraintDef per applied dimension); committed + // alongside the entities on finish() so the kernel keeps enforcing them. + const std::vector& constraints() const { return m_constraints; } + + // Emitted by finish() with the accumulated entities + driving constraints. + std::function&, + const std::vector&, + const SketchPlane&)> on_commit_entities; + // Legacy single-profile commit (kept for compatibility; unused by entity tools). + std::function on_commit; + // Emitted when a closed-loop face is clicked in Select mode (Onshape: a region + // becomes a selectable face → extrude). The panel commits the sketch + extrudes. + std::function on_face_selected; + // Esc pressed while the tool is active: exit/cancel the session (the panel restores + // Feature mode). Layered: an in-progress entity or a non-Select draw tool is dropped + // first; a second Esc exits the session. + std::function on_exit; + std::function on_move_exit; // right-click finished the move-body gizmo + void request_exit(); + // Ctrl+Z / Ctrl+Shift+Z (Ctrl+Y) while the Design canvas is focused: undo/redo the + // committed feature history. The tool just forwards to the host, which owns the + // CadDocument (the tool has no document of its own). redo == true requests redo. + std::function on_undo_redo; + void request_undo_redo(bool redo); + +private: + bool screen_to_plane(GLCanvas3D& canvas, const wxMouseEvent& evt, Vec2d& out) const; + bool near_first(const Vec2d& p) const; + + // Onshape-style angle inference: snap the direction anchor->raw to the nearest + // of {0,30,45,60,90} deg (replicated every 90 deg) when within tolerance, keeping + // the same length. Sets `locked` when a snap was applied. Suppressed by m_snap_off. + Vec2d snap_dir(const Vec2d& anchor, const Vec2d& raw, bool& locked) const; + // Snap a placed point onto the nearest existing entity endpoint within ~8 px so + // chains join across entities (a line + an arc can close into one loop). Shift + // disables it. `snapped` reports whether a vertex was hit. + Vec2d snap_vertex(GLCanvas3D& canvas, const wxMouseEvent& evt, const Vec2d& raw, bool& snapped) const; + + // --- P1 inference / auto-constraint engine --------------------------------- + // Plane-units tolerance equivalent to ~`px` screen pixels at the cursor. + double screen_tol(GLCanvas3D& canvas, const wxMouseEvent& evt, const Vec2d& at, double px = 8.0) const; + // Run kernel inference at the cursor, cache the target for the hint renderer. + InferenceSnap infer_at(GLCanvas3D& canvas, const wxMouseEvent& evt, const Vec2d& raw) const; + // True if m_constraints already holds an equivalent Coincident between the two refs. + bool has_coincident(int ea, SketchPointRole ra, int eb, SketchPointRole rb) const; + // Append candidates, live-solve, and roll back the batch if it turns the system + // inconsistent. Returns true when the batch was kept. + bool try_add_constraints(const std::vector& cands); + // After entities [base, end) were committed, auto-emit the constraints that make + // the new geometry stick: Coincident between co-located endpoints (so loops close + // on their own) and Horizontal/Vertical on axis-aligned new segments. + void infer_auto_constraints(int base); + + // Selection helpers (Mode::Select). + int hit_test(const Vec2d& p, double tol) const; // nearest entity within tol, or -1 + std::vector connected_loop(int seed) const; // entities joined by shared endpoints + void apply_angle_between(int ia, int ib, double deg); // rotate line B to set the A^B angle + bool selection_valid() const; // all selection indices in range + void record_dimension_constraint(double v); // append the driving def for the selection + void resolve_live(); // solve accumulated constraints on m_entities now + // Drag-aware re-solve: pins the dragged point at its current coord and lets the + // solver move the rest (Slvs dragged[]). Used live while a point grab is active. + void resolve_live_drag(int dragged_ei, SketchPointRole dragged_role); + + // Placed dimension annotation. References entity points/entities (not cached + // coords) so the quote follows the geometry as the kernel solves it. `value` + // drives the constraint stored at index `con` in m_constraints. + struct DimAnnot { + DimType kind{DimType::None}; + int ea{-1}; SketchPointRole ra{SketchPointRole::P0}; + int eb{-1}; SketchPointRole rb{SketchPointRole::P0}; + double value{0.0}; + double side{1.0}; // perpendicular offset sign of the quote line + int con{-1}; // slot in m_constraints driving this dimension + Vec2d label_pos{0, 0}; // cached label centre (plane coords), for picking + }; + + // --- Onshape-style visual editing: handles + parametric feature grouping ----- + // A draggable handle on a defining point of an entity (or a derived point of a + // feature group). GUI-only; recomputed from solved geometry every frame (never + // persisted), so handles always track the current solve. Derived roles (radius, + // slot width/centres, rect corners, polygon vertex, ellipse axes) let tools that + // decompose into raw Line/Arc entities still expose their parametric controls. + enum class HandleRole { P0, P1, Center, RadiusHandle, + SlotCenter0, SlotCenter1, SlotWidth, + RectCorner, PolygonVertex, MajorAxis, MinorAxis, BSplineCtrl }; + struct Handle { + HandleRole role{HandleRole::P0}; + int ei{-1}; // primary entity index + int group{-1}; // index into m_features, or -1 for a raw-entity handle + int ctrl_index{-1}; // BSplineCtrl pole index + Vec2d pos{0, 0}; // current plane coords (recomputed each frame) + bool hovered{false}; + }; + // A parametric grouping over a contiguous run of entities produced by one gesture. + // Slot/Rect/Polygon/etc. have no SketchEntity type of their own — they decompose + // into raw Line/Arc entities — so the Feature carries the gesture's anchors so + // derived handles + characteristic dimensions can be reconstructed. + enum class FeatureKind { Free, Line, Circle, Arc, CornerRect, CenterRect, + Slot, ArcSlot, Polygon, Ellipse, RoundedRect, BSpline }; + struct Feature { + FeatureKind kind{FeatureKind::Free}; + int begin{0}, end{0}; // [begin,end) into m_entities + Vec2d c0{0, 0}, c1{0, 0}; // slot centres / rect corners / ellipse centre+major + double param{0.0}; // slot half-width / polygon circumradius / fillet radius + int sides{0}; // polygon side count + }; + // Build the live handle set for the current selection / just-drawn feature. + std::vector build_handles() const; + // Nearest handle to plane-point p within tol; fills `out`. (Phase A: stub.) + bool hit_test_handle(const Vec2d& p, double tol, Handle& out) const; + // Move a handle to `target`, applying the role-specific geometry edit + re-solve. + void set_handle(const Handle& h, const Vec2d& target); + // On a no-button move, recompute the hovered handle; returns true iff it changed + // (so the caller forces exactly one repaint). No-op for non-Moving events. + bool update_hover(GLCanvas3D& canvas, wxMouseEvent& evt); + // Index of the Feature whose [begin,end) entity span contains ei, or -1. + int feature_of(int ei) const; + // Re-detect parametric Feature groups (polygon / rect / slot) from the raw entity + // list — used when a committed sketch is re-opened, where m_features is empty. + void rebuild_features_from_entities(); + // Open/close a Feature record around the entities a single gesture appends. + void begin_feature(FeatureKind kind); + void end_feature(const Vec2d& c0 = Vec2d(0, 0), const Vec2d& c1 = Vec2d(0, 0), + double param = 0.0, int sides = 0); + + bool point_at(int ei, SketchPointRole role, Vec2d& out) const; // current coords + void set_point(int ei, SketchPointRole role, const Vec2d& v); // move an entity point + bool hit_test_point(const Vec2d& p, double tol, int& ei, SketchPointRole& role) const; + int hit_test_dimension(const Vec2d& p, double tol) const; // nearest dim label + void edit_dimension(int di); // reopen value card for di + // Representative plane-coords anchor of a dimension (label centre if known, else a + // geometric midpoint/centre) — where the in-canvas value editor is positioned. + Vec2d dim_anchor(const DimAnnot& a) const; + // Open the in-canvas value editor on dimension `di` (falls back to the modal + // pick-complete callback when no inline-edit host is wired). + void open_value_editor(int di); + // In-canvas editor for a line's angle-to-horizontal; commit rotates the segment + // geometrically about P0 (no single-line angle constraint in libslvs). + void open_angle_editor(int ei); + void set_line_angle(int ei, double deg); + // Draw-then-edit (all creation tools): open the inline editor on the freshly-drawn + // selection's PRIMARY characteristic value. Called after render_live_quotes has computed + // the selection's quotes, so it dispatches on the same live-quote state a Select-mode + // click would use. + void open_primary_autoedit(); + // Compact "current values" string for the bottom-right HUD (see on_readout). + std::string build_readout() const; + // Open a characteristic live quote as a TENTATIVE driving dimension: the constraint is + // appended only if the user commits a value (Enter); cancel (Esc) adds nothing — so + // drawing never silently over-constrains. (place_dimension is the eager Select-mode twin.) + void open_next_autoedit_dim(); // opens m_autoedit_dims[idx]; commit -> next, Esc -> stop + void arm_polyline_segment_edit();// per-segment Length+Angle edit of the pending chain vertex + // In-canvas editors for a regular polygon's side length and orientation. Both edit + // the whole loop GEOMETRICALLY (polygon has no centre entity): side scales it + // uniformly about its centre, angle rotates it. set_polygon_radius is the shared + // uniform-scale primitive (circumradius). + void open_polygon_side_editor(int fi); + void open_polygon_angle_editor(int fi); + void set_polygon_side(int fi, double side); + void set_polygon_angle(int fi, double deg); + void set_polygon_radius(int fi, double R); + // Arc sweep-angle quote: geometric edit (SLVS angle is line-to-line only). Keeps the + // arc start point + radius fixed and moves the end point to span `deg` degrees. + void open_arc_angle_editor(int ei); + void set_arc_sweep(int ei, double deg); + // Arc handle drag (3 grips): Center rigidly translates; the START point changes the + // radius (keeps both sweep angles); the END point changes the sweep angle (keeps the + // radius). Geometric — no solver (SLVS has no arc radius/angle handle concept here). + void drag_arc_handle(int ei, SketchPointRole role, const Vec2d& target); + // Ellipse axis labels (geometric edit of the semi-axes a/b; phi via the major grip). + void open_ellipse_axis_editor(int ei, bool major); + void set_ellipse_axis(int ei, bool major, double v); + void set_ellipsearc_sweep(int ei, double deg); // draw-then-edit: included sweep of an elliptical arc + void set_rect_angle(int fi, double deg); // draw-then-edit: orientation of an oblique rect + // EllipseArc endpoint drag: Center translates; P0/P1 move the sweep start/end to the + // parametric angle of the cursor on the ellipse frame (radius/shape preserved). + void drag_ellipsearc_handle(int ei, SketchPointRole role, const Vec2d& target); + // Drop orientation constraints (H/V/Parallel/Perp/Angle/LockX/LockY) on entities in + // [begin,end). A ROTATION makes inferred per-edge H/V inconsistent, so re-solving + // against them collapses the shape — drop them first (fixes up DimAnnot.con indices). + void drop_orientation_constraints(int begin, int end); + // Drop every live constraint that references entity `ei` (Trim/Extend slide an endpoint, + // invalidating its constraints) and fix the dimensions' cached constraint indices. + void drop_constraints_referencing(int ei); + // Standalone Trim/Extend scissors on the LIVE sketch: pick the entity nearest `p` (within + // `tol` plane units) and cut it back to / out to its nearest intersection with the others. + // Returns true if an entity was modified. + bool apply_live_trim(const Vec2d& p, double tol, bool extend); + // Pure-computation hover preview for Trim/Extend: mirror apply_live_trim's pick + the + // engine's cut on a COPY (mutating nothing) and return, via `removed_poly`, the polyline + // of the sub-portion a click would REMOVE (Trim) or ADD (Extend). `subject_ei` is the + // picked entity. Returns false if nothing is in range or nothing would change. + bool compute_trim_preview(const Vec2d& p, double tol, bool extend, + int& subject_ei, std::vector& removed_poly) const; + // Drag a polygon vertex while keeping the loop REGULAR: scale + rotate the whole + // polygon about its centroid so the grabbed vertex follows `target` (adjusts + // circumradius + orientation together). + void drag_polygon_vertex(int fi, int ei, SketchPointRole role, const Vec2d& target); + double measure_dim(const DimAnnot& a) const; // value from geometry + SketchEntityConstraintDef constraint_for(const DimAnnot& a) const; // driving def + int place_dimension(DimAnnot a); // create+drive+notify + std::string dim_text(const DimAnnot& a) const; // rendered label string + void render_dimensions(double unit_per_px); // quote lines + labels + // Draw ONE dimension's quote (extension/dimension lines, arrowheads, label) and + // return its label centre in out_label; false if the annot can't be drawn. Shared + // by render_dimensions (placed driving quotes) and render_live_quotes (live ones). + bool draw_dim_quote(const DimAnnot& a, double th, const ColorRGBA& col, Vec2d& out_label); + // Live, non-driving characteristic quotes for the entity being edited (point/handle + // drag, or a lone selection): the tool's defining dimensions shown Onshape-style so + // editing shows live values; click one (m_live_quotes) to promote it to a driving + // dim. Self-gates; skips a dim already driven on that entity. + void render_live_quotes(double unit_per_px); + // Iconic constraint badges (C3.4b): for each m_constrain_cons entry, append a + // small screen-constant glyph (H, V, ∥, ⊥, =, ○, …) near its primary entity into + // `out`; glyphs touching the same entity stack so they don't overlap. + void build_constraint_glyphs(double unit_per_px, std::vector>& out) const; + void draw_strokes(GLModel& model, const std::vector>& segs, + double hw, const ColorRGBA& color); + void draw_text(GLModel& model, const std::string& s, const Vec2d& center, + double height, const ColorRGBA& color); // GL stroke font + + // Entity builders: append to m_entities (honoring the construction flag). + void push_line(const Vec2d& a, const Vec2d& b); + void push_closed_lines(const std::vector& corners); + void push_open_chain(const std::vector& pts); + void push_circle(const Vec2d& center, double radius); + void push_point(const Vec2d& p); + + // Multi-click tool builders: return the entities for a finished gesture so + // both on_mouse (append) and render (preview) share one geometry path. + std::vector make_three_point_circle(const Vec2d& a, const Vec2d& b, const Vec2d& c) const; + std::vector make_three_point_arc(const Vec2d& start, const Vec2d& end, const Vec2d& on_arc) const; + std::vector make_tangent_arc(const Vec2d& start, const Vec2d& end) const; + // Center-start-end arc: click center, then start (sets radius), then a third + // point whose direction from the center sets the CCW end angle. + std::vector make_center_arc(const Vec2d& center, const Vec2d& start, const Vec2d& end_dir) const; + std::vector make_slot(const Vec2d& c0, const Vec2d& c1, double half_width) const; + std::vector make_arc_slot(const Vec2d& center, const Vec2d& start, + const Vec2d& end_dir, double half_width) const; + std::vector make_rounded_rect(const Vec2d& a, const Vec2d& b, const Vec2d& radius_pt) const; + std::vector rounded_rect_entities(double xmin, double ymin, + double xmax, double ymax, double r) const; + // Rounded-rect grouped edit: W/H/fillet-R labels rebuild the 8-entity span in place. + void open_rounded_rect_editor(int fi, int which); // 0=Width 1=Height 2=fillet R + void set_rounded_rect(int fi, double w, double h, double r); + // Arc-slot grouped edit: centreline-radius + width labels rebuild the 4-arc span. + void open_arc_slot_editor(int fi, bool radius); // true=centreline R, false=width + void set_arc_slot(int fi, double Rc, double w); + // Grouped derived-handle drag: resize an axis-aligned rect by a corner (opposite corner + // fixed); move a slot end by its cap centre. Both rebuild the feature span geometrically. + void drag_rect_corner(int fi, const Vec2d& cursor); + void drag_slot_handle(int fi, const Vec2d& cursor); + std::vector make_polygon(const Vec2d& center, const Vec2d& vertex, int sides) const; + // Ellipse: click center, then major-axis endpoint (sets a + rotation phi), + // then a point whose perpendicular distance to the major axis sets b. + std::vector make_ellipse(const Vec2d& center, const Vec2d& major_end, + const Vec2d& minor_pt) const; + // Elliptical arc: same 3 axis clicks, then start and end points whose parametric + // angles on the ellipse bound the CCW sweep. + std::vector make_bspline(const std::vector& ctrl) const; + std::vector make_ellipse_arc(const Vec2d& center, const Vec2d& major_end, + const Vec2d& minor_pt, const Vec2d& start_pt, + const Vec2d& end_pt) const; + void append_entities(const std::vector& ents); + void draw_entities_preview(const std::vector& ents, const ColorRGBA& color); + + // --- In-canvas edit-op gizmo (Fillet/Chamfer/Offset/Mirror toolbar tools) -------- + // These replace the docked numeric card: pick the entities in-canvas, then a draggable + // arrow with a value label is projected toward the corner/centre (Fillet/Chamfer/Offset), + // or a two-phase pick (axis line, then targets) drives a live mirrored ghost. The + // SketchEngine op is recomputed live so a translucent ghost previews the result; confirm + // applies the geometry and binds constraints into m_constraints (try_add_constraints). + bool op_corner(int a, int b, Vec2d& C, Vec2d& bis, double& theta) const; // line-line vertex + inward bisector + void op_pick(int ei); // route an entity pick to the active op + void recompute_op_ghost(); // rebuild m_op_ghost from m_op_value + void render_op_gizmo(double unit_per_px); // ghost + arrow + value label (caches m_op_label) + bool hit_test_op_arrow(const Vec2d& p, double tol) const; + void drag_op_arrow(const Vec2d& target); // project cursor onto m_op_dir -> value + void open_op_editor(); // inline-edit the value label + void confirm_op(); // apply + bind, then reset for the next gesture + void reset_op(); // clear gizmo state (keeps the tool active) + bool op_ready() const; // required entities picked -> arrow/ghost live + + // Sample an entity into a 2D polyline for the overlay renderer. + std::vector entity_polyline(const SketchEntity& e, bool& closed) const; + + // Closed regions formed by the current (non-construction) entities: each a CCW- + // ordered boundary polygon on the plane. A circle is its own region; line/arc + // chains are walked endpoint-to-endpoint into loops. Used to fill faces. + std::vector> closed_regions() const; + std::vector> closed_regions(const std::vector& ents) const; + // Same loops, but each carries the indices of the entities that form it — so a single + // loop can be highlighted / extruded on its own (per-region selection on the plate). + struct RegionLoop { std::vector poly; std::vector ents; }; + std::vector region_loops(const std::vector& ents) const; + // Index of the closed region containing plane-point p (point-in-polygon), or -1. + int region_at(const Vec2d& p) const; + + void draw_quad_strip(GLModel& model, const std::vector& pts, bool closed, const ColorRGBA& color); + // half_size is the square marker half-extent in PLANE units. Callers pass a + // zoom-scaled value (k / zoom) for screen-constant handles; the default keeps + // legacy point markers exactly as before. + void draw_vertices(GLModel& model, const std::vector& pts, const ColorRGBA& color, + double half_size = 1.3); + void draw_fill(GLModel& model, const std::vector& poly, const ColorRGBA& color); + + bool m_active{false}; + SketchPlane m_plane; + std::vector m_points; // clicks of the in-progress entity / chain + std::vector m_entities; // committed entities of this session + bool m_construction{false}; + int m_polygon_sides{6}; + bool m_polygon_circumscribed{false}; + Vec2d m_cursor{0,0}; + bool m_has_cursor{false}; + bool m_snap_off{false}; // Shift held -> suppress angle snapping + InferenceSnap m_cursor_snap; // last cursor inference target (for hint render) + bool m_cursor_locked{false}; // rubber-band segment is angle-locked + bool m_awaiting_length{false}; // inline value editor open -> freeze canvas + int m_autoedit_seen{-1}; // entity count baseline for draw-then-edit + bool m_autoedit_pending{false};// a new entity just committed -> open editor + // Draw-then-edit step queue: every characteristic dimension of the freshly-drawn shape + // (scalar quote OR geometric editor) becomes one step, opened in sequence over its label. + struct AutoEditStep { + Vec2d label; // anchor (plane coords) — field opens over this + double value; // initial value shown + std::function apply; // commit: set the dimension + std::vector hi; // entities to highlight while THIS field is open + }; + std::vector m_autoedit_dims; // queued steps to edit in sequence + int m_autoedit_dim_idx{-1}; // index into m_autoedit_dims (-1 = idle) + std::vector m_selection; // selected entity indices (Mode::Select) + std::vector> m_point_sel; // selected individual points + int m_last_mouse_x{0}; // last cursor pos (canvas client px), for + int m_last_mouse_y{0}; // anchoring the in-canvas value editor + bool m_dragging_point{false}; // a point grab is in progress (Mode::Select) + int m_drag_ei{-1}; // entity whose point is being dragged + int m_drag_poly_fi{-1}; // >=0 if the grabbed point is a polygon + // vertex: drag scales+rotates the loop + int m_drag_rect_fi{-1}; // >=0 if dragging an axis-aligned rect corner + Vec2d m_drag_rect_anchor{0,0}; // the fixed (opposite) corner + int m_drag_slot_fi{-1}; // >=0 if dragging a slot cap centre + bool m_drag_slot_c1{false}; // true=cap@c1, false=cap@c0 + SketchPointRole m_drag_role{SketchPointRole::P0}; + std::vector m_constraints; // driving dims, committed on finish + + // Onshape-style visual editing state. + bool m_show_handles{false}; // draw + interact with handles + bool m_dragging_handle{false};// a handle grab is in progress + Handle m_drag_handle; // the handle being dragged + bool m_has_hover_handle{false};// cursor is near a handle (highlight it) + Handle m_hover_handle; // the hovered handle (recomputed on move) + std::vector m_live_quotes; // live non-driving characteristic quotes, + // clickable to promote to driving dims + Vec2d m_live_poly_side_label{0,0}; // polygon side-length quote label + Vec2d m_live_poly_angle_label{0,0}; // polygon orientation quote label + int m_live_poly_fi{-1}; // their Feature (geometric edits) + Vec2d m_live_arc_angle_label{0,0}; // arc sweep-angle quote label + int m_live_arc_ei{-1}; // the arc it belongs to (geometric edit) + Vec2d m_live_ellipse_major_label{0,0}; // ellipse semi-major quote label + Vec2d m_live_ellipse_minor_label{0,0}; // ellipse semi-minor quote label + Vec2d m_live_ellipsearc_sweep_label{0,0}; // elliptical-arc sweep quote label + int m_live_ellipse_ei{-1}; // the ellipse the labels belong to + Vec2d m_live_obrect_angle_label{0,0}; // oblique-rect orientation quote label + int m_live_obrect_fi{-1}; // an OBLIQUE rect Feature (angle editable) + Vec2d m_live_rrect_w_label{0,0}; // rounded-rect width quote label + Vec2d m_live_rrect_h_label{0,0}; // rounded-rect height quote label + Vec2d m_live_rrect_r_label{0,0}; // rounded-rect fillet-radius label + int m_live_rrect_fi{-1}; // the rounded-rect Feature (rebuild edits) + Vec2d m_live_aslot_r_label{0,0}; // arc-slot centreline-radius label + Vec2d m_live_aslot_w_label{0,0}; // arc-slot width label + int m_live_aslot_fi{-1}; // the arc-slot Feature (rebuild edits) + std::vector m_features; // parametric groups over m_entities + int m_open_feature{-1}; // index of the Feature being built, or -1 + + // In-canvas edit-op gizmo state (Fillet/Chamfer/Offset/Mirror). GUI-only, reset by + // set_tool/cancel. Fillet/Chamfer: m_op_a,m_op_b = the two lines; Offset: m_op_a = src; + // Mirror: m_op_a = axis line, m_mirror_targets = entities to mirror. + int m_op_a{-1}; + int m_op_b{-1}; + double m_op_value{0.0}; // radius / setback / signed offset distance + Vec2d m_op_anchor{0,0}; // arrow base (corner vertex / entity midpoint) + Vec2d m_op_dir{0,0}; // unit arrow direction (inward bisector / outward normal) + Vec2d m_op_label{1e18,1e18}; // cached arrow-label centre, for picking + std::vector m_op_ghost; // live result preview (recomputed on value change) + bool m_op_dragging_arrow{false}; // arrowhead drag in progress + std::vector m_mirror_targets; // Mirror: entities to be mirrored (axis = m_op_a) + + // In-canvas imported-art transform gizmo (Mode::TransformArt). GUI-only. The art's + // untransformed contours + its bbox in base coords; the live offset/scale; the grabbed + // handle (0..3 = corners, 4 = centre move, -1 = none) and the fixed world anchor (the + // opposite corner during a corner-scale drag). + std::vector>> m_xform_base; + int m_xform_feat{-1}; + Vec2d m_xform_min{0,0}, m_xform_max{0,0}; // bbox of m_xform_base (untransformed) + Vec2d m_xform_offset{0,0}; + double m_xform_sx{1.0}, m_xform_sy{1.0}; + int m_xform_handle{-1}; + Vec2d m_xform_anchor{0,0}; + void xform_world_corners(Vec2d out[4]) const; // 4 bbox corners in plane coords + int hit_test_xform_handle(const Vec2d& p, double tol) const; + void drag_xform_handle(const Vec2d& target); + void render_xform_gizmo(); + void emit_xform(); + void reset_xform(); + + // In-canvas transform gizmo state (Mode::Move/Rotate/Scale/Array/PolarArray). GUI-only, + // reset by set_tool/cancel. Pick one or more subject entities (m_tf_targets), then a + // single draggable handle drives the continuous parameter and a live translucent ghost + // previews the result; Array/PolarArray add a second editable label for the copy count. + // Mutating ops (Move/Rotate/Scale) drop the constraint classes the map invalidates; + // additive ops (Array/PolarArray) bind each copy to its source. See confirm_transform(). + std::vector m_tf_targets; // picked subject entity indices + Vec2d m_tf_pivot{0,0}; // rotate/scale/polar pivot = set centroid + Vec2d m_tf_delta{0,0}; // Move translation / Array per-step vector + double m_tf_angle{0.0}; // Rotate angle / PolarArray total sweep (rad) + double m_tf_scale{1.0}; // Scale factor + int m_tf_count{3}; // Array/PolarArray copy count (incl. original) + double m_tf_handle_r{1.0}; // ring/handle reference radius (set on pick) + std::vector m_tf_ghost; // live result preview + int m_tf_handle{-1}; // 0 = primary drag handle grabbed, -1 = none + bool m_tf_dragging{false}; + Vec2d m_tf_label_a{1e18,1e18}; // primary-param label centre (picking) + Vec2d m_tf_label_b{1e18,1e18}; // count label centre (Array/PolarArray) + bool tf_ready() const; // >=1 target picked -> gizmo + ghost live + void tf_pick(int ei); // accumulate a subject, seed defaults once + void compute_tf_pivot(); // centroid + extent of the target set + void recompute_tf_ghost(); + Vec2d tf_handle_pos() const; // world position of the drag handle + bool hit_test_tf_handle(const Vec2d& p, double tol) const; + void drag_tf_handle(const Vec2d& target); + void render_tf_gizmo(double unit_per_px); + void open_tf_editor_a(); // inline-edit the continuous parameter + void open_tf_editor_count(); // inline-edit the copy count + void confirm_transform(); // apply geometry + constraint web + void reset_tf(); + + // DoF feedback state, refreshed by resolve_live() from the libslvs solve result. + int m_dof{-1}; // remaining DoF; 0 = fully constrained, <0 = unknown + bool m_solve_ok{true}; // solver consistent (no conflicting constraints) + std::vector m_entity_conflict; // per-entity flag: touched by a conflicting constraint + std::vector m_dimensions; // placed dimension quotes (Mode::Dimension) + int m_dim_e0{-1}; // first picked point's entity (Dimension) + SketchPointRole m_dim_r0{SketchPointRole::P0}; + bool m_dim_has0{false}; // a first point is pending + int m_pending_dim{-1}; // dim awaiting a value-card entry + Mode m_mode{Mode::Polyline}; + int m_sel_a{-1}; // picked segment endpoints (legacy Constrain mode) + int m_sel_b{-1}; + bool m_constrain_entities{false}; // Constrain mode acts on entities + int m_pick0{-1}; // picked line-entity indices (entity Constrain) + int m_pick1{-1}; + int m_pick2{-1}; // third slot (Symmetric axis) + Vec2d m_pick0_pt{0,0}; // plane-coords of the slot-0 pick (trim/extend) + std::vector m_constraint_hl; // entities highlighted by the constraint manager + std::vector m_constrain_cons; // for glyph badges (C3.4b) + GLModel m_line_model; + GLModel m_vertex_model; + GLModel m_highlight_model; + GLModel m_fill_model; // translucent face fill for closed regions + std::vector m_display_sketches; // committed sketches drawn persistently + int m_display_pick{-1}; // FEATURE index of the click-selected display sketch (-1 none) + + // Solid (whole/face/edge) selection on the committed bodies. Pointers are non-owning, + // into CadDocument (bodies + display_mesh + per-triangle face/body ids), refreshed each + // recompute via set_solid_pick. m_sel_edge_pts caches the picked edge's world polyline. + const std::vector* m_solid_bodies{nullptr}; + const TriangleMesh* m_solid_mesh{nullptr}; + const std::vector* m_solid_tri_face{nullptr}; + const std::vector* m_solid_tri_body{nullptr}; + const std::vector* m_solid_visible{nullptr}; // per-body visibility; hidden bodies aren't pickable + const std::vector* m_solid_xform{nullptr}; // per-body display transform (for edge sampling) + Vec3d body_xform_pt(int body, const Vec3d& p) const; // map an OCCT-shape point through the body xform + bool body_pickable(int b) const; // false when the body is explicitly hidden + SolidSel m_solid_sel{SolidSel::None}; + int m_sel_body{-1}; // which body the face/edge selection is on + int m_sel_face{-1}; + int m_sel_edge{-1}; + std::vector m_sel_edge_pts; + bool handle_solid_click(GLCanvas3D& canvas, const wxMouseEvent& evt); // cycle + notify + void render_solid_highlight(); + void render_datum_planes(); // translucent rectangles for datum/reference planes + std::vector m_datum_planes; + GLModel m_solid_face_model; + GLModel m_solid_edge_model; + int m_display_pick_region{-1}; // selected closed-region index within that feature (-1 none) + + // Visual Extrude gizmo state (C5b). GUI-only; fed by the panel each refresh_preview. + bool m_ex_active{false}; + SketchPlane m_ex_plane; // profile plane (gives normal + to_world anchor) + Vec2d m_ex_centroid{0,0}; // arrow base in plane coords (profile centroid) + double m_ex_depth{0.0}; // primary depth (= m_distance) + double m_ex_depth2{0.0}; // second-side depth (TwoSided, = m_distance2) + bool m_ex_two_sided{false}; + bool m_ex_flip{false}; + int m_ex_drag{-1}; // 0 = primary arrow, 1 = second arrow, -1 = none + int m_ex_press_x{0}, m_ex_press_y{0}; // press px to tell click-to-edit from drag + void render_extrude_gizmo(); + bool hit_test_extrude_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int& which) const; + void drag_extrude_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int which); + void open_extrude_editor(int which); + GLModel m_ex_arrow_model; + + // Move-body gizmo state: 3 world-axis translate arrows + 3 world-axis rotate rings. + // Delta model: offset/rot are deltas about a fixed pivot, composed onto m_mv_base_xform + // (the body's pose when Move opened) so rotation works even on an already-placed body. + bool m_mv_active{false}; + int m_mv_body{-1}; + Vec3d m_mv_base{Vec3d::Zero()}; // pivot = body's world centroid at Move-open + Vec3d m_mv_offset{Vec3d::Zero()}; // delta translation along world X/Y/Z + Transform3d m_mv_base_xform{Transform3d::Identity()}; // pose when Move opened + Eigen::Matrix3d m_mv_rot{Eigen::Matrix3d::Identity()}; // accumulated delta rotation (world, about pivot) + Eigen::Matrix3d m_mv_rot_start{Eigen::Matrix3d::Identity()}; // rot snapshot at arc-drag start + double m_mv_arc_a0{0.0}; // mouse angle on the ring at drag start + int m_mv_drag{-1}; // 0..2 = X/Y/Z arrow, 3..5 = X/Y/Z ring, -1 none + int m_mv_press_x{0}, m_mv_press_y{0}; + Transform3d compose_move_xform() const; // T(offset)*T(pivot)*rot*T(-pivot)*base_xform + void ring_basis(int axis, Vec3d& e, Vec3d& u, Vec3d& v) const; // world axis + in-plane basis + void render_move_gizmo(); + bool hit_test_move_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int& axis) const; + bool hit_test_move_arc(GLCanvas3D& canvas, const wxMouseEvent& evt, int& axis) const; + void drag_move_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis); + void drag_move_arc(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis); + bool arc_mouse_angle(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis, double& ang) const; + void open_move_editor(int axis); + GLModel m_mv_arrow_model; + + // Fillet/Chamfer radius gizmo state (single world-space arrow at the picked edge midpoint). + bool m_fl_active{false}; + Vec3d m_fl_anchor{Vec3d::Zero()}; // edge midpoint (world, already body-transformed) + Vec3d m_fl_dir{Vec3d::UnitZ()}; // unit radius direction (perp to edge, outward) + double m_fl_radius{1.0}; // current radius (= dressup size) + bool m_fl_drag{false}; + int m_fl_press_x{0}, m_fl_press_y{0}; + double m_fl_grab_proj{0.0}; // axis projection at grab (relative drag reference) + double m_fl_grab_radius{1.0}; // radius at grab (relative drag reference) + void render_fillet_gizmo(); + bool hit_test_fillet_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt) const; + double fillet_axis_proj(GLCanvas3D& canvas, const wxMouseEvent& evt) const; // NaN if camera∥axis + void start_fillet_drag(GLCanvas3D& canvas, const wxMouseEvent& evt); + void drag_fillet_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt); + void open_fillet_editor(); + GLModel m_fl_arrow_model; + + // Hole gizmo state. The hole is a positioned circular cut on m_hl_plane at (m_hl_x, m_hl_y); + // the footprint circle is drawn on the plane, the diameter arrow runs along the plane u-axis, + // the depth arrow along +normal (matching the kernel's make_extrude). Three draggable handles: + // 0 = centre (reposition in plane u/v), 1 = diameter, 2 = depth (only shown when !through). + bool m_hl_active{false}; + SketchPlane m_hl_plane; + double m_hl_x{0.0}, m_hl_y{0.0}; // centre on the plane (u/v mm) + double m_hl_diameter{6.0}; + double m_hl_depth{10.0}; + bool m_hl_through{true}; + // #2 Part B: face (u,v) bounds, so the construction dims read as distance from the face SIDES + // (umin/vmin = two adjacent edges) rather than from the centre. Off for a dropdown-plane hole. + bool m_hl_has_bounds{false}; + double m_hl_umin{0}, m_hl_umax{0}, m_hl_vmin{0}, m_hl_vmax{0}; + int m_hl_drag{-1}; // 0=centre, 1=diameter, 2=depth, 3=X-dim, 4=Y-dim, -1=none + int m_hl_press_x{0}, m_hl_press_y{0}; + double m_hl_grab_proj{0.0}; // diameter/depth axis projection at grab (relative) + double m_hl_grab_val{0.0}; // radius (diameter drag) or depth at grab + Vec2d m_hl_grab_uv{0.0, 0.0}; // centre drag: plane-projected grab point + double m_hl_grab_x{0.0}, m_hl_grab_y{0.0}; // centre drag: x/y at grab + void render_hole_gizmo(); + int hit_test_hole_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const; // 0/1/2/-1 + double hole_axis_proj(GLCanvas3D& canvas, const wxMouseEvent& evt, + const Vec3d& anchor, const Vec3d& dir) const; // NaN if camera∥axis + void start_hole_drag(GLCanvas3D& canvas, const wxMouseEvent& evt, int which); + void drag_hole_handle(GLCanvas3D& canvas, const wxMouseEvent& evt); + void open_hole_editor(int which); + GLModel m_hl_stroke_model; + + // Thread gizmo state (mirrors the hole gizmo; radius arrow uses an R label, length arrow is + // always shown). Handles: 0 = centre (thread_x/y), 1 = radius, 2 = length. + bool m_th_active{false}; + SketchPlane m_th_plane; + double m_th_x{0.0}, m_th_y{0.0}; + double m_th_radius{5.0}; + double m_th_height{10.0}; + int m_th_drag{-1}; // 0=centre, 1=radius, 2=length, -1=none + int m_th_press_x{0}, m_th_press_y{0}; + double m_th_grab_proj{0.0}; + double m_th_grab_val{0.0}; + Vec2d m_th_grab_uv{0.0, 0.0}; + double m_th_grab_x{0.0}, m_th_grab_y{0.0}; + void render_thread_gizmo(); + int hit_test_thread_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const; // 0/1/2/-1 + void start_thread_drag(GLCanvas3D& canvas, const wxMouseEvent& evt, int which); + void drag_thread_handle(GLCanvas3D& canvas, const wxMouseEvent& evt); + void open_thread_editor(int which); + GLModel m_th_stroke_model; + + // Shell gizmo state (single inward thickness arrow at the picked face centroid). + bool m_sh_active{false}; + Vec3d m_sh_anchor{Vec3d::Zero()}; // picked face centroid (world) + Vec3d m_sh_dir{Vec3d::UnitZ()}; // inward unit direction (-outward normal) + double m_sh_thickness{2.0}; + bool m_sh_drag{false}; + int m_sh_press_x{0}, m_sh_press_y{0}; + double m_sh_grab_proj{0.0}; + double m_sh_grab_val{2.0}; + void render_shell_gizmo(); + bool hit_test_shell_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt) const; + void start_shell_drag(GLCanvas3D& canvas, const wxMouseEvent& evt); + void drag_shell_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt); + void open_shell_editor(); + GLModel m_sh_stroke_model; + + // Revolve gizmo state (arc center = projection of the profile centroid onto the axis). + bool m_rv_active{false}; + Vec3d m_rv_center{Vec3d::Zero()}; // arc center on the axis (world) + Vec3d m_rv_axis{Vec3d::UnitX()}; // revolve axis unit dir (world) + Vec3d m_rv_ref{Vec3d::UnitY()}; // angle-0 reference dir (perp to axis, toward profile) + double m_rv_radius{10.0}; // arc radius = profile perpendicular distance (world) + double m_rv_angle{360.0}; // current sweep magnitude (deg, 1..360) + bool m_rv_flip{false}; // sweep sense (matches the kernel's negative-angle flip) + bool m_rv_drag{false}; + int m_rv_press_x{0}, m_rv_press_y{0}; + void render_revolve_gizmo(); + bool hit_test_revolve_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const; + void drag_revolve_arc(GLCanvas3D& canvas, const wxMouseEvent& evt); + void open_revolve_editor(); + GLModel m_rv_stroke_model; + + // Pattern gizmo state. Linear arrow along m_pt_dirw from m_pt_base; circular arc like Revolve + // but axis = m_pt_normal through m_pt_origin (the world XY plane by default). + bool m_pt_active{false}; + bool m_pt_circular{false}; + Vec3d m_pt_base{Vec3d::Zero()}; // target body centroid (world): linear anchor / radius ref + Vec3d m_pt_dirw{Vec3d::UnitX()}; // linear march direction (world) + Vec3d m_pt_origin{Vec3d::Zero()}; // circular rotation axis origin (world) + Vec3d m_pt_normal{Vec3d::UnitZ()}; // circular rotation axis (world) + Vec3d m_pt_cref{Vec3d::UnitX()}; // circular angle-0 reference dir (perp to normal, toward body) + Vec3d m_pt_ccenter{Vec3d::Zero()}; // circular arc center (foot of body centroid on the axis) + double m_pt_radius{10.0}; // circular arc radius (world) + int m_pt_count{3}; + double m_pt_spacing{20.0}; + double m_pt_angle{360.0}; + bool m_pt_drag{false}; + int m_pt_press_x{0}, m_pt_press_y{0}; + void render_pattern_gizmo(); + bool hit_test_pattern_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const; + void drag_pattern_handle(GLCanvas3D& canvas, const wxMouseEvent& evt); + void open_pattern_editor(); + GLModel m_pt_stroke_model; +}; + +}} // namespace Slic3r::GUI + +#endif // slic3r_DesignSketchTool_hpp_ diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 8ffac135ff..d50e1cf520 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1,5 +1,6 @@ #include "libslic3r/libslic3r.h" #include "GLCanvas3D.hpp" +#include "DesignSketchTool.hpp" // SnapOrca Design: interactive 2D sketch tool #include @@ -1833,6 +1834,16 @@ void GLCanvas3D::enable_separator_toolbar(bool enable) m_separator_toolbar.set_enabled(enable); } +void GLCanvas3D::enable_collapse_toolbar(bool enable) +{ + m_collapse_toolbar_enabled = enable; +} + +void GLCanvas3D::enable_plate_chrome(bool enable) +{ + m_plate_chrome_enabled = enable; +} + void GLCanvas3D::zoom_to_bed() { BoundingBoxf3 box = m_bed.build_volume().bounding_volume(); @@ -2123,6 +2134,11 @@ void GLCanvas3D::render(bool only_init) if (_is_fxaa_enabled()) _render_fxaa_pass(static_cast(cnv_size.get_width()), static_cast(cnv_size.get_height())); + // SnapOrca Design: interactive 2D sketch overlay, drawn over the scene but + // beneath the UI overlays (toolbars, labels). + if (m_design_sketch_tool != nullptr && m_design_sketch_tool->has_display()) + m_design_sketch_tool->render(*this); + // draw overlays _render_overlays(); @@ -3278,6 +3294,56 @@ void GLCanvas3D::on_char(wxKeyEvent& evt) return; } + // SnapOrca Design: Delete/Backspace removes the selected sketch entities while a + // sketch tool is active and the canvas has focus (dialog text fields are separate + // wx controls, so this never eats their editing keys). + if (m_design_sketch_tool != nullptr && m_design_sketch_tool->is_active() + && (keyCode == WXK_DELETE || keyCode == WXK_BACK) + && !m_design_sketch_tool->selection().empty()) { + m_design_sketch_tool->delete_selected(); + m_dirty = true; + render(); + return; + } + + // Esc exits the active sketch tool (Onshape-like, layered: abort in-progress entity -> + // drop to Select -> exit the session back to Feature mode). + if (m_design_sketch_tool != nullptr && m_design_sketch_tool->is_active() + && keyCode == WXK_ESCAPE) { + m_design_sketch_tool->request_exit(); + m_dirty = true; + render(); + return; + } + + // SnapOrca Design: Ctrl+Z / Ctrl+Shift+Z (and Ctrl+Y) undo/redo the Design feature + // history. Scoped by m_design_sketch_tool — only the Design canvas owns one — so the + // main 3D editor's undo/redo (the CanvasView3D-gated cases further below) is untouched. + // Handled here, before the generic Ctrl block, so it takes precedence and early-returns. + if (m_design_sketch_tool != nullptr && (evt.GetModifiers() & ctrlMask) != 0) { + const bool is_z = (keyCode == 'z' || keyCode == 'Z' || keyCode == WXK_CONTROL_Z); + const bool is_y = (keyCode == 'y' || keyCode == 'Y' || keyCode == WXK_CONTROL_Y); + if (is_z || is_y) { + const bool redo = is_y || ((evt.GetModifiers() & shiftMask) != 0); + m_design_sketch_tool->request_undo_redo(redo); + m_dirty = true; + render(); + return; + } + } + + // SnapOrca Design: F = Place on Face (Prepare's lay-flat), when the Design viewport is up + // and a body face is selected. The tool forwards to DesignPanel::place_on_face; it returns + // false (no face picked) so F falls through to the default handler below. + if (m_design_sketch_tool != nullptr && m_design_sketch_tool->has_display() + && (keyCode == 'f' || keyCode == 'F') && (evt.GetModifiers() & ctrlMask) == 0) { + if (m_design_sketch_tool->request_place_on_face()) { + m_dirty = true; + render(); + return; + } + } + bool is_in_painting_mode = false; GLGizmoPainterBase *current_gizmo_painter = dynamic_cast(get_gizmos_manager().get_current()); if (current_gizmo_painter != nullptr) { @@ -3650,6 +3716,18 @@ public: void GLCanvas3D::on_key(wxKeyEvent& evt) { + // SnapOrca Design: Delete/Backspace removes selected sketch entities. GTK delivers + // these as KEY_DOWN rather than CHAR, so handle it here too. + if (evt.GetEventType() == wxEVT_KEY_DOWN + && m_design_sketch_tool != nullptr && m_design_sketch_tool->is_active() + && (evt.GetKeyCode() == WXK_DELETE || evt.GetKeyCode() == WXK_BACK) + && !m_design_sketch_tool->selection().empty()) { + m_design_sketch_tool->delete_selected(); + m_dirty = true; + render(); + return; + } + static GLCanvas3D const * thiz = nullptr; static TranslationProcessor translationProcessor(nullptr, nullptr); if (thiz != this) { @@ -4180,6 +4258,21 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) return; } + // SnapOrca Design: the interactive sketch tool owns the mouse whenever it has + // something on screen — an active session OR committed sketch overlays that the user + // can click to select. It runs after ImGui (so dialogs still work) but before + // camera/toolbar/gizmo handling; on_mouse returns false for events it doesn't consume + // (drag/orbit/wheel) so the camera keeps working over the display-only plate. + if (m_design_sketch_tool != nullptr && m_design_sketch_tool->has_display()) { + if (evt.LeftDown() && m_canvas != nullptr) + m_canvas->SetFocus(); // grab keyboard focus so Delete/keys reach this canvas + if (m_design_sketch_tool->on_mouse(evt, *this)) { + m_dirty = true; + render(); // force an immediate redraw so the sketch overlay updates live + return; + } + } + #ifdef __WXMSW__ bool on_enter_workaround = false; if (! evt.Entering() && ! evt.Leaving() && m_mouse.position.x() == -1.0) { @@ -7853,7 +7946,12 @@ void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d& void GLCanvas3D::_render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid) { - wxGetApp().plater()->get_partplate_list().render(view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid); + // SnapOrca Design: transiently suppress plate chrome for opted-out canvases. + auto& plate_list = wxGetApp().plater()->get_partplate_list(); + const bool prev_hide_chrome = plate_list.get_hide_chrome(); + plate_list.set_hide_chrome(!m_plate_chrome_enabled); + plate_list.render(view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid); + plate_list.set_hide_chrome(prev_hide_chrome); } void GLCanvas3D::_render_cast_shadows_on_plate(const Transform3d& view_matrix, const Transform3d& projection_matrix) @@ -9359,6 +9457,9 @@ void GLCanvas3D::_render_separator_toolbar_left() const void GLCanvas3D::_render_collapse_toolbar() const { + if (!m_collapse_toolbar_enabled) + return; + auto& plater = *wxGetApp().plater(); const auto sidebar_docking_dir = plater.get_sidebar_docking_state(); if (sidebar_docking_dir == Sidebar::None) { diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 72e7f1f8e2..2f4e697908 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -57,6 +57,7 @@ namespace GUI { class Bed3D; class PartPlateList; +class DesignSketchTool; // SnapOrca Design: interactive 2D sketch tool #if ENABLE_RETINA_GL class RetinaHelper; @@ -542,6 +543,9 @@ private: mutable Vec2i32 m_canvas_toolbar_pos = {140, 5}; mutable float m_sc{1}; mutable float m_paint_toolbar_width; + bool m_collapse_toolbar_enabled{true}; + bool m_plate_chrome_enabled{true}; + DesignSketchTool* m_design_sketch_tool{nullptr}; //BBS: add canvas type for assemble view usage ECanvasType m_canvas_type; @@ -879,6 +883,10 @@ public: void enable_assemble_view_toolbar(bool enable); void enable_return_toolbar(bool enable); void enable_separator_toolbar(bool enable); + void enable_collapse_toolbar(bool enable); + void enable_plate_chrome(bool enable); + void set_design_sketch_tool(DesignSketchTool* tool) { m_design_sketch_tool = tool; } + DesignSketchTool* get_design_sketch_tool() const { return m_design_sketch_tool; } void enable_dynamic_background(bool enable) { m_dynamic_background_enabled = enable; } void enable_labels(bool enable) { m_labels.enable(enable); } void enable_slope(bool enable) { m_slope.enable(enable); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPrimitive.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPrimitive.cpp new file mode 100644 index 0000000000..4e1a197a39 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoPrimitive.cpp @@ -0,0 +1,186 @@ +#include "GLGizmoPrimitive.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/ImGuiWrapper.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/GUI/NotificationManager.hpp" +#include "libslic3r/Model.hpp" + +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif +#include + +namespace Slic3r { +namespace GUI { + +GLGizmoPrimitive::GLGizmoPrimitive(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) + : GLGizmoBase(parent, icon_filename, sprite_id) {} + +bool GLGizmoPrimitive::on_init() { return true; } +std::string GLGizmoPrimitive::on_get_name() const { return _u8L("Primitive"); } +bool GLGizmoPrimitive::on_is_activable() const { return true; } +void GLGizmoPrimitive::on_render() {} +void GLGizmoPrimitive::on_set_state() +{ if (m_state == EState::On) { m_params = PrimitiveParams{}; m_preview_dirty = true; } } + +bool GLGizmoPrimitive::on_mouse(const wxMouseEvent&) { return false; } + +CommonGizmosDataID GLGizmoPrimitive::on_get_requirements() const +{ return CommonGizmosDataID(int(CommonGizmosDataID::SelectionInfo) | int(CommonGizmosDataID::InstancesHider)); } + +void GLGizmoPrimitive::on_load(cereal::BinaryInputArchive& ar) +{ ar(m_params); m_preview_dirty = true; } +void GLGizmoPrimitive::on_save(cereal::BinaryOutputArchive& ar) const +{ ar(m_params); } + +void GLGizmoPrimitive::apply_preset(const char*, double w, double h, double d) +{ + m_params.type = PrimitiveType::Box; + m_params.box_w = w; m_params.box_h = h; m_params.box_d = d; + m_preview_dirty = true; +} + +static void gen_mesh_and_add(PrimitiveParams& p, const char* snap_name) +{ + TopoDS_Solid solid = GeometryEngine::make_primitive(p); + TopoDS_Shape shape = solid; + if (p.dressup_enabled) { + if (p.dressup_type == DressUpType::Fillet) + shape = GeometryEngine::apply_fillet(shape, p.dressup_radius, p.dressup_faces); + else + shape = GeometryEngine::apply_chamfer(shape, p.dressup_chamfer_dist, p.dressup_faces); + } + TriangleMesh mesh = GeometryEngine::tessellate(shape, p.linear_deflection, p.angular_deflection); + if (mesh.its.indices.empty()) { + wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::WarningNotificationLevel, _u8L("Empty mesh generated")); + return; + } + wxGetApp().plater()->take_snapshot(snap_name); + ModelObject* mo = wxGetApp().model().add_object(); + std::string name = GeometryEngine::primitive_name(p.type); + if (p.dressup_enabled && p.dressup_type == DressUpType::Fillet) name += " (Fillet)"; + else if (p.dressup_enabled) name += " (Chamfer)"; + mo->name = name; + mo->add_volume(std::move(mesh))->set_new_unique_id(); + mo->ensure_on_bed(); + wxGetApp().plater()->update(); +} + +void GLGizmoPrimitive::apply_primitive() { gen_mesh_and_add(m_params, "Add Primitive"); } + +void GLGizmoPrimitive::on_render_input_window(float x, float y, float bottom_limit) +{ + y = std::min(y, bottom_limit - ImGui::GetWindowHeight()); + const float scale = m_parent.get_scale(); + ImGuiWrapper::push_toolbar_style(scale); + GizmoImguiSetNextWIndowPos(x, y, ImGuiCond_Always, 0.0f, 0.0f); + GizmoImguiBegin("Primitive", ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove + | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse + | ImGuiWindowFlags_NoTitleBar); + + if (ImGui::CollapsingHeader("Shape", ImGuiTreeNodeFlags_DefaultOpen)) { + static const char* names[] = {"Box", "Cylinder", "Sphere", "Cone", "Torus"}; + int cur = (int)m_params.type; + if (ImGui::Combo("##type", &cur, names, (int)PrimitiveType::COUNT)) { + m_params.type = (PrimitiveType)cur; + m_preview_dirty = true; + } + ImGui::Text("Quick:"); + ImGui::SameLine(); + if (ImGui::SmallButton("10mm")) apply_preset("10mm cube", 10, 10, 10); + ImGui::SameLine(); + if (ImGui::SmallButton("20mm")) apply_preset("20mm cube", 20, 20, 20); + ImGui::SameLine(); + if (ImGui::SmallButton("50mm")) apply_preset("50mm cube", 50, 50, 50); + } + + ImGui::Separator(); + + if (ImGui::CollapsingHeader("Dimensions", ImGuiTreeNodeFlags_DefaultOpen)) { + auto dim = [&](const char* label, double& val, double step=0.5, double fast=5.0) { + ImGui::SetNextItemWidth(130); + if (ImGui::InputDouble(label, &val, step, fast, "%.1f mm")) m_preview_dirty = true; + if (val < 0.5) val = 0.5; + }; + switch (m_params.type) { + case PrimitiveType::Box: + dim("Width (X)", m_params.box_w); + dim("Depth (Y)", m_params.box_d); + dim("Height (Z)", m_params.box_h); + break; + case PrimitiveType::Cylinder: + dim("Radius", m_params.cyl_radius); + dim("Height", m_params.cyl_height); + break; + case PrimitiveType::Sphere: + dim("Radius", m_params.sph_radius); + break; + case PrimitiveType::Cone: + dim("Bottom R", m_params.cone_r1); + dim("Top R", m_params.cone_r2); + dim("Height", m_params.cone_height); + break; + case PrimitiveType::Torus: + dim("Major R", m_params.torus_r1); + dim("Minor R", m_params.torus_r2, 0.1, 1.0); + break; + default: break; + } + } + + ImGui::Separator(); + + if (ImGui::CollapsingHeader("Fillet / Chamfer")) { + ImGui::Checkbox("Enable", &m_params.dressup_enabled); + if (m_params.dressup_enabled) { + static const char* dn[] = {"Fillet", "Chamfer"}; + int du = (int)m_params.dressup_type; + ImGui::SetNextItemWidth(100); + if (ImGui::Combo("##dtype", &du, dn, 2)) { m_params.dressup_type = (DressUpType)du; m_preview_dirty = true; } + static const char* fn[] = {"All edges", "Top edges", "Bottom edges", "Lateral edges"}; + int fg = (int)m_params.dressup_faces; + ImGui::SetNextItemWidth(140); + if (ImGui::Combo("Edges", &fg, fn, 4)) { m_params.dressup_faces = (FaceGroup)fg; m_preview_dirty = true; } + if (m_params.dressup_type == DressUpType::Fillet) { + ImGui::SetNextItemWidth(100); + if (ImGui::InputDouble("Radius", &m_params.dressup_radius, 0.1, 1.0, "%.1f mm")) { + if (m_params.dressup_radius < 0.1) m_params.dressup_radius = 0.1; + m_preview_dirty = true; + } + } else { + ImGui::SetNextItemWidth(100); + if (ImGui::InputDouble("Distance", &m_params.dressup_chamfer_dist, 0.1, 1.0, "%.1f mm")) { + if (m_params.dressup_chamfer_dist < 0.1) m_params.dressup_chamfer_dist = 0.1; + m_preview_dirty = true; + } + } + } + } + + ImGui::Separator(); + + if (ImGui::CollapsingHeader("Quality")) { + ImGui::SetNextItemWidth(130); + if (ImGui::InputDouble("Mesh resolution", &m_params.linear_deflection, 0.001, 0.1, "%.3f mm")) { + if (m_params.linear_deflection < 0.001) m_params.linear_deflection = 0.001; + if (m_params.linear_deflection > 1.0) m_params.linear_deflection = 1.0; + m_preview_dirty = true; + } + } + + ImGui::Separator(); + + if (ImGui::Button("Add Shape", {-1, 28})) + apply_primitive(); + + if (ImGui::Button("Close", {-1, 0})) + m_parent.reset_all_gizmos(); + + GizmoImguiEnd(); + ImGuiWrapper::pop_toolbar_style(); +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPrimitive.hpp b/src/slic3r/GUI/Gizmos/GLGizmoPrimitive.hpp new file mode 100644 index 0000000000..013dc0d9c2 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoPrimitive.hpp @@ -0,0 +1,43 @@ +#ifndef slic3r_GLGizmoPrimitive_hpp_ +#define slic3r_GLGizmoPrimitive_hpp_ + +#include "GLGizmoBase.hpp" +#include "GLGizmosCommon.hpp" +#include "libslic3r/GeometryEngine.hpp" + +namespace Slic3r { +namespace GUI { + +class GLGizmoPrimitive : public GLGizmoBase +{ +public: + GLGizmoPrimitive(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); + ~GLGizmoPrimitive() = default; + + bool on_mouse(const wxMouseEvent& mouse_event) override; + +protected: + bool on_init() override; + std::string on_get_name() const override; + bool on_is_activable() const override; + void on_render() override; + void on_set_state() override; + CommonGizmosDataID on_get_requirements() const override; + void on_render_input_window(float x, float y, float bottom_limit) override; + + void on_load(cereal::BinaryInputArchive& ar) override; + void on_save(cereal::BinaryOutputArchive& ar) const override; + +private: + void apply_primitive(); + void apply_preset(const char* name, double w, double h, double d); + + PrimitiveParams m_params; + TriangleMesh m_preview_mesh; + bool m_preview_dirty{true}; +}; + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_GLGizmoPrimitive_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSketch.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSketch.cpp new file mode 100644 index 0000000000..88bd636563 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoSketch.cpp @@ -0,0 +1,459 @@ +#include "GLGizmoSketch.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/ImGuiWrapper.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/GUI/NotificationManager.hpp" +#include "libslic3r/Model.hpp" +#include +#include +#include + +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif +#include + +#define L(s) Slic3r::GUI::I18N::translate((s)).c_str() +#define UL(s) Slic3r::GUI::I18N::translate_utf8((s)).c_str() + +namespace Slic3r { +namespace GUI { + +GLGizmoSketch::GLGizmoSketch(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) + : GLGizmoBase(parent, icon_filename, sprite_id) {} + +bool GLGizmoSketch::on_init() { return true; } +std::string GLGizmoSketch::on_get_name() const { return _u8L("Sketch"); } +bool GLGizmoSketch::on_is_activable() const { return true; } +void GLGizmoSketch::on_render() {} +void GLGizmoSketch::on_set_state() { if (m_state == EState::On) clear_all(); } +bool GLGizmoSketch::on_mouse(const wxMouseEvent&) { return false; } + +CommonGizmosDataID GLGizmoSketch::on_get_requirements() const +{ return CommonGizmosDataID(int(CommonGizmosDataID::SelectionInfo)); } + +void GLGizmoSketch::on_load(cereal::BinaryInputArchive& ar) +{ + ar(m_tool, m_profiles, m_plane, m_sp, m_rect_w, m_rect_h, m_circle_r, m_poly_sides, m_poly_r, m_snap_grid, m_grid_step); + m_active_profile = -1; +} + +void GLGizmoSketch::on_save(cereal::BinaryOutputArchive& ar) const +{ + ar(m_tool, m_profiles, m_plane, m_sp, m_rect_w, m_rect_h, m_circle_r, m_poly_sides, m_poly_r, m_snap_grid, m_grid_step); +} + +SketchProfile& GLGizmoSketch::active_profile() +{ + if (m_active_profile < 0 || m_active_profile >= (int)m_profiles.size()) { + m_profiles.emplace_back(); + m_active_profile = (int)m_profiles.size() - 1; + } + return m_profiles[m_active_profile]; +} + +bool GLGizmoSketch::has_closed_profile() const +{ + for (auto& p : m_profiles) if (p.closed && p.points.size() >= 3) return true; + return false; +} + +void GLGizmoSketch::clear_all() +{ + m_profiles.clear(); + m_canvas_points.clear(); + m_active_profile = -1; +} + +void GLGizmoSketch::add_closed_profile() +{ + auto& ap = active_profile(); + if (ap.points.size() >= 3) { + ap.closed = true; + m_active_profile = -1; + } +} + +void GLGizmoSketch::delete_profile(int idx) +{ + if (idx >= 0 && idx < (int)m_profiles.size()) { + m_profiles.erase(m_profiles.begin() + idx); + if (m_active_profile >= (int)m_profiles.size()) m_active_profile = -1; + } +} + +Vec2d GLGizmoSketch::snap(Vec2d pt) const +{ + if (!m_snap_grid) return pt; + double gs = m_grid_step; + return {round(pt.x() / gs) * gs, round(pt.y() / gs) * gs}; +} + +void GLGizmoSketch::build_preset_profile() +{ + auto& ap = active_profile(); + ap.clear(); + auto add = [&](double x, double y) { ap.points.emplace_back(x, y); }; + switch (m_tool) { + case SketchTool::Rectangle: + add(-m_rect_w/2, -m_rect_h/2); add( m_rect_w/2, -m_rect_h/2); + add( m_rect_w/2, m_rect_h/2); add(-m_rect_w/2, m_rect_h/2); + ap.closed = true; m_active_profile = -1; break; + case SketchTool::Circle: + for (int i = 0; i <= m_circle_seg; ++i) { + double a = 2.0*M_PI*i/m_circle_seg; + add(cos(a)*m_circle_r, sin(a)*m_circle_r); + } + ap.closed = true; m_active_profile = -1; break; + case SketchTool::Polygon: + for (int i = 0; i < m_poly_sides; ++i) { + double a = 2.0*M_PI*i/m_poly_sides - M_PI/2; + add(cos(a)*m_poly_r, sin(a)*m_poly_r); + } + ap.closed = true; m_active_profile = -1; break; + default: break; + } +} + +void GLGizmoSketch::handle_canvas_click(ImVec2 pos) +{ + Vec2d pt = snap({pos.x / m_canvas_scale, -pos.y / m_canvas_scale}); + if (m_tool == SketchTool::Line) { + auto& ap = active_profile(); + if (ap.points.size() >= 3 && (pt - ap.points.front()).norm() < m_grid_step) { + ap.points.push_back(ap.points.front()); + ap.closed = true; + m_active_profile = -1; + return; + } + ap.points.push_back(pt); + } +} + +void GLGizmoSketch::draw_canvas() +{ + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImVec2 pos = ImGui::GetCursorScreenPos(); + float w = 280, h = 200; + ImVec2 end(pos.x+w, pos.y+h); + float cx = pos.x+w/2, cy = pos.y+h/2; + auto tc = [&](const ImVec2& p) { return ImVec2(cx+p.x*m_canvas_scale, cy-p.y*m_canvas_scale); }; + + dl->AddRectFilled(pos, end, IM_COL32(28,28,36,255)); + dl->AddRect(pos, end, IM_COL32(55,55,68,255)); + + float gs = m_grid_step; + for (float g = 0; g < w; g += gs * m_canvas_scale) { + ImU32 gc = (int(g/(gs*m_canvas_scale)) % 5 == 0) ? IM_COL32(60,60,75,100) : IM_COL32(45,45,55,60); + dl->AddLine({pos.x+g,pos.y}, {pos.x+g,end.y}, gc); + } + for (float g = 0; g < h; g += gs * m_canvas_scale) { + ImU32 gc = (int(g/(gs*m_canvas_scale)) % 5 == 0) ? IM_COL32(60,60,75,100) : IM_COL32(45,45,55,60); + dl->AddLine({pos.x,pos.y+g}, {end.x,pos.y+g}, gc); + } + + dl->AddLine({cx,pos.y},{cx,end.y}, IM_COL32(70,70,85,180), 1.5f); + dl->AddLine({pos.x,cy},{end.x,cy}, IM_COL32(70,70,85,180), 1.5f); + dl->AddText({end.x-12, cy+2}, IM_COL32(120,120,140,200), "X"); + dl->AddText({cx+4, pos.y+2}, IM_COL32(120,120,140,200), "Y"); + + for (size_t pi = 0; pi < m_profiles.size(); ++pi) { + auto& prof = m_profiles[pi]; + if (prof.points.size() < 2) continue; + std::vector sp; + for (auto& p : prof.points) sp.push_back(tc({(float)p.x(), (float)p.y()})); + if (prof.closed && sp.size() >= 3) { + bool is_outer = (pi == 0); + ImU32 fill = is_outer ? IM_COL32(0,180,90,35) : IM_COL32(180,60,60,35); + ImU32 line = is_outer ? IM_COL32(0,220,100,255) : IM_COL32(220,80,80,255); + dl->AddConvexPolyFilled(sp.data(), (int)sp.size(), fill); + for (size_t i=0; iAddLine(sp[i], sp[(i+1)%sp.size()], line, (pi==0)?2.5f:2.0f); + for (size_t i=0; iAddCircleFilled(sp[i], 3.0f, IM_COL32(255,255,255,255)); + } + } + + auto& ap = active_profile(); + if (!ap.closed && ap.points.size() >= 1) { + std::vector sp; + for (auto& p : ap.points) sp.push_back(tc({(float)p.x(), (float)p.y()})); + for (size_t i=1; iAddLine(sp[i-1], sp[i], IM_COL32(0,200,255,200), 2.0f); + for (auto& s : sp) dl->AddCircleFilled(s, 3.5f, IM_COL32(100,200,255,255)); + ImVec2 mouse = ImGui::GetMousePos(); + if (mouse.x > pos.x && mouse.x < end.x && mouse.y > pos.y && mouse.y < end.y) + dl->AddLine(sp.back(), mouse, IM_COL32(100,160,220,120), 1.5f); + } + + ImGui::InvisibleButton("canvas", ImVec2(w,h)); + if (ImGui::IsItemHovered()) { + ImVec2 m = ImGui::GetMousePos(); + Vec2d sk({(m.x-cx)/m_canvas_scale, -(m.y-cy)/m_canvas_scale}); + if (m_snap_grid) sk = snap(sk); + auto txt = wxString::Format("X:%.1f Y:%.1f", sk.x(), sk.y()).ToStdString(); + dl->AddText({pos.x+4, end.y-16}, IM_COL32(160,160,180,200), txt.c_str()); + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + handle_canvas_click({(m.x-cx)/m_canvas_scale, -(m.y-cy)/m_canvas_scale}); + if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { + auto& ap2 = active_profile(); + if (ap2.points.size() >= 3) { + ap2.points.push_back(ap2.points.front()); + ap2.closed = true; + m_active_profile = -1; + } + } + } +} + +TopoDS_Shape GLGizmoSketch::build_combined_shape() +{ + if (m_profiles.empty() || !m_profiles[0].closed) + throw std::runtime_error("No outer profile"); + + TopoDS_Wire outer_wire = m_profiles[0].to_occt_wire(m_plane); + BRepBuilderAPI_MakeFace face_maker(outer_wire); + if (!face_maker.IsDone()) throw std::runtime_error("Failed to make outer face"); + + for (size_t i = 1; i < m_profiles.size(); ++i) { + if (!m_profiles[i].closed) continue; + TopoDS_Wire inner = m_profiles[i].to_occt_wire(m_plane); + face_maker.Add(inner); + } + face_maker.Build(); + if (!face_maker.IsDone()) throw std::runtime_error("Failed to build face with holes"); + + TopoDS_Face face = face_maker.Face(); + + TopoDS_Shape shape; + if (m_sp.revolve_deg < 360.0 && m_sp.revolve_deg > 0.0) { + gp_Pnt o(m_plane.origin.x(), m_plane.origin.y(), m_plane.origin.z()); + gp_Dir xd(m_plane.x_axis.x(), m_plane.x_axis.y(), m_plane.x_axis.z()); + gp_Ax1 axis(o, xd); + BRepPrimAPI_MakeRevol rev(face, axis, m_sp.revolve_deg * M_PI / 180.0); + if (!rev.IsDone()) throw std::runtime_error("Revolve failed"); + shape = rev.Shape(); + } else { + shape = SketchEngine::make_extrude_face(face, m_plane, m_sp.extrude_len, m_sp.extrude_sym); + } + + if (m_sp.dressup_enabled) { + if (m_sp.dressup_type == DressUpType::Fillet) + shape = GeometryEngine::apply_fillet(shape, m_sp.dressup_radius, m_sp.dressup_faces); + else + shape = GeometryEngine::apply_chamfer(shape, m_sp.dressup_chamfer_dist, m_sp.dressup_faces); + } + return shape; +} + +void GLGizmoSketch::on_render_input_window(float x, float y, float bottom_limit) +{ + y = std::min(y, bottom_limit - ImGui::GetWindowHeight()); + const float scale = m_parent.get_scale(); + ImGuiWrapper::push_toolbar_style(scale); + GizmoImguiSetNextWIndowPos(x, y, ImGuiCond_Always, 0.0f, 0.0f); + GizmoImguiBegin("Sketch", ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove + | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse + | ImGuiWindowFlags_NoTitleBar); + + if (ImGui::CollapsingHeader(UL("Profile"), ImGuiTreeNodeFlags_DefaultOpen)) { + static const char* names[] = {"Line", "Rectangle", "Circle", "Polygon"}; + int cur = (int)m_tool; + if (ImGui::Combo("##shape", &cur, names, (int)SketchTool::COUNT)) { + m_tool = (SketchTool)cur; + if (m_tool != SketchTool::Line) build_preset_profile(); + } + ImGui::SameLine(); + if (m_imgui->button("+##newprofile")) m_active_profile = -1; + if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", UL("Start new profile (for holes)")); + + if (m_tool == SketchTool::Rectangle) { + ImGui::SetNextItemWidth(80); if (ImGui::InputDouble("W", &m_rect_w,1,10,"%.0f")) build_preset_profile(); + ImGui::SameLine(); + ImGui::SetNextItemWidth(80); if (ImGui::InputDouble("H", &m_rect_h,1,10,"%.0f")) build_preset_profile(); + } else if (m_tool == SketchTool::Circle) { + ImGui::SetNextItemWidth(80); if (ImGui::InputDouble("R", &m_circle_r,1,5,"%.0f")) build_preset_profile(); + ImGui::SameLine(); + ImGui::SetNextItemWidth(80); if (ImGui::SliderInt("Seg", &m_circle_seg,8,64)) build_preset_profile(); + } else if (m_tool == SketchTool::Polygon) { + ImGui::SetNextItemWidth(80); if (ImGui::SliderInt("Sides", &m_poly_sides,3,12)) build_preset_profile(); + ImGui::SameLine(); + ImGui::SetNextItemWidth(80); if (ImGui::InputDouble("R", &m_poly_r,1,5,"%.0f")) build_preset_profile(); + } else { + ImGui::Text("%s", UL("Click on canvas to draw")); + } + + ImGui::Checkbox(UL("Snap to grid"), &m_snap_grid); + ImGui::SameLine(); + ImGui::SetNextItemWidth(80); ImGui::InputFloat("Step", &m_grid_step, 1, 5, "%.0f mm"); + + draw_canvas(); + + if (!m_profiles.empty()) { + ImGui::Text("%s: %zu", UL("Profiles"), m_profiles.size()); + for (int i = 0; i < (int)m_profiles.size(); ++i) { + auto& p = m_profiles[i]; + ImGui::PushID(i); + bool outer = (i == 0); + ImVec4 col = outer ? ImVec4(0,1,0,1) : ImVec4(1,0.3f,0.3f,1); + const char* label = outer ? "Outer" : "Hole"; + ImGui::TextColored(col, "%s %d: %zu pts %s", label, i+1, p.points.size(), p.closed ? "CLOSED" : ""); + ImGui::SameLine(); + if (ImGui::SmallButton("X")) delete_profile(i); + ImGui::PopID(); + } + } + } + + ImGui::Separator(); + + bool is_revolve = false; + bool has_sel = false; + + if (ImGui::CollapsingHeader(UL("Operation"), ImGuiTreeNodeFlags_DefaultOpen)) { + static int pi = 0; + if (ImGui::Combo(UL("Plane"), &pi, "XY (Top)\0XZ (Front)\0YZ (Side)\0")) + m_plane = (pi==0) ? SketchPlane::XY() : (pi==1) ? SketchPlane::XZ() : SketchPlane::YZ(); + + is_revolve = (m_sp.revolve_deg > 0 && m_sp.revolve_deg < 360); + ImGui::SetNextItemWidth(100); + if (ImGui::InputDouble(UL("Revolve deg"), &m_sp.revolve_deg, 15, 90, "%.0f")) { + if (m_sp.revolve_deg > 360) m_sp.revolve_deg = 360; + if (m_sp.revolve_deg < 0) m_sp.revolve_deg = 0; + } + if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", UL("Set to 0 for extrude, >0 for revolve")); + + if (!is_revolve) { + ImGui::SetNextItemWidth(100); + ImGui::InputDouble(UL("Length"), &m_sp.extrude_len, 0.5, 5, "%.1f mm"); + ImGui::SameLine(); + ImGui::Checkbox(UL("Symmetric"), &m_sp.extrude_sym); + } + + has_sel = !m_parent.get_selection().is_empty(); + if (has_sel) { + if (ImGui::Checkbox(UL("Pocket (cut)"), &m_sp.is_pocket)) + if (m_sp.is_pocket) m_sp.dressup_enabled = false; + } else m_sp.is_pocket = false; + } + + ImGui::Separator(); + + if (!m_sp.is_pocket && ImGui::CollapsingHeader(UL("Fillet / Chamfer"))) { + ImGui::Checkbox(UL("Enable"), &m_sp.dressup_enabled); + if (m_sp.dressup_enabled) { + static const char* dn[] = {"Fillet", "Chamfer"}; + int du = (int)m_sp.dressup_type; + ImGui::SetNextItemWidth(100); + if (ImGui::Combo("##dtype", &du, dn, 2)) m_sp.dressup_type = (DressUpType)du; + static const char* fn[] = {"All edges", "Top edges", "Bottom edges", "Lateral edges"}; + int fg = (int)m_sp.dressup_faces; + ImGui::SetNextItemWidth(140); + ImGui::Combo(UL("Edges"), &fg, fn, 4); m_sp.dressup_faces = (FaceGroup)fg; + ImGui::SetNextItemWidth(100); + if (m_sp.dressup_type == DressUpType::Fillet) + ImGui::InputDouble(UL("Radius"), &m_sp.dressup_radius, 0.1, 1, "%.1f mm"); + else + ImGui::InputDouble(UL("Distance"), &m_sp.dressup_chamfer_dist, 0.1, 1, "%.1f mm"); + } + } + + ImGui::Separator(); + + bool ok = has_closed_profile(); + if (ok) ImGui::TextColored({0,1,0,1}, "%zu %s", m_profiles.size(), UL("closed profile(s)")); + else ImGui::TextColored({0.6f,0.6f,0.6f,1}, "%s", UL("Draw a closed profile to enable")); + + auto btn = [&](const char* label, bool enabled) { + if (!enabled) { ImGui::PushItemFlag(ImGuiItemFlags_Disabled,true); ImGui::PushStyleColor(ImGuiCol_Button,{0.25f,0.25f,0.25f,1}); } + bool clicked = ImGui::Button(label, {-1,0}); + if (!enabled) { ImGui::PopStyleColor(); ImGui::PopItemFlag(); } + return clicked && enabled; + }; + + if (m_sp.is_pocket && has_sel) { + if (btn(L("Pocket (Cut)"), ok)) apply_pocket(); + } else if (is_revolve) { + if (btn(L("Revolve"), ok)) apply_revolve(); + } else { + if (btn(L("Extrude"), ok)) apply_extrude(); + } + + if (ImGui::Button(L("Clear All"), {-1,0})) clear_all(); + if (ImGui::Button(L("Close"), {-1,0})) m_parent.reset_all_gizmos(); + + GizmoImguiEnd(); + ImGuiWrapper::pop_toolbar_style(); +} + +void GLGizmoSketch::apply_extrude() +{ + try { + TopoDS_Shape shape = build_combined_shape(); + TriangleMesh mesh = SketchEngine::tessellate(shape, m_sp.linear_deflection); + if (mesh.its.indices.empty()) throw std::runtime_error("Empty result"); + wxGetApp().plater()->take_snapshot("Sketch Extrude"); + ModelObject* mo = wxGetApp().model().add_object(); + mo->name = "Extrusion"; + mo->add_volume(std::move(mesh))->set_new_unique_id(); + mo->ensure_on_bed(); + wxGetApp().plater()->update(); + clear_all(); + } catch (const std::exception& e) { + wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel, std::string("Extrude: ")+e.what()); + } +} + +void GLGizmoSketch::apply_revolve() +{ + try { + TopoDS_Shape shape = build_combined_shape(); + TriangleMesh mesh = SketchEngine::tessellate(shape, m_sp.linear_deflection); + if (mesh.its.indices.empty()) throw std::runtime_error("Empty result"); + wxGetApp().plater()->take_snapshot("Sketch Revolve"); + ModelObject* mo = wxGetApp().model().add_object(); + mo->name = "Revolve"; + mo->add_volume(std::move(mesh))->set_new_unique_id(); + mo->ensure_on_bed(); + wxGetApp().plater()->update(); + clear_all(); + } catch (const std::exception& e) { + wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel, std::string("Revolve: ")+e.what()); + } +} + +void GLGizmoSketch::apply_pocket() +{ + try { + Selection& sel = m_parent.get_selection(); + int obj_idx = sel.get_object_idx(); + if (obj_idx < 0) throw std::runtime_error("No object selected"); + ModelObject* mo = wxGetApp().model().objects[obj_idx]; + + TopoDS_Wire outer = m_profiles[0].to_occt_wire(m_plane); + BRepBuilderAPI_MakeFace fm(outer); + if (!fm.IsDone()) throw std::runtime_error("Face failed"); + for (size_t i = 1; i < m_profiles.size(); ++i) + if (m_profiles[i].closed) fm.Add(m_profiles[i].to_occt_wire(m_plane)); + fm.Build(); + if (!fm.IsDone()) throw std::runtime_error("Face with holes failed"); + + TopoDS_Shape tool = SketchEngine::make_extrude_face(fm.Face(), m_plane, m_sp.extrude_len + 5.0, false); + TriangleMesh tool_mesh = SketchEngine::tessellate(tool, m_sp.linear_deflection); + if (tool_mesh.its.indices.empty()) throw std::runtime_error("Tool mesh empty"); + + wxGetApp().plater()->take_snapshot("Sketch Pocket"); + mo->add_volume(std::move(tool_mesh), ModelVolumeType::NEGATIVE_VOLUME)->set_new_unique_id(); + mo->ensure_on_bed(); + wxGetApp().plater()->update(); + clear_all(); + wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::RegularNotificationLevel, UL("Pocket added (negative volume)")); + } catch (const std::exception& e) { + wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel, std::string("Pocket: ")+e.what()); + } +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSketch.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSketch.hpp new file mode 100644 index 0000000000..52f6cbedd2 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoSketch.hpp @@ -0,0 +1,74 @@ +#ifndef slic3r_GLGizmoSketch_hpp_ +#define slic3r_GLGizmoSketch_hpp_ + +#include "GLGizmoBase.hpp" +#include "GLGizmosCommon.hpp" +#include "libslic3r/SketchEngine.hpp" +#include + +namespace Slic3r { +namespace GUI { + +enum class SketchTool { Line, Rectangle, Circle, Polygon, COUNT }; + +class GLGizmoSketch : public GLGizmoBase +{ +public: + GLGizmoSketch(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); + + bool on_mouse(const wxMouseEvent& mouse_event) override; + +protected: + bool on_init() override; + std::string on_get_name() const override; + bool on_is_activable() const override; + void on_render() override; + void on_set_state() override; + CommonGizmosDataID on_get_requirements() const override; + void on_render_input_window(float x, float y, float bottom_limit) override; + + void on_load(cereal::BinaryInputArchive& ar) override; + void on_save(cereal::BinaryOutputArchive& ar) const override; + +private: + SketchTool m_tool{SketchTool::Line}; + std::vector m_profiles; // multiple profiles (outer + holes) + SketchPlane m_plane{SketchPlane::XY()}; + SketchParams m_sp; + + // Shape presets + double m_rect_w{20}, m_rect_h{15}; + double m_circle_r{10}; int m_circle_seg{32}; + int m_poly_sides{6}; double m_poly_r{10}; + + // Canvas + std::vector m_canvas_points; + Vec2d m_canvas_center{0,0}; + float m_canvas_scale{5.0f}; + bool m_snap_grid{true}; + float m_grid_step{5.0f}; + + // Current profile being drawn + int m_active_profile{-1}; + + SketchProfile& active_profile(); + bool has_closed_profile() const; + + void build_preset_profile(); + void add_closed_profile(); + void delete_profile(int idx); + void clear_all(); + + TopoDS_Shape build_combined_shape(); // all profiles as face with holes + void apply_extrude(); + void apply_revolve(); + void apply_pocket(); + void draw_canvas(); + void handle_canvas_click(ImVec2 pos); + Vec2d snap(Vec2d pt) const; +}; + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_GLGizmoSketch_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 94c76d896b..65bcecd1f2 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -27,6 +27,8 @@ #include "slic3r/GUI/Gizmos/GLGizmoSVG.hpp" #include "slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp" #include "slic3r/GUI/Gizmos/GLGizmoAssembly.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoPrimitive.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoSketch.hpp" #include "libslic3r/format.hpp" #include "libslic3r/Model.hpp" @@ -176,6 +178,12 @@ void GLGizmosManager::switch_gizmos_icon_filename() case (EType::BrimEars): gizmo->set_icon_filename(m_is_dark ? "toolbar_brimears_dark.svg" : "toolbar_brimears.svg"); break; + case (EType::Primitive): + gizmo->set_icon_filename(m_is_dark ? "toolbar_modifier_cube_dark.svg" : "toolbar_modifier_cube.svg"); + break; + case (EType::Sketch): + gizmo->set_icon_filename(m_is_dark ? "toolbar_sketch_dark.svg" : "toolbar_sketch.svg"); + break; } } @@ -219,6 +227,8 @@ bool GLGizmosManager::init() m_gizmos.emplace_back(new GLGizmoAssembly(m_parent, m_is_dark ? "toolbar_assembly_dark.svg" : "toolbar_assembly.svg", EType::Assembly)); m_gizmos.emplace_back(new GLGizmoSimplify(m_parent, "reduce_triangles.svg", EType::Simplify)); m_gizmos.emplace_back(new GLGizmoBrimEars(m_parent, m_is_dark ? "toolbar_brimears_dark.svg" : "toolbar_brimears.svg", EType::BrimEars)); + m_gizmos.emplace_back(new GLGizmoPrimitive(m_parent, m_is_dark ? "toolbar_modifier_cube_dark.svg" : "toolbar_modifier_cube.svg", static_cast(Primitive))); + m_gizmos.emplace_back(new GLGizmoSketch(m_parent, m_is_dark ? "toolbar_sketch_dark.svg" : "toolbar_sketch.svg", static_cast(Sketch))); //m_gizmos.emplace_back(new GLGizmoSlaSupports(m_parent, "sla_supports.svg", sprite_id++)); //m_gizmos.emplace_back(new GLGizmoFaceDetector(m_parent, "face recognition.svg", sprite_id++)); //m_gizmos.emplace_back(new GLGizmoHollow(m_parent, "hollow.svg", sprite_id++)); diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 01814521aa..d45cd52f77 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -90,6 +90,8 @@ public: Assembly, Simplify, BrimEars, + Primitive, + Sketch, //SlaSupports, // BBS //FaceRecognition, diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 66d7d87835..543d665a71 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -37,6 +37,7 @@ #include "I18N.hpp" #include "GLCanvas3D.hpp" #include "Plater.hpp" +#include "DesignPanel.hpp" #include "WebViewDialog.hpp" #include "../Utils/Process.hpp" #include "format.hpp" @@ -1014,6 +1015,8 @@ void MainFrame::update_layout() { case ESettingsLayout::Old: { + m_design_panel->Reparent(m_tabpanel); + m_tabpanel->InsertPage(tpDesign, m_design_panel, _L("Design"), std::string("tab_design_active"), std::string("tab_design_active"), false); m_plater->Reparent(m_tabpanel); m_tabpanel->InsertPage(tp3DEditor, m_plater, _L("Prepare"), std::string("tab_3d_active"), std::string("tab_3d_active"), false); m_tabpanel->InsertPage(tpPreview, m_plater, _L("Preview"), std::string("tab_preview_active"), std::string("tab_preview_active"), false); @@ -1270,6 +1273,12 @@ void MainFrame::init_tabpanel() { } //else if (panel == m_param_panel) // m_param_panel->OnActivate(); + else if (panel == m_design_panel) { + // Re-sync the Design bed to the active printer: the panel is built before the + // printer profile is fully applied, so its bed must refresh on activation or the + // grid (true bed) spills past the stale default bed quad. + m_design_panel->on_tab_shown(); + } else if (panel == m_monitor) { //monitor } @@ -1316,11 +1325,13 @@ void MainFrame::init_tabpanel() { } m_plater = new Plater(this, this); + // Register the plater with the app BEFORE constructing DesignPanel: its + // DesignCanvas reads wxGetApp().plater()->config() at construction time. + wxGetApp().plater_ = m_plater; + m_design_panel = new DesignPanel(this); m_plater->SetBackgroundColour(*wxWHITE); m_plater->Hide(); - wxGetApp().plater_ = m_plater; - create_preset_tabs(); //BBS add pages diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 20229a611e..47d767e981 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -50,6 +50,7 @@ namespace GUI class Tab; class PrintHostQueueDialog; class Plater; +class DesignPanel; class MainFrame; class ParamsDialog; #ifdef __WXGTK__ @@ -218,14 +219,15 @@ public: enum TabPosition { tpHome = 0, - tp3DEditor = 1, - tpPreview = 2, - tpMonitor = 3, - tpMultiDevice = 4, - tpProject = 5, - tpCalibration = 6, - tpAuxiliary = 7, - toDebugTool = 8, + tpDesign = 1, + tp3DEditor = 2, + tpPreview = 3, + tpMonitor = 4, + tpMultiDevice = 5, + tpProject = 6, + tpCalibration = 7, + tpAuxiliary = 8, + toDebugTool = 9, }; //BBS: add slice&&print status update logic @@ -375,6 +377,7 @@ public: BBLTopbar* m_topbar{ nullptr }; PrintHostQueueDialog* printhost_queue_dlg() { return m_printhost_queue_dlg; } Plater* m_plater { nullptr }; + DesignPanel* m_design_panel { nullptr }; //BBS: GUI refactor MonitorPanel* m_monitor{ nullptr }; diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 18d6e17b33..86295925e7 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -3384,16 +3384,20 @@ void PartPlate::render(const Transform3d& view_matrix, const Transform3d& projec if (wxGetApp().show_plate_gridlines() && show_grid) render_grid(bottom); - if (!bottom && m_selected && !force_background_color) { + const bool hide_chrome = m_partplate_list && m_partplate_list->get_hide_chrome(); + + if (!hide_chrome && !bottom && m_selected && !force_background_color) { if (m_partplate_list) render_logo(bottom, m_partplate_list->render_cali_logo && render_cali); else render_logo(bottom); } - render_icons(bottom, only_body, hover_id); - if (!force_background_color) { - render_only_numbers(bottom); + if (!hide_chrome) { + render_icons(bottom, only_body, hover_id); + if (!force_background_color) { + render_only_numbers(bottom); + } } glsafe(::glDisable(GL_DEPTH_TEST)); diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index d4c5399142..1207dd9e15 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -614,6 +614,11 @@ class PartPlateList : public ObjectBase bool render_bedtype_logo = true; bool render_plate_settings = true; bool render_cali_logo = true; + // SnapOrca Design: when true, PartPlate::render skips all overlay chrome + // (corner icons, logo watermark, plate numbers) but keeps the bed grid. + // Toggled transiently per-frame by GLCanvas3D::_render_platelist for the + // DesignCanvas; stays false for the main editor. + bool m_hide_chrome = false; bool m_is_dark = false; @@ -838,6 +843,8 @@ public: void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current = false, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true); void set_render_option(bool bedtype_texture, bool plate_settings); void set_render_cali(bool value = true) { render_cali_logo = value; } + void set_hide_chrome(bool value) { m_hide_chrome = value; } + bool get_hide_chrome() const { return m_hide_chrome; } void register_raycasters_for_picking(GLCanvas3D& canvas) { for (auto plate : m_plate_list) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 843246e215..a64acd439c 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -17477,6 +17477,11 @@ PartPlateList& Plater::get_partplate_list() return p->partplate_list; } +BackgroundSlicingProcess* Plater::get_background_process() +{ + return &p->background_process; +} + void Plater::apply_background_progress() { PartPlate* part_plate = p->partplate_list.get_curr_plate(); diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index c70c5ca7c1..0839c72256 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -46,6 +46,7 @@ class Model; class ModelObject; class ModelInstance; class Print; +class BackgroundSlicingProcess; class SLAPrint; //BBS: add partplatelist and SlicingStatusEvent class PartPlateList; @@ -706,6 +707,10 @@ public: //BBS: partplate list related functions PartPlateList& get_partplate_list(); + // Shared background slicing process (same instance View3D/Preview/AssembleView + // use). Exposed so the Design tab's native GLCanvas3D can be wired exactly like + // the editor canvases (GLCanvas3D::render() dereferences the process). + BackgroundSlicingProcess* get_background_process(); void validate_current_plate(bool& model_fits, bool& validate_error); //BBS: select the plate by index int select_plate(int plate_index, bool need_slice = false); diff --git a/src/slic3r/GUI/SketchInlineEditor.cpp b/src/slic3r/GUI/SketchInlineEditor.cpp new file mode 100644 index 0000000000..dcc1f54343 --- /dev/null +++ b/src/slic3r/GUI/SketchInlineEditor.cpp @@ -0,0 +1,133 @@ +#include "SketchInlineEditor.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace Slic3r { +namespace GUI { + +namespace { +// Locale-safe value <-> text (wx sets LC_NUMERIC to the user locale, so snprintf may +// emit a comma; parsing accepts either separator). Mirrors DesignPanel's en_*. +wxString en_format(double v, int digits = 2) +{ + char fmt[16]; + std::snprintf(fmt, sizeof(fmt), "%%.%df", digits); + char buf[64]; + std::snprintf(buf, sizeof(buf), fmt, v); + for (char* c = buf; *c; ++c) if (*c == ',') *c = '.'; + return wxString::FromUTF8(buf); +} +bool en_parse(const wxString& text, double& out) +{ + wxString t(text); + t.Replace(wxT(","), wxT(".")); + return t.ToCDouble(&out); +} +} // namespace + +SketchInlineEditor::SketchInlineEditor(wxWindow* parent_canvas) +{ + wxWindow* top = parent_canvas ? wxGetTopLevelParent(parent_canvas) : nullptr; + // Borderless floating frame: a top-level window so the WM composites it above the + // GL canvas (a child widget would be hidden by the GL surface). Floats on its + // parent and stays on top so it tracks the main window. + // NB: no wxFRAME_FLOAT_ON_PARENT — that maps to a GTK _UTILITY_ window-type hint, which + // many WMs (incl. the xrdp/x11vnc session on :10) refuse to give keyboard focus, so the + // field opened un-focusable and needed a click before typing. Plain stay-on-top frame is + // WM-focusable; we present + SetFocus it explicitly in open(). + m_frame = new wxFrame(top, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, + wxFRAME_NO_TASKBAR | wxBORDER_NONE | wxSTAY_ON_TOP); + m_ctrl = new wxTextCtrl(m_frame, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(82, -1), + wxTE_PROCESS_ENTER | wxTE_RIGHT | wxBORDER_SIMPLE); + auto* sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(m_ctrl, 1, wxEXPAND); + m_frame->SetSizerAndFit(sizer); + m_frame->Hide(); + + m_ctrl->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { do_commit(); }); + m_ctrl->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& e) { + if (e.GetKeyCode() == WXK_ESCAPE) do_cancel(); + else e.Skip(); + }); +} + +void SketchInlineEditor::open(const wxPoint& screen_px, double value, + std::function on_commit, + std::function on_cancel) +{ + if (m_frame == nullptr || m_ctrl == nullptr) { if (on_cancel) on_cancel(); return; } + if (m_open) close(); + m_commit = std::move(on_commit); + m_cancel = std::move(on_cancel); + m_ctrl->ChangeValue(en_format(value)); + m_frame->Fit(); + const wxSize sz = m_frame->GetSize(); + wxPoint pos(screen_px.x - sz.GetWidth() / 2, screen_px.y - sz.GetHeight() / 2); + // Keep the frame fully on-screen: an anchor that maps off the display makes GTK drop + // the window at a default corner (top-left) instead of the requested point. + const wxRect area = wxGetClientDisplayRect(); + pos.x = std::max(area.GetLeft(), std::min(pos.x, area.GetRight() - sz.GetWidth())); + pos.y = std::max(area.GetTop(), std::min(pos.y, area.GetBottom() - sz.GetHeight())); + // Show() BEFORE Move(): GTK ignores a Move() issued before the window is mapped (the + // WM places it at its default, i.e. the top-left corner). Move after Show sticks. + m_frame->Show(); + m_frame->Move(pos); + m_frame->Raise(); // gtk_window_present -> activate the top-level so SetFocus routes + m_frame->SetFocus(); + m_ctrl->SetFocus(); + m_ctrl->SelectAll(); + m_open = true; + // Re-assert on the next tick too: the GL canvas can reclaim focus while it finishes + // handling the click/render that opened us, so a single immediate SetFocus may be stolen. + m_ctrl->CallAfter([this] { + if (m_open && m_ctrl) { m_frame->Raise(); m_ctrl->SetFocus(); m_ctrl->SelectAll(); } + }); +} + +void SketchInlineEditor::do_commit() +{ + if (!m_open || m_ctrl == nullptr) return; + double v = 0.0; + if (!en_parse(m_ctrl->GetValue(), v)) { // invalid: keep editing + m_ctrl->SetFocus(); + m_ctrl->SelectAll(); + return; + } + auto cb = m_commit; // copy-then-close: the callback re-enters (re-solve + render) + close(); + if (cb) cb(v); +} + +void SketchInlineEditor::cancel() +{ + if (m_open) do_cancel(); +} + +void SketchInlineEditor::do_cancel() +{ + if (!m_open) return; + auto cb = m_cancel; + close(); + if (cb) cb(); +} + +void SketchInlineEditor::close() +{ + if (m_frame == nullptr || !m_open) return; + m_closing = true; + m_open = false; + m_frame->Hide(); + m_commit = nullptr; + m_cancel = nullptr; + m_closing = false; +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/SketchInlineEditor.hpp b/src/slic3r/GUI/SketchInlineEditor.hpp new file mode 100644 index 0000000000..02a47daa06 --- /dev/null +++ b/src/slic3r/GUI/SketchInlineEditor.hpp @@ -0,0 +1,48 @@ +#ifndef slic3r_SketchInlineEditor_hpp_ +#define slic3r_SketchInlineEditor_hpp_ + +#include + +class wxWindow; +class wxFrame; +class wxTextCtrl; +class wxPoint; + +namespace Slic3r { +namespace GUI { + +// Onshape-style in-canvas value editor: a small borderless floating frame holding a +// wxTextCtrl, shown at screen coordinates over the GL canvas. A top-level frame is +// used (not a child widget) because a native child cannot be composited over the +// double-buffered wxGLCanvas under GTK3/llvmpipe — it stays invisible. Enter (or blur) +// commits the parsed number, Esc cancels. This is the single numeric-entry path for +// sketch dimensions, replacing the docked/modal value cards. +class SketchInlineEditor +{ +public: + explicit SketchInlineEditor(wxWindow* parent_canvas); + + // Show the editor centred on `screen_px` (absolute screen coords), pre-filled with + // `value`. on_commit(parsed) fires on Enter with a valid number; on_cancel() on Esc. + void open(const wxPoint& screen_px, double value, + std::function on_commit, + std::function on_cancel); + void close(); + void cancel(); // if open, run the registered cancel (keep-as-drawn) + bool is_open() const { return m_open; } + +private: + void do_commit(); + void do_cancel(); + + wxFrame* m_frame{nullptr}; + wxTextCtrl* m_ctrl{nullptr}; + std::function m_commit; + std::function m_cancel; + bool m_open{false}; + bool m_closing{false}; +}; + +}} // namespace Slic3r::GUI + +#endif // slic3r_SketchInlineEditor_hpp_ diff --git a/tests/libslic3r/test_caddocument.cpp b/tests/libslic3r/test_caddocument.cpp new file mode 100644 index 0000000000..800b4bac6f --- /dev/null +++ b/tests/libslic3r/test_caddocument.cpp @@ -0,0 +1,1366 @@ +#include + +#include "libslic3r/CadDocument.hpp" +#include "libslic3r/SketchEngine.hpp" +#include "libslic3r/SketchImport.hpp" +#include "libslic3r/ThreadStandards.hpp" +#include "libslic3r/Utils.hpp" + +#include +#include +#include +#include +#include +#include +#include + +using namespace Slic3r; + +TEST_CASE("CadDocument profile sketch -> extrude -> solid", "[CadDocument]") +{ + CadDocument doc; + + SketchProfile sp; + sp.points.push_back(Vec2d(-10, -10)); + sp.points.push_back(Vec2d( 10, -10)); + sp.points.push_back(Vec2d( 10, 10)); + sp.points.push_back(Vec2d(-10, 10)); + sp.closed = true; + + int sk_idx = doc.add_sketch_profile(sp, SketchPlane::XY(), "SquareProfile"); + REQUIRE(sk_idx >= 0); + doc.add_extrude(sk_idx, 5.0, false, BooleanMode::New, "Extrude1"); + + bool ok = doc.recompute(); + REQUIRE(ok); + REQUIRE(doc.error.empty()); + REQUIRE(doc.display_mesh.facets_count() > 0); +} + +TEST_CASE("CadDocument legacy Rectangle sketch still works", "[CadDocument]") +{ + CadDocument doc; + + int sk_idx = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), + 20, 20, 10, "RectSketch"); + REQUIRE(sk_idx >= 0); + doc.add_extrude(sk_idx, 5.0, false, BooleanMode::New, "Extrude1"); + + bool ok = doc.recompute(); + REQUIRE(ok); + REQUIRE(doc.error.empty()); + REQUIRE(doc.display_mesh.facets_count() > 0); +} + +TEST_CASE("CadDocument preview with profile on self-contained candidate", "[CadDocument]") +{ + CadDocument doc; + + CadFeature candidate; + candidate.type = CadFeatureType::Extrude; + candidate.plane = SketchPlane::XY(); + candidate.distance = 4; + candidate.mode = BooleanMode::New; + // sketch_ref is -1 by default -> apply_feature uses candidate's own params + + SketchProfile tri; + tri.points.push_back(Vec2d(0, 0)); + tri.points.push_back(Vec2d(10, 0)); + tri.points.push_back(Vec2d(5, 8.66)); + tri.closed = true; + candidate.profile = tri; + + TriangleMesh mesh; + std::string err; + bool ok = doc.preview(candidate, mesh, err); + REQUIRE(ok); + REQUIRE(mesh.facets_count() > 0); +} + +TEST_CASE("CadDocument solve_sketch_feature snaps a rough quad to a rectangle", "[CadDocument]") +{ + CadDocument doc; + SketchProfile sp; + sp.points = { Vec2d(0,0), Vec2d(8,1), Vec2d(9,5), Vec2d(-1,4) }; + sp.closed = true; + int sk = doc.add_sketch_profile(sp, SketchPlane::XY(), "S"); + + auto& cons = doc.features[sk].constraints; + cons.push_back({SketchConstraintType::Fix, 0,-1,-1,-1, 0}); + cons.push_back({SketchConstraintType::LockX, 0,-1,-1,-1, 0}); + cons.push_back({SketchConstraintType::LockY, 0,-1,-1,-1, 0}); + cons.push_back({SketchConstraintType::Horizontal, 0, 1,-1,-1, 0}); + cons.push_back({SketchConstraintType::Vertical, 1, 2,-1,-1, 0}); + cons.push_back({SketchConstraintType::Horizontal, 2, 3,-1,-1, 0}); + cons.push_back({SketchConstraintType::Vertical, 3, 0,-1,-1, 0}); + cons.push_back({SketchConstraintType::Distance, 0, 1,-1,-1, 10}); + cons.push_back({SketchConstraintType::Distance, 1, 2,-1,-1, 6}); + + REQUIRE(doc.solve_sketch_feature(sk)); + const auto& pts = doc.features[sk].profile.points; + REQUIRE_THAT(pts[1].x(), Catch::Matchers::WithinAbs(10.0, 1e-3)); + REQUIRE_THAT(pts[1].y(), Catch::Matchers::WithinAbs(0.0, 1e-3)); + REQUIRE_THAT(pts[2].x(), Catch::Matchers::WithinAbs(10.0, 1e-3)); + REQUIRE_THAT(pts[2].y(), Catch::Matchers::WithinAbs(6.0, 1e-3)); + REQUIRE_THAT(pts[3].x(), Catch::Matchers::WithinAbs(0.0, 1e-3)); + REQUIRE_THAT(pts[3].y(), Catch::Matchers::WithinAbs(6.0, 1e-3)); + + // the solved profile still extrudes into a solid + doc.add_extrude(sk, 5.0, false, BooleanMode::New, "E"); + REQUIRE(doc.recompute()); + REQUIRE(doc.display_mesh.facets_count() > 0); +} + +TEST_CASE("sketch entities -> wire -> extrude", "[CadDocument]") +{ + SECTION("square from 4 lines") { + CadDocument doc; + + CadFeature sk; + sk.type = CadFeatureType::Sketch; + sk.name = "square"; + sk.plane = SketchPlane::XY(); + sk.entities = { + {SketchEntity::Type::Line, Vec2d(-10,-10), Vec2d(10,-10)}, + {SketchEntity::Type::Line, Vec2d(10,-10), Vec2d(10,10)}, + {SketchEntity::Type::Line, Vec2d(10,10), Vec2d(-10,10)}, + {SketchEntity::Type::Line, Vec2d(-10,10), Vec2d(-10,-10)}, + }; + doc.features.push_back(sk); + + CadFeature ex; + ex.type = CadFeatureType::Extrude; + ex.name = "extrude"; + ex.sketch_ref = 0; + ex.distance = 5; + ex.mode = BooleanMode::New; + doc.features.push_back(ex); + + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE(doc.display_mesh.facets_count() > 0); + + auto bb = doc.display_mesh.bounding_box(); + auto sz = bb.max - bb.min; + REQUIRE(std::abs(sz.x() - 20.0) < 0.5); + REQUIRE(std::abs(sz.y() - 20.0) < 0.5); + REQUIRE(std::abs(sz.z() - 5.0) < 0.5); + } + + SECTION("single circle") { + CadDocument doc; + + CadFeature sk; + sk.type = CadFeatureType::Sketch; + sk.name = "circle"; + sk.plane = SketchPlane::XY(); + sk.entities = { + {SketchEntity::Type::Circle, Vec2d(0,0), Vec2d(0,0), Vec2d(0,0), 10.0}, + }; + doc.features.push_back(sk); + + CadFeature ex; + ex.type = CadFeatureType::Extrude; + ex.name = "extrude"; + ex.sketch_ref = 0; + ex.distance = 8; + ex.mode = BooleanMode::New; + doc.features.push_back(ex); + + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE(doc.display_mesh.facets_count() > 0); + + auto bb = doc.display_mesh.bounding_box(); + auto sz = bb.max - bb.min; + REQUIRE(std::abs(sz.x() - 20.0) < 0.5); + REQUIRE(std::abs(sz.y() - 20.0) < 0.5); + REQUIRE(std::abs(sz.z() - 8.0) < 0.5); + } + + SECTION("construction line excluded") { + CadDocument doc; + + CadFeature sk; + sk.type = CadFeatureType::Sketch; + sk.name = "square_with_construction"; + sk.plane = SketchPlane::XY(); + + SketchEntity cline; + cline.type = SketchEntity::Type::Line; + cline.p0 = Vec2d(-10, -10); + cline.p1 = Vec2d(10, 10); + cline.construction = true; + + sk.entities = { + {SketchEntity::Type::Line, Vec2d(-10,-10), Vec2d(10,-10)}, + {SketchEntity::Type::Line, Vec2d(10,-10), Vec2d(10,10)}, + {SketchEntity::Type::Line, Vec2d(10,10), Vec2d(-10,10)}, + {SketchEntity::Type::Line, Vec2d(-10,10), Vec2d(-10,-10)}, + cline, + }; + doc.features.push_back(sk); + + CadFeature ex; + ex.type = CadFeatureType::Extrude; + ex.name = "extrude"; + ex.sketch_ref = 0; + ex.distance = 5; + ex.mode = BooleanMode::New; + doc.features.push_back(ex); + + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE(doc.display_mesh.facets_count() > 0); + + auto bb = doc.display_mesh.bounding_box(); + auto sz = bb.max - bb.min; + REQUIRE(std::abs(sz.x() - 20.0) < 0.5); + REQUIRE(std::abs(sz.y() - 20.0) < 0.5); + REQUIRE(std::abs(sz.z() - 5.0) < 0.5); + } +} + +// Mirrors the GUI interactive-sketch commit path (DesignPanel -> +// add_sketch_entities) for the Fase 4.1 entity drawing tools: a corner-rect and +// a center-rect produce 4 closed Line entities; a center-circle produces 1 +// Circle entity. add_sketch_entities must store them and extrude into a solid. +TEST_CASE("add_sketch_entities commit path -> extrude", "[CadDocument]") +{ + SECTION("corner-rect 4 lines -> 30x16x5") { + CadDocument doc; + // Corner A=(-15,-8), B=(15,8): the tool's push_closed_lines order. + const Vec2d A(-15, -8), B(15, 8); + std::vector ents = { + {SketchEntity::Type::Line, A, Vec2d(B.x(), A.y())}, + {SketchEntity::Type::Line, Vec2d(B.x(),A.y()), B}, + {SketchEntity::Type::Line, B, Vec2d(A.x(), B.y())}, + {SketchEntity::Type::Line, Vec2d(A.x(),B.y()), A}, + }; + int sk = doc.add_sketch_entities(ents, SketchPlane::XY(), "Sketch1"); + REQUIRE(sk == 0); + doc.add_extrude(sk, 5.0, false, BooleanMode::New, "Extrude1"); + + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE(doc.display_mesh.facets_count() > 0); + auto sz = doc.display_mesh.bounding_box().size(); + REQUIRE(std::abs(sz.x() - 30.0) < 0.5); + REQUIRE(std::abs(sz.y() - 16.0) < 0.5); + REQUIRE(std::abs(sz.z() - 5.0) < 0.5); + } + + SECTION("center-circle 1 entity -> r=7 cylinder") { + CadDocument doc; + SketchEntity c; + c.type = SketchEntity::Type::Circle; + c.center = Vec2d(0, 0); + c.p0 = Vec2d(0, 0); + c.radius = 7.0; + int sk = doc.add_sketch_entities({c}, SketchPlane::XY(), "Sketch1"); + doc.add_extrude(sk, 4.0, false, BooleanMode::New, "Extrude1"); + + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE(doc.display_mesh.facets_count() > 0); + auto sz = doc.display_mesh.bounding_box().size(); + REQUIRE(std::abs(sz.x() - 14.0) < 0.5); + REQUIRE(std::abs(sz.y() - 14.0) < 0.5); + REQUIRE(std::abs(sz.z() - 4.0) < 0.5); + } +} + +// A slot (stadium): 2 lines + 2 semicircular Arc entities forming one closed +// loop — the shape the Fase 4.1b Slot tool emits. Validates the kernel's Arc +// edge path (GC_MakeArcOfCircle via center/radius/start_angle/end_angle, with +// the mid reconstructed at (start+end)/2) inside a mixed Line/Arc wire. +TEST_CASE("slot (line+arc closed wire) -> extrude", "[CadDocument]") +{ + const double PI = 3.14159265358979323846; + CadDocument doc; + + // Centerline ends c0=(-10,0), c1=(10,0); half-width w=5 → stadium 30 x 10. + SketchEntity top; // top line A0(-10,5) -> A1(10,5) + top.type = SketchEntity::Type::Line; top.p0 = Vec2d(-10, 5); top.p1 = Vec2d(10, 5); + + SketchEntity cap1; // right cap @c1=(10,0): A1(10,5) -> B1(10,-5) through (15,0) + cap1.type = SketchEntity::Type::Arc; cap1.center = Vec2d(10, 0); cap1.radius = 5; + cap1.p0 = Vec2d(10, 5); cap1.p1 = Vec2d(10, -5); + cap1.start_angle = PI / 2; cap1.end_angle = -PI / 2; // mid angle 0 -> (15,0) + + SketchEntity bot; // bottom line B1(10,-5) -> B0(-10,-5) + bot.type = SketchEntity::Type::Line; bot.p0 = Vec2d(10, -5); bot.p1 = Vec2d(-10, -5); + + SketchEntity cap0; // left cap @c0=(-10,0): B0(-10,-5) -> A0(-10,5) through (-15,0) + cap0.type = SketchEntity::Type::Arc; cap0.center = Vec2d(-10, 0); cap0.radius = 5; + cap0.p0 = Vec2d(-10, -5); cap0.p1 = Vec2d(-10, 5); + cap0.start_angle = -PI / 2; cap0.end_angle = -3 * PI / 2; // mid angle -PI -> (-15,0) + + int sk = doc.add_sketch_entities({top, cap1, bot, cap0}, SketchPlane::XY(), "Slot"); + doc.add_extrude(sk, 4.0, false, BooleanMode::New, "Extrude1"); + + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE(doc.display_mesh.facets_count() > 0); + auto sz = doc.display_mesh.bounding_box().size(); + REQUIRE(std::abs(sz.x() - 30.0) < 0.5); + REQUIRE(std::abs(sz.y() - 10.0) < 0.5); + REQUIRE(std::abs(sz.z() - 4.0) < 0.5); +} + +// Onshape-style constraints on coexisting entities (Fase 4.2). The solver maps +// each Line/Point entity endpoint to a solver variable, applies the entity +// constraints, and writes the solved coordinates back into the entities. +TEST_CASE("entity constraints: solve on SketchEntity endpoints", "[CadDocument]") +{ + using R = SketchPointRole; + using T = SketchConstraintType; + + auto dir = [](const SketchEntity& e) { return Vec2d(e.p1 - e.p0); }; + + SECTION("perpendicular rotates line1 normal to a pinned line0") { + CadDocument doc; + std::vector ents = { + {SketchEntity::Type::Line, Vec2d(0,0), Vec2d(10,0)}, // line0 (pinned) + {SketchEntity::Type::Line, Vec2d(0,0), Vec2d(7,7)}, // line1 @45 deg + }; + int sk = doc.add_sketch_entities(ents, SketchPlane::XY(), "S"); + auto& ec = doc.features[sk].entity_constraints; + ec.push_back({T::Fix, 0, -1, R::P0, R::P0, 0.0}); + ec.push_back({T::Fix, 0, -1, R::P1, R::P0, 0.0}); + ec.push_back({T::Perpendicular, 0, 1, R::P0, R::P0, 0.0}); + + REQUIRE(doc.solve_sketch_feature(sk)); + const auto& e = doc.features[sk].entities; + // line0 stayed put. + REQUIRE(std::abs(e[0].p0.x() - 0.0) < 1e-6); + REQUIRE(std::abs(e[0].p1.x() - 10.0) < 1e-6); + // line1 is now perpendicular to line0: directions dot to ~0. + const double d = dir(e[0]).dot(dir(e[1])); + REQUIRE(std::abs(d) < 1e-6); + } + + // Driving length: the Dimension tool records a Distance between a line's own + // P0/P1 (committed via add_sketch_entities' constraints arg). Solving drives + // the line to that exact length. + SECTION("driving length: Distance(P0,P1) sets a line's length") { + CadDocument doc; + std::vector ents = { + {SketchEntity::Type::Line, Vec2d(0,0), Vec2d(10,0)}, // length 10 + }; + std::vector cons; + cons.push_back({T::Fix, 0, -1, R::P0, R::P0, 0.0}); // pin the start + cons.push_back({T::Distance, 0, 0, R::P0, R::P1, 25.0}); // length -> 25 + int sk = doc.add_sketch_entities(ents, SketchPlane::XY(), "S", cons); + REQUIRE(doc.features[sk].entity_constraints.size() == 2); // constraints stored + REQUIRE(doc.solve_sketch_feature(sk)); + const auto& e = doc.features[sk].entities; + REQUIRE(std::abs((e[0].p1 - e[0].p0).norm() - 25.0) < 1e-6); + } + + SECTION("parallel flattens line1 onto a pinned horizontal line0") { + CadDocument doc; + std::vector ents = { + {SketchEntity::Type::Line, Vec2d(0,0), Vec2d(10,0)}, // line0 (pinned) + {SketchEntity::Type::Line, Vec2d(0,5), Vec2d(7,9)}, // line1 tilted + }; + int sk = doc.add_sketch_entities(ents, SketchPlane::XY(), "S"); + auto& ec = doc.features[sk].entity_constraints; + ec.push_back({T::Fix, 0, -1, R::P0, R::P0, 0.0}); + ec.push_back({T::Fix, 0, -1, R::P1, R::P0, 0.0}); + ec.push_back({T::Parallel, 0, 1, R::P0, R::P0, 0.0}); + + REQUIRE(doc.solve_sketch_feature(sk)); + const auto& e = doc.features[sk].entities; + const Vec2d d0 = dir(e[0]), d1 = dir(e[1]); + const double cross = d0.x() * d1.y() - d0.y() * d1.x(); + REQUIRE(std::abs(cross) < 1e-6); + } + + SECTION("coincident merges a line endpoint onto another") { + CadDocument doc; + std::vector ents = { + {SketchEntity::Type::Line, Vec2d(0,0), Vec2d(10,0)}, // line0 + {SketchEntity::Type::Line, Vec2d(12,1), Vec2d(20,1)}, // line1 + }; + int sk = doc.add_sketch_entities(ents, SketchPlane::XY(), "S"); + auto& ec = doc.features[sk].entity_constraints; + ec.push_back({T::Coincident, 0, 1, R::P1, R::P0, 0.0}); // line0.end == line1.start + + REQUIRE(doc.solve_sketch_feature(sk)); + const auto& e = doc.features[sk].entities; + const Vec2d gap = Vec2d(e[0].p1 - e[1].p0); + REQUIRE(gap.norm() < 1e-6); + } + + SECTION("horizontal levels a tilted line's endpoints") { + CadDocument doc; + std::vector ents = { + {SketchEntity::Type::Line, Vec2d(0,0), Vec2d(10,2)}, // tilted line + }; + int sk = doc.add_sketch_entities(ents, SketchPlane::XY(), "S"); + auto& ec = doc.features[sk].entity_constraints; + ec.push_back({T::Horizontal, 0, 0, R::P0, R::P1, 0.0}); // p0.y == p1.y + + REQUIRE(doc.solve_sketch_feature(sk)); + const auto& e = doc.features[sk].entities; + REQUIRE(std::abs(e[0].p0.y() - e[0].p1.y()) < 1e-6); + } +} + +TEST_CASE("entity constraints: arc/circle registration + concentric", "[CadDocument]") +{ + using R = SketchPointRole; + using T = SketchConstraintType; + + SECTION("concentric centers coincide") { + CadDocument doc; + std::vector ents = { + {SketchEntity::Type::Circle, Vec2d(0,0), Vec2d(0,0), Vec2d(0,0), 5.0}, // circle0 + {SketchEntity::Type::Circle, Vec2d(10,2), Vec2d(10,2), Vec2d(10,2), 3.0}, // circle1 + }; + int sk = doc.add_sketch_entities(ents, SketchPlane::XY(), "S"); + auto& ec = doc.features[sk].entity_constraints; + ec.push_back({T::Fix, 0, -1, R::Center, R::Center, 0.0}); + ec.push_back({T::Concentric, 0, 1, R::Center, R::Center, 0.0}); + + REQUIRE(doc.solve_sketch_feature(sk)); + const auto& e = doc.features[sk].entities; + REQUIRE((e[1].center - e[0].center).norm() < 1e-6); + REQUIRE(e[0].center.x() < 1e-6); + REQUIRE(e[0].center.y() < 1e-6); + } + + SECTION("arc reflow keeps radius and angle consistent") { + CadDocument doc; + const double PI2 = M_PI / 2; + std::vector ents = { + {SketchEntity::Type::Arc, Vec2d(5,0), Vec2d(0,5), Vec2d(0,0), 5.0, 0.0, PI2}, + }; + int sk = doc.add_sketch_entities(ents, SketchPlane::XY(), "S"); + auto& ec = doc.features[sk].entity_constraints; + ec.push_back({T::Fix, 0, -1, R::Center, R::Center, 0.0}); + ec.push_back({T::Fix, 0, -1, R::P0, R::P0, 0.0}); + + REQUIRE(doc.solve_sketch_feature(sk)); + const auto& e = doc.features[sk].entities; + REQUIRE(std::abs((e[0].p0 - e[0].center).norm() - 5.0) < 1e-6); + REQUIRE(std::abs(e[0].start_angle - 0.0) < 1e-6); + REQUIRE(std::abs(e[0].end_angle - PI2) < 1e-3); + REQUIRE(e[0].end_angle > e[0].start_angle); + } +} + +TEST_CASE("entity constraints: radius/diameter dimensions", "[CadDocument]") +{ + using R = SketchPointRole; + using T = SketchConstraintType; + + SECTION("circle radius dimension") { + CadDocument doc; + std::vector ents = { + {SketchEntity::Type::Circle, Vec2d(0,0), Vec2d(0,0), Vec2d(0,0), 5.0}, + }; + int sk = doc.add_sketch_entities(ents, SketchPlane::XY(), "S"); + auto& ec = doc.features[sk].entity_constraints; + ec.push_back({T::Fix, 0, -1, R::Center, R::Center, 0.0, -1, R::P0}); + ec.push_back({T::Radius, 0, -1, R::Center, R::P0, 8.0, -1, R::P0}); + + REQUIRE(doc.solve_sketch_feature(sk)); + const auto& e = doc.features[sk].entities; + REQUIRE(std::abs(e[0].radius - 8.0) < 1e-9); + } + + SECTION("circle diameter dimension") { + CadDocument doc; + std::vector ents = { + {SketchEntity::Type::Circle, Vec2d(0,0), Vec2d(0,0), Vec2d(0,0), 5.0}, + }; + int sk = doc.add_sketch_entities(ents, SketchPlane::XY(), "S"); + auto& ec = doc.features[sk].entity_constraints; + ec.push_back({T::Fix, 0, -1, R::Center, R::Center, 0.0, -1, R::P0}); + ec.push_back({T::Diameter, 0, -1, R::Center, R::P0, 20.0, -1, R::P0}); + + REQUIRE(doc.solve_sketch_feature(sk)); + const auto& e = doc.features[sk].entities; + REQUIRE(std::abs(e[0].radius - 10.0) < 1e-9); + } + + SECTION("arc radius rescales endpoints") { + CadDocument doc; + std::vector ents = { + {SketchEntity::Type::Arc, Vec2d(5,0), Vec2d(0,5), Vec2d(0,0), 5.0, 0.0, M_PI/2}, + }; + int sk = doc.add_sketch_entities(ents, SketchPlane::XY(), "S"); + auto& ec = doc.features[sk].entity_constraints; + ec.push_back({T::Fix, 0, -1, R::Center, R::Center, 0.0, -1, R::P0}); + ec.push_back({T::Radius, 0, -1, R::Center, R::P0, 10.0, -1, R::P0}); + + REQUIRE(doc.solve_sketch_feature(sk)); + const auto& e = doc.features[sk].entities; + REQUIRE(std::abs(e[0].radius - 10.0) < 1e-9); + REQUIRE((e[0].p0 - Vec2d(10,0)).norm() < 1e-6); + REQUIRE((e[0].p1 - Vec2d(0,10)).norm() < 1e-6); + } +} + +// PointOnLine (Fase: persistent positioning). A point-like entity is held on a +// line (value 0) or at perpendicular distance `value`. Drives e.g. a circle centre +// onto a construction axis and, being a real constraint, keeps it there on re-solve. +TEST_CASE("entity constraints: point-on-line positions a centre onto an axis", "[CadDocument]") +{ + using R = SketchPointRole; + using T = SketchConstraintType; + + SECTION("circle centre snaps onto a pinned axis (value 0) and persists") { + CadDocument doc; + std::vector ents = { + {SketchEntity::Type::Line, Vec2d(0,0), Vec2d(10,0)}, // axis (X) + {SketchEntity::Type::Circle, Vec2d(5,7), Vec2d(5,7), Vec2d(5,7), 3.0}, // off-axis centre + }; + std::vector cons; + cons.push_back({T::Fix, 0, -1, R::P0, R::P0, 0.0}); // pin axis endpoints + cons.push_back({T::Fix, 0, -1, R::P1, R::P1, 0.0}); + cons.push_back({T::PointOnLine, 1, 0, R::Center, R::P0, 0.0}); // centre onto axis + int sk = doc.add_sketch_entities(ents, SketchPlane::XY(), "S", cons); + REQUIRE(doc.solve_sketch_feature(sk)); + REQUIRE(std::abs(doc.features[sk].entities[1].center.y()) < 1e-6); // on the axis + // Re-solving keeps it on the axis (a driving constraint, not a one-shot move). + REQUIRE(doc.solve_sketch_feature(sk)); + REQUIRE(std::abs(doc.features[sk].entities[1].center.y()) < 1e-6); + } + + SECTION("non-zero perpendicular distance via the free solve_sketch_entities") { + std::vector ents = { + {SketchEntity::Type::Line, Vec2d(0,0), Vec2d(10,0)}, + {SketchEntity::Type::Point, Vec2d(4,9)}, // point above the axis + }; + std::vector cons; + cons.push_back({T::Fix, 0, -1, R::P0, R::P0, 0.0}); + cons.push_back({T::Fix, 0, -1, R::P1, R::P1, 0.0}); + cons.push_back({T::PointOnLine, 1, 0, R::P0, R::P0, 2.0}); // hold at distance 2 + REQUIRE(solve_sketch_entities(ents, cons)); + REQUIRE(std::abs(std::abs(ents[1].p0.y()) - 2.0) < 1e-6); // 2 mm off the axis + } +} + +TEST_CASE("entity constraints: tangent/midpoint/symmetric/angle", "[CadDocument]") +{ + using R = SketchPointRole; + using T = SketchConstraintType; + + auto dir = [](const SketchEntity& e) { return Vec2d(e.p1 - e.p0); }; + + SECTION("angle 90 between two lines") { + CadDocument doc; + std::vector ents = { + {SketchEntity::Type::Line, Vec2d(0,0), Vec2d(10,0)}, // line0 + {SketchEntity::Type::Line, Vec2d(0,0), Vec2d(5,5)}, // line1 + }; + int sk = doc.add_sketch_entities(ents, SketchPlane::XY(), "S"); + auto& ec = doc.features[sk].entity_constraints; + ec.push_back({T::Fix, 0,-1, R::P0,R::P0, 0.0, -1,R::P0}); + ec.push_back({T::Fix, 0,-1, R::P1,R::P0, 0.0, -1,R::P0}); + ec.push_back({T::Fix, 1,-1, R::P0,R::P0, 0.0, -1,R::P0}); + ec.push_back({T::Angle,0, 1, R::P0,R::P0, M_PI/2,-1,R::P0}); + + REQUIRE(doc.solve_sketch_feature(sk)); + const auto& e = doc.features[sk].entities; + const Vec2d d0 = dir(e[0]).normalized(); + const Vec2d d1 = dir(e[1]).normalized(); + REQUIRE(std::abs(d0.dot(d1)) < 1e-3); + } + + SECTION("midpoint of a line") { + CadDocument doc; + std::vector ents = { + {SketchEntity::Type::Line, Vec2d(0,0), Vec2d(10,0)}, // line0 + {SketchEntity::Type::Point, Vec2d(3,9)}, // point p + }; + int sk = doc.add_sketch_entities(ents, SketchPlane::XY(), "S"); + auto& ec = doc.features[sk].entity_constraints; + ec.push_back({T::Fix, 0,-1, R::P0,R::P0, 0.0,-1,R::P0}); + ec.push_back({T::Fix, 0,-1, R::P1,R::P0, 0.0,-1,R::P0}); + ec.push_back({T::Midpoint,1, 0, R::P0,R::P0, 0.0,-1,R::P0}); + + REQUIRE(doc.solve_sketch_feature(sk)); + const auto& e = doc.features[sk].entities; + REQUIRE((e[1].p0 - Vec2d(5,0)).norm() < 1e-3); + } + + SECTION("tangent line to circle") { + CadDocument doc; + std::vector ents = { + {SketchEntity::Type::Circle, Vec2d(0,0), Vec2d(0,0), Vec2d(0,0), 5.0}, // circle0 r=5 + {SketchEntity::Type::Line, Vec2d(-10,8), Vec2d(10,8)}, // line1 y=8 + }; + int sk = doc.add_sketch_entities(ents, SketchPlane::XY(), "S"); + auto& ec = doc.features[sk].entity_constraints; + ec.push_back({T::Fix, 0,-1, R::Center,R::Center, 0.0,-1,R::P0}); + ec.push_back({T::LockX, 1,-1, R::P0, R::P0, -10.0,-1,R::P0}); + ec.push_back({T::LockX, 1,-1, R::P1, R::P0, 10.0,-1,R::P0}); + ec.push_back({T::Horizontal, 1, 1, R::P0, R::P1, 0.0,-1,R::P0}); + ec.push_back({T::Tangent, 0, 1, R::Center,R::P0, 0.0,-1,R::P0}); + + REQUIRE(doc.solve_sketch_feature(sk)); + const auto& e = doc.features[sk].entities; + REQUIRE(std::abs(std::abs(e[1].p0.y()) - 5.0) < 1e-3); + } + + SECTION("symmetric across a line") { + CadDocument doc; + std::vector ents = { + {SketchEntity::Type::Point, Vec2d(2,3)}, // pointA + {SketchEntity::Type::Point, Vec2d(-1,1)}, // pointB + {SketchEntity::Type::Line, Vec2d(0,0), Vec2d(0,10)}// axis (Y axis) + }; + int sk = doc.add_sketch_entities(ents, SketchPlane::XY(), "S"); + auto& ec = doc.features[sk].entity_constraints; + ec.push_back({T::Fix, 0,-1, R::P0,R::P0, 0.0,-1,R::P0}); + ec.push_back({T::Fix, 2,-1, R::P0,R::P0, 0.0,-1,R::P0}); + ec.push_back({T::Fix, 2,-1, R::P1,R::P0, 0.0,-1,R::P0}); + ec.push_back({T::Symmetric, 0, 1, R::P0,R::P0, 0.0, 2,R::P0}); + + REQUIRE(doc.solve_sketch_feature(sk)); + const auto& e = doc.features[sk].entities; + REQUIRE((e[1].p0 - Vec2d(-2,3)).norm() < 1e-3); + } +} + +TEST_CASE("imported text glyphs all extrude without failing (charset sweep)", "[CadDocument]") +{ + std::string font = resources_dir().empty() + ? std::string("resources/fonts/HarmonyOS_Sans_SC_Regular.ttf") + : resources_dir() + "/fonts/HarmonyOS_Sans_SC_Regular.ttf"; + { + std::ifstream probe(font); + if (!probe.good()) { + SUCCEED("bundled font not reachable in this environment; covered live on :10"); + return; + } + } + + const std::string charset = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "@#$%&*()[]{}<>?/+-=.,;:!" + "\xC3\xA0\xC3\xA8\xC3\xA9\xC3\xAC\xC3\xB2\xC3\xB9"; // à è é ì ò ù (UTF-8) + + std::string failures; + for (char c : charset) { + ImportRegions regs = text_to_regions(std::string(1, c), 12.0, font); + if (regs.empty()) continue; + CadDocument doc; + CadFeature sk; + sk.type = CadFeatureType::Sketch; + sk.plane = SketchPlane::XY(); + sk.imported_regions = regs; + doc.features.push_back(sk); + CadFeature ex; + ex.type = CadFeatureType::Extrude; + ex.sketch_ref = 0; + ex.distance = 3; + doc.features.push_back(ex); + if (!doc.recompute() || !doc.error.empty()) + failures += c; + } + INFO("glyphs that failed to extrude: [" << failures << "]"); + CHECK(failures.empty()); + + // A realistic multi-glyph word must extrude too. + { + ImportRegions regs = text_to_regions("Snapmaker", 12.0, font); + REQUIRE_FALSE(regs.empty()); + CadDocument doc; + CadFeature sk; + sk.type = CadFeatureType::Sketch; + sk.plane = SketchPlane::XY(); + sk.imported_regions = regs; + doc.features.push_back(sk); + CadFeature ex; + ex.type = CadFeatureType::Extrude; + ex.sketch_ref = 0; + ex.distance = 3; + doc.features.push_back(ex); + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE(doc.display_mesh.facets_count() > 0); + } +} + +TEST_CASE("imported regions: faces-with-holes extrude (Text/SVG carrier)", "[CadDocument]") +{ + SECTION("square with a square hole -> tube volume") { + CadDocument doc; + + CadFeature sk; + sk.type = CadFeatureType::Sketch; + sk.name = "art"; + sk.plane = SketchPlane::XY(); + // One region: outer 20x20 (CCW) + inner 8x8 hole (CW). + sk.imported_regions = {{ + {Vec2d(-10,-10), Vec2d(10,-10), Vec2d(10,10), Vec2d(-10,10)}, // outer + {Vec2d(-4,-4), Vec2d(-4,4), Vec2d(4,4), Vec2d(4,-4)}, // hole (reversed winding) + }}; + doc.features.push_back(sk); + + CadFeature ex; + ex.type = CadFeatureType::Extrude; + ex.name = "extrude"; + ex.sketch_ref = 0; + ex.distance = 5; + ex.mode = BooleanMode::New; + doc.features.push_back(ex); + + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE(doc.display_mesh.facets_count() > 0); + + // (20*20 - 8*8) * 5 = 1680 mm^3 + REQUIRE_THAT(double(doc.display_mesh.volume()), Catch::Matchers::WithinRel(1680.0, 0.02)); + + auto sz = doc.display_mesh.bounding_box().size(); + REQUIRE(std::abs(sz.x() - 20.0) < 0.5); + REQUIRE(std::abs(sz.y() - 20.0) < 0.5); + REQUIRE(std::abs(sz.z() - 5.0) < 0.5); + } + + SECTION("inverted winding (CW outer, CCW hole) keeps body solid, counter empty") { + // Real glyph contours (P, e, o, 8) arrive with outer CW + hole CCW. The + // extrude must NORMALISE winding so the letter BODY is solid and the + // counter is the hole — not the inverse (the reported bug). 20x20 outer + // CW with an 8x8 hole CCW -> volume (400-64)*5 = 1680, NOT the inverse. + CadDocument doc; + CadFeature sk; + sk.type = CadFeatureType::Sketch; + sk.plane = SketchPlane::XY(); + sk.imported_regions = {{ + {Vec2d(-10,-10), Vec2d(-10,10), Vec2d(10,10), Vec2d(10,-10)}, // outer CW + {Vec2d(-4,-4), Vec2d(4,-4), Vec2d(4,4), Vec2d(-4,4)}, // hole CCW + }}; + doc.features.push_back(sk); + + CadFeature ex; + ex.type = CadFeatureType::Extrude; + ex.sketch_ref = 0; + ex.distance = 5; + doc.features.push_back(ex); + + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE_THAT(double(doc.display_mesh.volume()), Catch::Matchers::WithinRel(1680.0, 0.02)); + } + + SECTION("degenerate / duplicate points are sanitised, not fatal") { + // FreeType/SVG flattening can emit repeated points; the extrude must + // survive them (previously to_occt_wire threw and failed the whole op). + CadDocument doc; + CadFeature sk; + sk.type = CadFeatureType::Sketch; + sk.plane = SketchPlane::XY(); + sk.imported_regions = {{ + // 20x20 outer square with consecutive dups + an explicit closing dup + {Vec2d(-10,-10), Vec2d(-10,-10), Vec2d(10,-10), Vec2d(10,-10), + Vec2d(10,10), Vec2d(-10,10), Vec2d(-10,-10)}, + }}; + doc.features.push_back(sk); + + CadFeature ex; + ex.type = CadFeatureType::Extrude; + ex.sketch_ref = 0; + ex.distance = 5; + doc.features.push_back(ex); + + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE_THAT(double(doc.display_mesh.volume()), Catch::Matchers::WithinRel(2000.0, 0.02)); + } + + SECTION("a degenerate region is skipped, valid ones still extrude") { + CadDocument doc; + CadFeature sk; + sk.type = CadFeatureType::Sketch; + sk.plane = SketchPlane::XY(); + sk.imported_regions = { + { {Vec2d(0,0), Vec2d(0,0), Vec2d(0,0)} }, // collapses to nothing + { {Vec2d(0,0), Vec2d(5,0), Vec2d(5,5), Vec2d(0,5)} }, // valid 5x5 + }; + doc.features.push_back(sk); + + CadFeature ex; + ex.type = CadFeatureType::Extrude; + ex.sketch_ref = 0; + ex.distance = 4; + doc.features.push_back(ex); + + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE_THAT(double(doc.display_mesh.volume()), Catch::Matchers::WithinRel(100.0, 0.02)); + } + + SECTION("two disjoint regions form one shape") { + CadDocument doc; + + CadFeature sk; + sk.type = CadFeatureType::Sketch; + sk.plane = SketchPlane::XY(); + sk.imported_regions = { + {{Vec2d(0,0), Vec2d(5,0), Vec2d(5,5), Vec2d(0,5)}}, + {{Vec2d(10,0), Vec2d(15,0), Vec2d(15,5), Vec2d(10,5)}}, + }; + doc.features.push_back(sk); + + CadFeature ex; + ex.type = CadFeatureType::Extrude; + ex.sketch_ref = 0; + ex.distance = 3; + doc.features.push_back(ex); + + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + // 2 * (5*5*3) = 150 mm^3 + REQUIRE_THAT(double(doc.display_mesh.volume()), Catch::Matchers::WithinRel(150.0, 0.02)); + } +} + +TEST_CASE("tessellate tracks per-triangle face id", "[CadDocument]") +{ + TopoDS_Shape box = BRepPrimAPI_MakeBox(10., 10., 10.).Shape(); + std::vector tf; + TriangleMesh m = SketchEngine::tessellate(box, tf); + + REQUIRE(tf.size() == m.its.indices.size()); + REQUIRE(!tf.empty()); + + std::set distinct(tf.begin(), tf.end()); + REQUIRE(distinct.size() == 6); + REQUIRE(*distinct.begin() == 0); + REQUIRE(*distinct.rbegin() == 5); + + for (int fid : distinct) { + int count = 0; + for (int x : tf) if (x == fid) ++count; + REQUIRE(count >= 2); + } + + REQUIRE(m.its.indices.size() >= 12); +} + +TEST_CASE("extrude two-sided + through-all + intersect", "[CadDocument]") +{ + using namespace Slic3r; + SketchPlane xy = SketchPlane::XY(); + // a 10x10 square wire centred on origin + SketchProfile sp; + sp.points = { Vec2d(-5,-5), Vec2d(5,-5), Vec2d(5,5), Vec2d(-5,5) }; + sp.closed = true; + TopoDS_Wire w = sp.to_occt_wire(xy); + + SECTION("two-sided height = up+down") { + TopoDS_Shape s = SketchEngine::make_extrude_two_sided(w, xy, 10.0, 4.0); + REQUIRE_FALSE(s.IsNull()); + Bnd_Box bb; BRepBndLib::Add(s, bb); + double xmin,ymin,zmin,xmax,ymax,zmax; bb.Get(xmin,ymin,zmin,xmax,ymax,zmax); + REQUIRE_THAT(zmax - zmin, Catch::Matchers::WithinAbs(14.0, 0.05)); // 10 up + 4 down + REQUIRE_THAT(zmax, Catch::Matchers::WithinAbs(10.0, 0.05)); + REQUIRE_THAT(zmin, Catch::Matchers::WithinAbs(-4.0, 0.05)); + } +} + +TEST_CASE("extrude taper + up-to-face distance", "[CadDocument]") +{ + using namespace Slic3r; + SketchPlane xy = SketchPlane::XY(); + SketchProfile sp; sp.points = { Vec2d(-5,-5),Vec2d(5,-5),Vec2d(5,5),Vec2d(-5,5) }; sp.closed = true; + TopoDS_Wire w = sp.to_occt_wire(xy); + + SECTION("taper widens the top") { + TopoDS_Shape s = SketchEngine::make_extrude_taper(w, xy, 10.0, 15.0); + REQUIRE_FALSE(s.IsNull()); + Bnd_Box bb; BRepBndLib::Add(s, bb); + double x0,y0,z0,x1,y1,z1; bb.Get(x0,y0,z0,x1,y1,z1); + REQUIRE_THAT(z1 - z0, Catch::Matchers::WithinAbs(10.0, 0.1)); + REQUIRE((x1 - x0) > 12.0); + } + SECTION("extreme taper falls back to a straight prism") { + TopoDS_Shape s = SketchEngine::make_extrude_taper(w, xy, 10.0, 89.0); + REQUIRE_FALSE(s.IsNull()); + Bnd_Box bb; BRepBndLib::Add(s, bb); + double x0,y0,z0,x1,y1,z1; bb.Get(x0,y0,z0,x1,y1,z1); + REQUIRE_THAT(x1 - x0, Catch::Matchers::WithinAbs(10.0, 0.1)); + } +} + +TEST_CASE("internal thread cuts a visible groove into the bore wall", "[CadDocument]") +{ + using namespace Slic3r; + SketchPlane xy = SketchPlane::XY(); + + // 40x40x20 box centred on the origin, extruded +Z. + auto make_box = [&](CadDocument& doc) { + CadFeature sk; + sk.type = CadFeatureType::Sketch; + sk.plane = xy; + sk.imported_regions = {{ + {Vec2d(-20,-20), Vec2d(20,-20), Vec2d(20,20), Vec2d(-20,20)}, + }}; + doc.features.push_back(sk); + CadFeature ex; + ex.type = CadFeatureType::Extrude; + ex.sketch_ref = 0; + ex.distance = 20; + doc.features.push_back(ex); + }; + + // Reference: box with a plain Ø12 bore through it. + CadDocument hole_doc; + make_box(hole_doc); + hole_doc.add_hole(12.0, 20.0, true, 0.0, 0.0, xy, "Hole"); + REQUIRE(hole_doc.recompute()); + REQUIRE(hole_doc.error.empty()); + const double v_hole = double(hole_doc.display_mesh.volume()); + + // Threaded: same box, internal thread of radius 6 (the bore radius). + CadDocument thr_doc; + make_box(thr_doc); + thr_doc.add_thread(6.0, 3.0, 20.0, 1.0, /*internal=*/true, 0.0, 0.0, xy, "Thread"); + REQUIRE(thr_doc.recompute()); + REQUIRE(thr_doc.error.empty()); + const double v_thread = double(thr_doc.display_mesh.volume()); + + // The helical groove must carve material OUT of the wall, beyond the plain + // bore -> a visible internal thread. The old inward-pointing profile only + // swept already-empty bore space and removed essentially nothing, so it would + // give v_thread ~= v_hole; the fixed profile removes a meaningful volume. + REQUIRE(v_thread > 0.0); + REQUIRE(v_thread < v_hole); + REQUIRE((v_hole - v_thread) > 20.0); +} + +TEST_CASE("revolve builds a solid of revolution about an in-plane axis", "[CadDocument]") +{ + using namespace Slic3r; + SketchPlane xy = SketchPlane::XY(); + + // Rectangle profile (10 wide x 10 tall, area 100) offset to +v so it lies entirely + // on one side of the X axis; revolved 360deg about X -> a rectangular-section ring. + // Pappus: V = 2*pi*R*A = 2*pi*15*100 = ~9424.78 mm^3 (tessellation slightly under). + auto make_rev_doc = [&](double angle, int axis, double u0, double v0) { + auto doc = std::make_unique(); + CadFeature sk; + sk.type = CadFeatureType::Sketch; + sk.plane = xy; + sk.profile.points = { Vec2d(u0 - 5, v0 - 5), Vec2d(u0 + 5, v0 - 5), + Vec2d(u0 + 5, v0 + 5), Vec2d(u0 - 5, v0 + 5) }; + sk.profile.closed = true; + doc->features.push_back(sk); + doc->add_revolve(0, angle, axis, false, BooleanMode::New, "Rev"); + return doc; + }; + + auto full = make_rev_doc(360.0, /*axis=X*/0, 0.0, 15.0); + REQUIRE(full->recompute()); + REQUIRE(full->error.empty()); + const double v_full = double(full->display_mesh.volume()); + REQUIRE(v_full > 0.0); + REQUIRE(v_full == Approx(9424.78).epsilon(0.05)); + + // A 180deg sweep removes exactly half the material. + auto half = make_rev_doc(180.0, 0, 0.0, 15.0); + REQUIRE(half->recompute()); + REQUIRE(half->error.empty()); + const double v_half = double(half->display_mesh.volume()); + REQUIRE(v_half > 0.0); + REQUIRE(v_full == Approx(2.0 * v_half).epsilon(0.05)); + + // Axis = plane Y: profile offset to +u (one side of the Y axis) gives the same ring. + auto ydoc = make_rev_doc(360.0, /*axis=Y*/1, 15.0, 0.0); + REQUIRE(ydoc->recompute()); + REQUIRE(ydoc->error.empty()); + REQUIRE(double(ydoc->display_mesh.volume()) == Approx(9424.78).epsilon(0.05)); +} + +TEST_CASE("sweep builds a solid by sweeping a profile along a path", "[CadDocument]") +{ + using namespace Slic3r; + CadDocument doc; + + // Profile: circle r=5 on the XY plane at the origin (area = 25*pi). + SketchEntity circ; + circ.type = SketchEntity::Type::Circle; + circ.center = Vec2d(0, 0); + circ.radius = 5.0; + const int prof = doc.add_sketch_entities({circ}, SketchPlane::XY(), "Profile"); + + // Path: a straight line on the XZ plane from 2D (0,0)->(0,100), i.e. world + // (0,0,0)->(0,0,100): the spine starts on the profile plane and runs +Z by 100. + SketchEntity line; + line.type = SketchEntity::Type::Line; + line.p0 = Vec2d(0, 0); + line.p1 = Vec2d(0, 100); + const int path = doc.add_sketch_entities({line}, SketchPlane::XZ(), "Path"); + + doc.add_sweep(prof, path, BooleanMode::New, "Sweep1"); + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + + // Straight sweep of a circle == a cylinder: V = pi*r^2*h = pi*25*100 = ~7853.98. + const double v = double(doc.display_mesh.volume()); + REQUIRE(v > 0.0); + REQUIRE_THAT(v, Catch::Matchers::WithinRel(M_PI * 25.0 * 100.0, 0.03)); + + // A valid path sketch is mandatory: a -1 path ref must error cleanly, not crash. + CadDocument bad; + const int p2 = bad.add_sketch_entities({circ}, SketchPlane::XY(), "Profile"); + bad.add_sweep(p2, -1, BooleanMode::New, "BadSweep"); + REQUIRE_FALSE(bad.recompute()); +} + +TEST_CASE("pattern replicates a body linearly and circularly", "[CadDocument]") +{ + using namespace Slic3r; + + // Linear: a 10x10x10 box (V=1000) repeated 3x at 20mm spacing along plane X. + // 20 > 10 so the copies are disjoint -> total V = 3*1000 = 3000. + { + CadDocument doc; + int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), + 10, 10, 5, "Box"); + doc.add_extrude(sk, 10.0, false, BooleanMode::New, "E"); + doc.add_pattern(/*circular=*/false, /*count=*/3, /*spacing=*/20, + /*dir=*/0, /*angle=*/0, /*target=*/-1, "LinearPattern"); + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + const double v = double(doc.display_mesh.volume()); + REQUIRE_THAT(v, Catch::Matchers::WithinRel(3000.0, 0.02)); + } + + // Circular: a 10x10x10 box centred at x=50 (radius 50 from the Z axis), 4 copies + // over 360deg about the plane normal through the origin -> a ring of 4 disjoint + // boxes -> V = 4*1000 = 4000. + { + CadDocument doc; + SketchProfile sp; + sp.points.push_back(Vec2d(45, -5)); + sp.points.push_back(Vec2d(55, -5)); + sp.points.push_back(Vec2d(55, 5)); + sp.points.push_back(Vec2d(45, 5)); + sp.closed = true; + int sk = doc.add_sketch_profile(sp, SketchPlane::XY(), "OffsetBox"); + doc.add_extrude(sk, 10.0, false, BooleanMode::New, "E"); + doc.add_pattern(/*circular=*/true, /*count=*/4, /*spacing=*/0, + /*dir=*/0, /*angle=*/360, /*target=*/-1, "CircularPattern"); + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + const double v = double(doc.display_mesh.volume()); + REQUIRE_THAT(v, Catch::Matchers::WithinRel(4000.0, 0.02)); + } + + // A pattern with no body must error cleanly, not crash. + { + CadDocument bad; + bad.add_pattern(false, 3, 20, 0, 0, -1, "NoBody"); + REQUIRE_FALSE(bad.recompute()); + } +} + +TEST_CASE("thread standards table carries correct ISO/UTS measures", "[CadDocument]") +{ + using namespace Slic3r; + + // Table is non-empty and every entry is self-consistent. + const auto& table = thread_standards(); + REQUIRE(table.size() > 40); + for (const ThreadSpec& s : table) { + REQUIRE(s.major_diameter_mm > 0.0); + REQUIRE(s.pitch_mm > 0.0); + REQUIRE(s.minor_diameter_mm() < s.major_diameter_mm); + REQUIRE(s.minor_diameter_mm() > 0.0); + // 60deg V cut depth = 0.6134 * pitch. + REQUIRE(s.thread_depth_mm() == Approx(0.6134 * s.pitch_mm)); + } + + // ISO metric coarse: known nominal/pitch pairs. + const ThreadSpec* m6 = find_thread_standard("M6"); + REQUIRE(m6 != nullptr); + REQUIRE(m6->major_diameter_mm == Approx(6.0)); + REQUIRE(m6->pitch_mm == Approx(1.0)); + REQUIRE(m6->series == ThreadSpec::Series::MetricCoarse); + REQUIRE_FALSE(m6->imperial()); + REQUIRE(m6->thread_depth_mm() == Approx(0.6134)); + // Tapped minor (tap-drill) diameter D - 1.0825*P = 6 - 1.0825 = 4.9175. + REQUIRE(m6->minor_diameter_mm() == Approx(4.9175)); + + const ThreadSpec* m3 = find_thread_standard("M3"); + REQUIRE(m3 != nullptr); + REQUIRE(m3->pitch_mm == Approx(0.5)); + + // Imperial UNC: 1/4-20 -> 0.25in major, pitch = 25.4/20 = 1.27 mm. + const ThreadSpec* q = find_thread_standard("1/4-20 UNC"); + REQUIRE(q != nullptr); + REQUIRE(q->major_diameter_mm == Approx(6.35)); + REQUIRE(q->pitch_mm == Approx(1.27)); + REQUIRE(q->series == ThreadSpec::Series::UNC); + REQUIRE(q->imperial()); + + // Imperial UNF fine variant has a finer pitch than its UNC sibling. + const ThreadSpec* qf = find_thread_standard("1/4-28 UNF"); + REQUIRE(qf != nullptr); + REQUIRE(qf->major_diameter_mm == Approx(6.35)); + REQUIRE(qf->pitch_mm == Approx(25.4 / 28.0)); + REQUIRE(qf->pitch_mm < q->pitch_mm); + + // Unknown designation -> nullptr. + REQUIRE(find_thread_standard("M7.3 bogus") == nullptr); +} + +TEST_CASE("datum plane: offset + tilt resolution and sketching on it", "[CadDocument]") +{ + using Catch::Matchers::WithinRel; + using Catch::Matchers::WithinAbs; + + // Parallel offset plane 30 mm above XY (normal +Z, origin at z=30). + CadDocument doc; + int p0 = doc.add_plane(0 /*XY*/, 30.0, 0.0, 0, "Plane1"); + REQUIRE(p0 == 0); + + auto planes = doc.resolve_datum_planes(); + REQUIRE(planes.size() == 1); + REQUIRE(planes[0].first == "Plane1"); + CHECK_THAT(planes[0].second.origin.z(), WithinAbs(30.0, 1e-9)); + CHECK_THAT(planes[0].second.normal.z(), WithinAbs(1.0, 1e-9)); + + // A second datum plane tilted 90 deg about the base (XY) X axis: its normal + // rotates from +Z toward -Y (Rodrigues about +X: +Z -> -Y). + doc.add_plane(0 /*XY*/, 0.0, 90.0, 0 /*about X*/, "Plane2"); + planes = doc.resolve_datum_planes(); + REQUIRE(planes.size() == 2); + CHECK_THAT(planes[1].second.normal.y(), WithinAbs(-1.0, 1e-9)); + CHECK_THAT(planes[1].second.normal.z(), WithinAbs(0.0, 1e-9)); + + // Sketch a 10x10 square ON Plane1 and extrude 4 mm: the solid must sit in z=[30,34]. + SketchPlane sp = planes[0].second; + SketchProfile prof; + prof.points = {{-5,-5},{5,-5},{5,5},{-5,5}}; + prof.closed = true; + int sk = doc.add_sketch_profile(prof, sp, "S"); + doc.add_extrude(sk, 4.0, false, BooleanMode::New, "E"); + REQUIRE(doc.recompute()); + + Bnd_Box bb; + BRepBndLib::Add(doc.body, bb); + double xmin,ymin,zmin,xmax,ymax,zmax; + bb.Get(xmin,ymin,zmin,xmax,ymax,zmax); + CHECK_THAT(zmin, WithinAbs(30.0, 1e-6)); + CHECK_THAT(zmax, WithinAbs(34.0, 1e-6)); + CHECK_THAT(double(doc.display_mesh.volume()), WithinRel(400.0, 0.02)); + + // A datum-plane-only document has no solid -> recompute is a benign failure. + CadDocument only_plane; + only_plane.add_plane(0, 10.0, 0.0, 0, "P"); + REQUIRE_FALSE(only_plane.recompute()); +} + +TEST_CASE("loft builds a solid skinning two profiles on parallel planes", "[CadDocument]") +{ + using Catch::Matchers::WithinRel; + using Catch::Matchers::WithinAbs; + + CadDocument doc; + // Bottom 20x20 square on XY. + SketchProfile bot; + bot.points = {{-10,-10},{10,-10},{10,10},{-10,10}}; + bot.closed = true; + int s0 = doc.add_sketch_profile(bot, SketchPlane::XY(), "Bottom"); + + // Top 10x10 square on a datum plane 20 mm above XY (exercises datum -> loft). + doc.add_plane(0 /*XY*/, 20.0, 0.0, 0, "Plane1"); + SketchPlane top = doc.resolve_datum_planes()[0].second; + SketchProfile tp; + tp.points = {{-5,-5},{5,-5},{5,5},{-5,5}}; + tp.closed = true; + int s1 = doc.add_sketch_profile(tp, top, "Top"); + + // Ruled (straight) sections -> exact planar end caps at z=0 and z=20. + doc.add_loft({s0, s1}, true /*ruled*/, BooleanMode::New, "Loft1"); + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + + // The loft spans z=[0,20]. ThruSections approximates each section as a BSpline + // curve, so the lateral surface bulges ~0.05 mm past the end planes (sub-visual, + // ~0.25%) — assert the span loosely; the volume below is the real correctness gate. + Bnd_Box bb; + BRepBndLib::Add(doc.body, bb); + double xmin,ymin,zmin,xmax,ymax,zmax; + bb.Get(xmin,ymin,zmin,xmax,ymax,zmax); + CHECK_THAT(zmin, WithinAbs(0.0, 0.1)); + CHECK_THAT(zmax, WithinAbs(20.0, 0.1)); + // Square frustum volume = h/3*(A1+A2+sqrt(A1*A2)) = 20/3*(400+100+200) = 4666.67. + CHECK_THAT(double(doc.display_mesh.volume()), WithinRel(4666.67, 0.03)); + + // A single profile is not enough -> benign recompute failure. + CadDocument one; + SketchProfile sp; sp.points = {{-5,-5},{5,-5},{5,5},{-5,5}}; sp.closed = true; + int only = one.add_sketch_profile(sp, SketchPlane::XY(), "Only"); + one.add_loft({only}, false, BooleanMode::New, "L"); + REQUIRE_FALSE(one.recompute()); +} + +TEST_CASE("draft tapers a solid face about the body base", "[CadDocument]") +{ + using namespace Slic3r; + + // 10x10x10 box from z=0..10 (V=1000). Drafting a vertical side face by +10deg about + // the bottom (neutral) plane tilts its top edge inward, removing material so V<1000. + // A box's 2 horizontal faces are parallel to the neutral plane and cannot be drafted + // (Add fails -> recompute returns false), so exactly the 4 vertical sides succeed. + int ok_faces = 0; + bool saw_taper = false; + for (int fid = 0; fid < 6; ++fid) { + CadDocument doc; + int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 10, 10, 5, "Box"); + doc.add_extrude(sk, 10.0, false, BooleanMode::New, "E"); + doc.add_draft(10.0, fid, -1, "Draft1"); + if (!doc.recompute()) continue; // top/bottom faces: parallel to base -> benign skip + ++ok_faces; + const double v = double(doc.display_mesh.volume()); + REQUIRE(v > 0.0); + if (v < 999.0) saw_taper = true; + } + REQUIRE(ok_faces == 4); + REQUIRE(saw_taper); + + // Draft with no body must fail cleanly, not crash. + CadDocument bad; + bad.add_draft(5.0, 0, -1, "NoBody"); + REQUIRE_FALSE(bad.recompute()); +} + +TEST_CASE("cut splits a body with a plane", "[cut]") +{ + using Catch::Matchers::WithinRel; + + auto make_box = [](CadDocument& doc, double w, double h, double d) { + CadFeature sk; + sk.type = CadFeatureType::Sketch; + sk.plane = SketchPlane::XY(); + sk.imported_regions = {{ + {Vec2d(-w / 2, -h / 2), Vec2d(w / 2, -h / 2), + Vec2d(w / 2, h / 2), Vec2d(-w / 2, h / 2)}, + }}; + doc.features.push_back(sk); + CadFeature ex; + ex.type = CadFeatureType::Extrude; + ex.sketch_ref = 0; + ex.distance = d; + doc.features.push_back(ex); + }; + + SECTION("keep upper half only") { + CadDocument doc; + make_box(doc, 20.0, 20.0, 20.0); + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + const double v_orig = double(doc.display_mesh.volume()); + const int n_before = int(doc.bodies.size()); + REQUIRE(v_orig > 0.0); + + CadFeature cut; + cut.type = CadFeatureType::Cut; + cut.plane = SketchPlane::XY(); + cut.cut_offset = 10.0; // mid-height of the 0..20 box + cut.cut_keep_upper = true; + cut.cut_keep_lower = false; + doc.features.push_back(cut); + + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE(int(doc.bodies.size()) == n_before); + REQUIRE_THAT(double(doc.display_mesh.volume()), WithinRel(v_orig * 0.5, 0.01)); + } + + SECTION("keep both halves splits into two bodies") { + CadDocument doc; + make_box(doc, 20.0, 20.0, 20.0); + REQUIRE(doc.recompute()); + const double v_orig = double(doc.display_mesh.volume()); + const int n_before = int(doc.bodies.size()); + REQUIRE(v_orig > 0.0); + + CadFeature cut; + cut.type = CadFeatureType::Cut; + cut.plane = SketchPlane::XY(); + cut.cut_offset = 10.0; + cut.cut_keep_upper = true; + cut.cut_keep_lower = true; + doc.features.push_back(cut); + + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE(int(doc.bodies.size()) == n_before + 1); + REQUIRE_THAT(double(doc.display_mesh.volume()), WithinRel(v_orig, 0.01)); + + for (const auto& b : doc.bodies) { + double v = double(SketchEngine::tessellate(b.shape).volume()); + REQUIRE_THAT(v, WithinRel(v_orig * 0.5, 0.01)); + } + } + + SECTION("keep lower half only") { + CadDocument doc; + make_box(doc, 20.0, 20.0, 20.0); + REQUIRE(doc.recompute()); + const double v_orig = double(doc.display_mesh.volume()); + const int n_before = int(doc.bodies.size()); + + CadFeature cut; + cut.type = CadFeatureType::Cut; + cut.plane = SketchPlane::XY(); + cut.cut_offset = 10.0; + cut.cut_keep_upper = false; + cut.cut_keep_lower = true; + doc.features.push_back(cut); + + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE(int(doc.bodies.size()) == n_before); + REQUIRE_THAT(double(doc.display_mesh.volume()), WithinRel(v_orig * 0.5, 0.01)); + } + + SECTION("flip swaps which side is kept") { + CadDocument doc; + make_box(doc, 20.0, 20.0, 20.0); + REQUIRE(doc.recompute()); + const double v_orig = double(doc.display_mesh.volume()); + + // cut with flip=true, keep_upper=true → the -normal side (original bottom half) + CadFeature cut; + cut.type = CadFeatureType::Cut; + cut.plane = SketchPlane::XY(); + cut.cut_offset = 10.0; + cut.cut_flip = true; + cut.cut_keep_upper = true; + cut.cut_keep_lower = false; + doc.features.push_back(cut); + + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE_THAT(double(doc.display_mesh.volume()), WithinRel(v_orig * 0.5, 0.01)); + } + + SECTION("both keep flags false throws") { + CadDocument doc; + make_box(doc, 20.0, 20.0, 20.0); + REQUIRE(doc.recompute()); + + CadFeature cut; + cut.type = CadFeatureType::Cut; + cut.plane = SketchPlane::XY(); + cut.cut_keep_upper = false; + cut.cut_keep_lower = false; + doc.features.push_back(cut); + + REQUIRE_FALSE(doc.recompute()); + } +} diff --git a/tests/libslic3r/test_sketchconstraints.cpp b/tests/libslic3r/test_sketchconstraints.cpp new file mode 100644 index 0000000000..64ffd5fd3b --- /dev/null +++ b/tests/libslic3r/test_sketchconstraints.cpp @@ -0,0 +1,169 @@ +#include +#include "libslic3r/SketchConstraints.hpp" + +using namespace Slic3r; + +TEST_CASE("Coincident with anchor", "[SketchConstraints]") +{ + SketchConstraints sc; + int a = sc.add_point(0, 0); + int b = sc.add_point(5, 5); + sc.fix_point(a); + sc.coincident(a, b); + REQUIRE(sc.solve()); + Vec2d pb = sc.get_point(b); + REQUIRE_THAT(pb.x(), Catch::Matchers::WithinAbs(0.0, 1e-4)); + REQUIRE_THAT(pb.y(), Catch::Matchers::WithinAbs(0.0, 1e-4)); +} + +TEST_CASE("Horizontal + distance", "[SketchConstraints]") +{ + SketchConstraints sc; + int a = sc.add_point(0, 0); + int b = sc.add_point(5, 3); + sc.fix_point(a); + sc.horizontal(a, b); + sc.distance(a, b, 10); + REQUIRE(sc.solve()); + Vec2d pb = sc.get_point(b); + REQUIRE_THAT(pb.y(), Catch::Matchers::WithinAbs(0.0, 1e-3)); + REQUIRE_THAT(std::abs(pb.x()), Catch::Matchers::WithinAbs(10.0, 1e-3)); +} + +TEST_CASE("Rectangle", "[SketchConstraints]") +{ + SketchConstraints sc; + int p0 = sc.add_point(0, 0); + int p1 = sc.add_point(8, 1); + int p2 = sc.add_point(9, 5); + int p3 = sc.add_point(-1, 4); + sc.fix_point(p0); + sc.lock_x(p0, 0); + sc.lock_y(p0, 0); + sc.horizontal(p0, p1); + sc.vertical(p1, p2); + sc.horizontal(p2, p3); + sc.vertical(p3, p0); + sc.distance(p0, p1, 10); + sc.distance(p1, p2, 6); + REQUIRE(sc.solve()); + Vec2d pp1 = sc.get_point(p1); + Vec2d pp2 = sc.get_point(p2); + Vec2d pp3 = sc.get_point(p3); + REQUIRE_THAT(pp1.x(), Catch::Matchers::WithinAbs(10.0, 1e-3)); + REQUIRE_THAT(pp1.y(), Catch::Matchers::WithinAbs(0.0, 1e-3)); + REQUIRE_THAT(pp2.x(), Catch::Matchers::WithinAbs(10.0, 1e-3)); + REQUIRE_THAT(pp2.y(), Catch::Matchers::WithinAbs(6.0, 1e-3)); + REQUIRE_THAT(pp3.x(), Catch::Matchers::WithinAbs(0.0, 1e-3)); + REQUIRE_THAT(pp3.y(), Catch::Matchers::WithinAbs(6.0, 1e-3)); +} + +TEST_CASE("residual_norm after each solve", "[SketchConstraints]") +{ + SECTION("coincident case") + { + SketchConstraints sc; + int a = sc.add_point(0, 0); + int b = sc.add_point(5, 5); + sc.fix_point(a); + sc.coincident(a, b); + REQUIRE(sc.solve()); + REQUIRE(sc.residual_norm() < 1e-5); + } + SECTION("horizontal+distance case") + { + SketchConstraints sc; + int a = sc.add_point(0, 0); + int b = sc.add_point(5, 3); + sc.fix_point(a); + sc.horizontal(a, b); + sc.distance(a, b, 10); + REQUIRE(sc.solve()); + REQUIRE(sc.residual_norm() < 1e-5); + } + SECTION("rectangle case") + { + SketchConstraints sc; + int p0 = sc.add_point(0, 0); + int p1 = sc.add_point(8, 1); + int p2 = sc.add_point(9, 5); + int p3 = sc.add_point(-1, 4); + sc.fix_point(p0); + sc.lock_x(p0, 0); + sc.lock_y(p0, 0); + sc.horizontal(p0, p1); + sc.vertical(p1, p2); + sc.horizontal(p2, p3); + sc.vertical(p3, p0); + sc.distance(p0, p1, 10); + sc.distance(p1, p2, 6); + REQUIRE(sc.solve()); + REQUIRE(sc.residual_norm() < 1e-5); + } +} + +TEST_CASE("midpoint", "[SketchConstraints]") +{ + SketchConstraints sc; + int a = sc.add_point(0, 0); + int b = sc.add_point(10, 0); + int m = sc.add_point(3, 7); + sc.fix_point(a); + sc.fix_point(b); + sc.midpoint(m, a, b); + REQUIRE(sc.solve()); + Vec2d pm = sc.get_point(m); + REQUIRE_THAT(pm.x(), Catch::Matchers::WithinAbs(5.0, 1e-3)); + REQUIRE_THAT(pm.y(), Catch::Matchers::WithinAbs(0.0, 1e-3)); +} + +TEST_CASE("symmetric across Y axis", "[SketchConstraints]") +{ + SketchConstraints sc; + int a = sc.add_point(2, 3); + int b = sc.add_point(-1, 1); + int c = sc.add_point(0, 0); + int d = sc.add_point(0, 1); + sc.fix_point(a); + sc.fix_point(c); + sc.fix_point(d); + sc.symmetric(a, b, c, d); + REQUIRE(sc.solve()); + Vec2d pb = sc.get_point(b); + REQUIRE_THAT(pb.x(), Catch::Matchers::WithinAbs(-2.0, 1e-3)); + REQUIRE_THAT(pb.y(), Catch::Matchers::WithinAbs(3.0, 1e-3)); +} + +TEST_CASE("angle 90 degrees", "[SketchConstraints]") +{ + SketchConstraints sc; + int a = sc.add_point(0, 0); + int b = sc.add_point(1, 0); + int c = sc.add_point(0, 0); + int d = sc.add_point(1, 1); + sc.fix_point(a); + sc.fix_point(b); + sc.fix_point(c); + sc.angle(a, b, c, d, M_PI / 2); + REQUIRE(sc.solve()); + Vec2d pd = sc.get_point(d); + Vec2d pc = sc.get_point(c); + REQUIRE_THAT(pd.x() - pc.x(), Catch::Matchers::WithinAbs(0.0, 1e-3)); + REQUIRE(pd.y() > pc.y()); +} + +TEST_CASE("point-line distance", "[SketchConstraints]") +{ + SketchConstraints sc; + int a = sc.add_point(0, 0); + int b = sc.add_point(10, 0); + int p = sc.add_point(3, 1); + sc.fix_point(a); + sc.fix_point(b); + sc.lock_x(p, 3.0); + sc.point_line_distance(p, a, b, 5.0); + REQUIRE(sc.solve()); + Vec2d pp = sc.get_point(p); + REQUIRE_THAT(std::abs(pp.y()), Catch::Matchers::WithinAbs(5.0, 1e-3)); + REQUIRE_THAT(pp.x(), Catch::Matchers::WithinAbs(3.0, 1e-3)); +} diff --git a/tests/libslic3r/test_sketchedit.cpp b/tests/libslic3r/test_sketchedit.cpp new file mode 100644 index 0000000000..a0193445d5 --- /dev/null +++ b/tests/libslic3r/test_sketchedit.cpp @@ -0,0 +1,488 @@ +#include +#include "libslic3r/SketchEngine.hpp" +#include + +using namespace Slic3r; + +using Catch::Matchers::WithinAbs; + +TEST_CASE("Mirror Line across Y axis", "[SketchEdit]") +{ + SketchEntity e; + e.type = SketchEntity::Type::Line; + e.p0 = Vec2d(3, 2); + e.p1 = Vec2d(5, 4); + + Vec2d a(0, -1); + Vec2d b(0, 1); + + auto result = SketchEngine::mirror_entities({e}, a, b); + REQUIRE(result.size() == 1); + + const auto& m = result[0]; + REQUIRE(m.type == SketchEntity::Type::Line); + REQUIRE_THAT(m.p0.x(), WithinAbs(-3.0, 1e-9)); + REQUIRE_THAT(m.p0.y(), WithinAbs(2.0, 1e-9)); + REQUIRE_THAT(m.p1.x(), WithinAbs(-5.0, 1e-9)); + REQUIRE_THAT(m.p1.y(), WithinAbs(4.0, 1e-9)); +} + +TEST_CASE("Mirror Circle across Y axis", "[SketchEdit]") +{ + SketchEntity e; + e.type = SketchEntity::Type::Circle; + e.center = Vec2d(5, 0); + e.p0 = Vec2d(5, 0); + e.radius = 3; + + Vec2d a(0, -1); + Vec2d b(0, 1); + + auto result = SketchEngine::mirror_entities({e}, a, b); + REQUIRE(result.size() == 1); + + const auto& m = result[0]; + REQUIRE(m.type == SketchEntity::Type::Circle); + REQUIRE_THAT(m.center.x(), WithinAbs(-5.0, 1e-9)); + REQUIRE_THAT(m.center.y(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(m.radius, WithinAbs(3.0, 1e-9)); + REQUIRE_THAT(m.p0.x(), WithinAbs(-5.0, 1e-9)); + REQUIRE_THAT(m.p0.y(), WithinAbs(0.0, 1e-9)); +} + +TEST_CASE("Mirror Arc across X axis", "[SketchEdit]") +{ + SketchEntity e; + e.type = SketchEntity::Type::Arc; + e.center = Vec2d(0, 0); + e.radius = 1.0; + e.start_angle = 0.0; + e.end_angle = M_PI / 2.0; + e.p0 = Vec2d(1, 0); + e.p1 = Vec2d(0, 1); + + Vec2d a(-1, 0); + Vec2d b(1, 0); + + auto result = SketchEngine::mirror_entities({e}, a, b); + REQUIRE(result.size() == 1); + + const auto& m = result[0]; + REQUIRE(m.type == SketchEntity::Type::Arc); + + REQUIRE_THAT(m.p0.x(), WithinAbs(1.0, 1e-9)); + REQUIRE_THAT(m.p0.y(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(m.p1.x(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(m.p1.y(), WithinAbs(-1.0, 1e-9)); + + double sweep = m.end_angle - m.start_angle; + double orig_sweep = e.end_angle - e.start_angle; + REQUIRE(orig_sweep > 0.0); + REQUIRE(sweep < 0.0); +} + +TEST_CASE("Offset Line by positive d", "[SketchEdit]") +{ + SketchEntity e; + e.type = SketchEntity::Type::Line; + e.p0 = Vec2d(0, 0); + e.p1 = Vec2d(10, 0); + + auto result = SketchEngine::offset_entities({e}, 2.0); + REQUIRE(result.size() == 1); + + const auto& o = result[0]; + REQUIRE(o.type == SketchEntity::Type::Line); + REQUIRE_THAT(o.p0.x(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(o.p0.y(), WithinAbs(2.0, 1e-9)); + REQUIRE_THAT(o.p1.x(), WithinAbs(10.0, 1e-9)); + REQUIRE_THAT(o.p1.y(), WithinAbs(2.0, 1e-9)); +} + +TEST_CASE("Offset Circle: expand and collapse", "[SketchEdit]") +{ + SketchEntity e; + e.type = SketchEntity::Type::Circle; + e.center = Vec2d(0, 0); + e.p0 = Vec2d(0, 0); + e.radius = 5; + + auto expanded = SketchEngine::offset_entities({e}, 2.0); + REQUIRE(expanded.size() == 1); + REQUIRE_THAT(expanded[0].radius, WithinAbs(7.0, 1e-9)); + + auto collapsed = SketchEngine::offset_entities({e}, -5.0); + REQUIRE(collapsed.empty()); +} + +TEST_CASE("Offset Arc by positive d", "[SketchEdit]") +{ + SketchEntity e; + e.type = SketchEntity::Type::Arc; + e.center = Vec2d(0, 0); + e.radius = 4.0; + e.start_angle = 0.0; + e.end_angle = M_PI / 2.0; + e.p0 = Vec2d(4, 0); + e.p1 = Vec2d(0, 4); + + auto result = SketchEngine::offset_entities({e}, 1.0); + REQUIRE(result.size() == 1); + + const auto& o = result[0]; + REQUIRE(o.type == SketchEntity::Type::Arc); + REQUIRE_THAT(o.radius, WithinAbs(5.0, 1e-9)); + REQUIRE_THAT(o.p0.x(), WithinAbs(5.0, 1e-9)); + REQUIRE_THAT(o.p0.y(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(o.p1.x(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(o.p1.y(), WithinAbs(5.0, 1e-9)); +} + +TEST_CASE("Fillet right-angle corner", "[SketchEdit]") +{ + SketchEntity a; + a.type = SketchEntity::Type::Line; + a.p0 = Vec2d(0, 0); + a.p1 = Vec2d(10, 0); + + SketchEntity b; + b.type = SketchEntity::Type::Line; + b.p0 = Vec2d(10, 0); + b.p1 = Vec2d(10, 10); + + SketchEntity a_out, b_out, arc_out; + bool ok = SketchEngine::fillet_lines(a, b, 2.0, a_out, b_out, arc_out); + REQUIRE(ok); + + REQUIRE_THAT(a_out.p0.x(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(a_out.p0.y(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(a_out.p1.x(), WithinAbs(8.0, 1e-9)); + REQUIRE_THAT(a_out.p1.y(), WithinAbs(0.0, 1e-9)); + + REQUIRE_THAT(b_out.p0.x(), WithinAbs(10.0, 1e-9)); + REQUIRE_THAT(b_out.p0.y(), WithinAbs(2.0, 1e-9)); + REQUIRE_THAT(b_out.p1.x(), WithinAbs(10.0, 1e-9)); + REQUIRE_THAT(b_out.p1.y(), WithinAbs(10.0, 1e-9)); + + REQUIRE(arc_out.type == SketchEntity::Type::Arc); + REQUIRE_THAT(arc_out.radius, WithinAbs(2.0, 1e-9)); + REQUIRE_THAT(arc_out.center.x(), WithinAbs(8.0, 1e-9)); + REQUIRE_THAT(arc_out.center.y(), WithinAbs(2.0, 1e-9)); + REQUIRE_THAT((arc_out.p0 - arc_out.center).norm(), WithinAbs(2.0, 1e-9)); + REQUIRE_THAT((arc_out.p1 - arc_out.center).norm(), WithinAbs(2.0, 1e-9)); +} + +TEST_CASE("Fillet parallel lines returns false", "[SketchEdit]") +{ + SketchEntity a; + a.type = SketchEntity::Type::Line; + a.p0 = Vec2d(0, 0); + a.p1 = Vec2d(10, 0); + + SketchEntity b; + b.type = SketchEntity::Type::Line; + b.p0 = Vec2d(0, 5); + b.p1 = Vec2d(10, 5); + + SketchEntity a_out, b_out, arc_out; + REQUIRE_FALSE(SketchEngine::fillet_lines(a, b, 1.0, a_out, b_out, arc_out)); +} + +TEST_CASE("Fillet arc too big returns false", "[SketchEdit]") +{ + SketchEntity a; + a.type = SketchEntity::Type::Line; + a.p0 = Vec2d(0, 0); + a.p1 = Vec2d(1, 0); + + SketchEntity b; + b.type = SketchEntity::Type::Line; + b.p0 = Vec2d(1, 0); + b.p1 = Vec2d(1, 1); + + SketchEntity a_out, b_out, arc_out; + REQUIRE_FALSE(SketchEngine::fillet_lines(a, b, 5.0, a_out, b_out, arc_out)); +} + +TEST_CASE("Trim right arm", "[SketchEdit]") +{ + SketchEntity e; + e.type = SketchEntity::Type::Line; + e.p0 = Vec2d(-5, 0); + e.p1 = Vec2d(5, 0); + + SketchEntity vc; + vc.type = SketchEntity::Type::Line; + vc.p0 = Vec2d(0, -5); + vc.p1 = Vec2d(0, 5); + + bool ok = SketchEngine::trim_entity(e, {vc}, Vec2d(3, 0)); + REQUIRE(ok); + REQUIRE_THAT(e.p0.x(), WithinAbs(-5.0, 1e-9)); + REQUIRE_THAT(e.p0.y(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(e.p1.x(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(e.p1.y(), WithinAbs(0.0, 1e-9)); +} + +TEST_CASE("Trim left arm", "[SketchEdit]") +{ + SketchEntity e; + e.type = SketchEntity::Type::Line; + e.p0 = Vec2d(-5, 0); + e.p1 = Vec2d(5, 0); + + SketchEntity vc; + vc.type = SketchEntity::Type::Line; + vc.p0 = Vec2d(0, -5); + vc.p1 = Vec2d(0, 5); + + bool ok = SketchEngine::trim_entity(e, {vc}, Vec2d(-3, 0)); + REQUIRE(ok); + REQUIRE_THAT(e.p0.x(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(e.p0.y(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(e.p1.x(), WithinAbs(5.0, 1e-9)); + REQUIRE_THAT(e.p1.y(), WithinAbs(0.0, 1e-9)); +} + +TEST_CASE("Trim no cut (u out of range)", "[SketchEdit]") +{ + SketchEntity e; + e.type = SketchEntity::Type::Line; + e.p0 = Vec2d(-5, 0); + e.p1 = Vec2d(5, 0); + + SketchEntity other; + other.type = SketchEntity::Type::Line; + other.p0 = Vec2d(0, 3); + other.p1 = Vec2d(0, 8); + + REQUIRE_FALSE(SketchEngine::trim_entity(e, {other}, Vec2d(3, 0))); +} + +TEST_CASE("Extend forward to line", "[SketchEdit]") +{ + SketchEntity e; + e.type = SketchEntity::Type::Line; + e.p0 = Vec2d(0, 0); + e.p1 = Vec2d(2, 0); + + SketchEntity other; + other.type = SketchEntity::Type::Line; + other.p0 = Vec2d(5, -5); + other.p1 = Vec2d(5, 5); + + bool ok = SketchEngine::extend_entity(e, {other}, Vec2d(2, 0)); + REQUIRE(ok); + REQUIRE_THAT(e.p0.x(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(e.p0.y(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(e.p1.x(), WithinAbs(5.0, 1e-9)); + REQUIRE_THAT(e.p1.y(), WithinAbs(0.0, 1e-9)); +} + +TEST_CASE("Extend forward to circle", "[SketchEdit]") +{ + SketchEntity e; + e.type = SketchEntity::Type::Line; + e.p0 = Vec2d(0, 0); + e.p1 = Vec2d(2, 0); + + SketchEntity other; + other.type = SketchEntity::Type::Circle; + other.center = Vec2d(10, 0); + other.p0 = Vec2d(10, 0); + other.radius = 3; + + bool ok = SketchEngine::extend_entity(e, {other}, Vec2d(2, 0)); + REQUIRE(ok); + REQUIRE_THAT(e.p0.x(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(e.p0.y(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(e.p1.x(), WithinAbs(7.0, 1e-9)); + REQUIRE_THAT(e.p1.y(), WithinAbs(0.0, 1e-9)); +} + +TEST_CASE("Extend backward", "[SketchEdit]") +{ + SketchEntity e; + e.type = SketchEntity::Type::Line; + e.p0 = Vec2d(0, 0); + e.p1 = Vec2d(2, 0); + + SketchEntity other; + other.type = SketchEntity::Type::Line; + other.p0 = Vec2d(-3, -5); + other.p1 = Vec2d(-3, 5); + + bool ok = SketchEngine::extend_entity(e, {other}, Vec2d(0, 0)); + REQUIRE(ok); + REQUIRE_THAT(e.p0.x(), WithinAbs(-3.0, 1e-9)); + REQUIRE_THAT(e.p0.y(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(e.p1.x(), WithinAbs(2.0, 1e-9)); + REQUIRE_THAT(e.p1.y(), WithinAbs(0.0, 1e-9)); +} + +TEST_CASE("Extend no target", "[SketchEdit]") +{ + SketchEntity e; + e.type = SketchEntity::Type::Line; + e.p0 = Vec2d(0, 0); + e.p1 = Vec2d(2, 0); + + SketchEntity other; + other.type = SketchEntity::Type::Line; + other.p0 = Vec2d(5, -5); + other.p1 = Vec2d(5, -1); + + REQUIRE_FALSE(SketchEngine::extend_entity(e, {other}, Vec2d(2, 0))); +} + +// --- Arc/Circle subject trim & extend (Fase 4.5 kernel) ------------------- + +TEST_CASE("Trim arc drops the picked (start) side", "[SketchEdit]") +{ + // Upper semicircle r=5, ccw from (5,0) to (-5,0); cutter = vertical axis. + SketchEntity e; + e.type = SketchEntity::Type::Arc; + e.center = Vec2d(0, 0); + e.radius = 5; + e.start_angle = 0.0; + e.end_angle = M_PI; + + SketchEntity cut; + cut.type = SketchEntity::Type::Line; + cut.p0 = Vec2d(0, -10); + cut.p1 = Vec2d(0, 10); + + // Pick the right quarter (phi=pi/4) -> it is removed, left quarter kept. + bool ok = SketchEngine::trim_entity(e, {cut}, Vec2d(5 * std::cos(M_PI/4), 5 * std::sin(M_PI/4))); + REQUIRE(ok); + REQUIRE(e.type == SketchEntity::Type::Arc); + REQUIRE_THAT(e.radius, WithinAbs(5.0, 1e-9)); + REQUIRE_THAT(e.start_angle, WithinAbs(M_PI / 2.0, 1e-9)); + REQUIRE_THAT(e.end_angle, WithinAbs(M_PI, 1e-9)); +} + +TEST_CASE("Trim arc drops the picked (end) side", "[SketchEdit]") +{ + SketchEntity e; + e.type = SketchEntity::Type::Arc; + e.center = Vec2d(0, 0); + e.radius = 5; + e.start_angle = 0.0; + e.end_angle = M_PI; + + SketchEntity cut; + cut.type = SketchEntity::Type::Line; + cut.p0 = Vec2d(0, -10); + cut.p1 = Vec2d(0, 10); + + // Pick the left quarter (phi=3pi/4) -> removed, right quarter kept. + bool ok = SketchEngine::trim_entity(e, {cut}, Vec2d(5 * std::cos(3*M_PI/4), 5 * std::sin(3*M_PI/4))); + REQUIRE(ok); + REQUIRE(e.type == SketchEntity::Type::Arc); + REQUIRE_THAT(e.start_angle, WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(e.end_angle, WithinAbs(M_PI / 2.0, 1e-9)); +} + +TEST_CASE("Trim circle opens into an arc excluding the pick", "[SketchEdit]") +{ + // Full circle r=5; vertical axis cuts it at (0,+-5). Pick the right side + // (5,0): the kept arc is the left half, sweeping pi and centred on (-5,0). + SketchEntity e; + e.type = SketchEntity::Type::Circle; + e.center = Vec2d(0, 0); + e.p0 = Vec2d(5, 0); + e.radius = 5; + + SketchEntity cut; + cut.type = SketchEntity::Type::Line; + cut.p0 = Vec2d(0, -10); + cut.p1 = Vec2d(0, 10); + + bool ok = SketchEngine::trim_entity(e, {cut}, Vec2d(5, 0)); + REQUIRE(ok); + REQUIRE(e.type == SketchEntity::Type::Arc); + REQUIRE_THAT(e.radius, WithinAbs(5.0, 1e-9)); + REQUIRE_THAT(e.end_angle - e.start_angle, WithinAbs(M_PI, 1e-9)); + // Midpoint of the kept arc must point left (away from the pick). + double mid = 0.5 * (e.start_angle + e.end_angle); + REQUIRE_THAT(5 * std::cos(mid), WithinAbs(-5.0, 1e-9)); + REQUIRE_THAT(5 * std::sin(mid), WithinAbs(0.0, 1e-9)); +} + +TEST_CASE("Extend arc forward (end) to a crossing", "[SketchEdit]") +{ + // Quarter arc (5,0)->(0,5); cutter crosses the circle at (-5,0). Picking + // near the end grows the sweep ccw to pi. + SketchEntity e; + e.type = SketchEntity::Type::Arc; + e.center = Vec2d(0, 0); + e.radius = 5; + e.start_angle = 0.0; + e.end_angle = M_PI / 2.0; + + SketchEntity cut; + cut.type = SketchEntity::Type::Line; + cut.p0 = Vec2d(-10, 0); + cut.p1 = Vec2d(0, 0); + + bool ok = SketchEngine::extend_entity(e, {cut}, Vec2d(0, 5)); + REQUIRE(ok); + REQUIRE(e.type == SketchEntity::Type::Arc); + REQUIRE_THAT(e.start_angle, WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(e.end_angle, WithinAbs(M_PI, 1e-9)); +} + +TEST_CASE("Extend arc backward (start) to a crossing", "[SketchEdit]") +{ + // Quarter arc (0,5)->(-5,0); cutter crosses at (5,0). Picking near the + // start grows the sweep cw to start_angle 0. + SketchEntity e; + e.type = SketchEntity::Type::Arc; + e.center = Vec2d(0, 0); + e.radius = 5; + e.start_angle = M_PI / 2.0; + e.end_angle = M_PI; + + SketchEntity cut; + cut.type = SketchEntity::Type::Line; + cut.p0 = Vec2d(10, 0); + cut.p1 = Vec2d(0, 0); + + bool ok = SketchEngine::extend_entity(e, {cut}, Vec2d(0, 5)); + REQUIRE(ok); + REQUIRE_THAT(e.start_angle, WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(e.end_angle, WithinAbs(M_PI, 1e-9)); +} + +TEST_CASE("Extend circle returns false (closed)", "[SketchEdit]") +{ + SketchEntity e; + e.type = SketchEntity::Type::Circle; + e.center = Vec2d(0, 0); + e.p0 = Vec2d(5, 0); + e.radius = 5; + + SketchEntity cut; + cut.type = SketchEntity::Type::Line; + cut.p0 = Vec2d(0, -10); + cut.p1 = Vec2d(0, 10); + + REQUIRE_FALSE(SketchEngine::extend_entity(e, {cut}, Vec2d(5, 0))); +} + +TEST_CASE("Trim arc with no crossing returns false", "[SketchEdit]") +{ + SketchEntity e; + e.type = SketchEntity::Type::Arc; + e.center = Vec2d(0, 0); + e.radius = 5; + e.start_angle = 0.0; + e.end_angle = M_PI / 2.0; + + SketchEntity cut; // far away, never reaches the r=5 circle + cut.type = SketchEntity::Type::Line; + cut.p0 = Vec2d(20, -5); + cut.p1 = Vec2d(20, 5); + + REQUIRE_FALSE(SketchEngine::trim_entity(e, {cut}, Vec2d(5 * std::cos(M_PI/4), 5 * std::sin(M_PI/4)))); +} diff --git a/tests/libslic3r/test_sketchimport.cpp b/tests/libslic3r/test_sketchimport.cpp new file mode 100644 index 0000000000..ad58ca4706 --- /dev/null +++ b/tests/libslic3r/test_sketchimport.cpp @@ -0,0 +1,93 @@ +#include + +#include "libslic3r/SketchImport.hpp" +#include "libslic3r/Utils.hpp" // resources_dir + +#include +#include + +using namespace Slic3r; + +TEST_CASE("svg_to_regions parses a filled path into a region", "[SketchImport]") +{ + // A 10x10 mm filled square. Written to a temp file because nanosvg reads + // from disk. + const std::string path = "/tmp/snaporca_test_square.svg"; + { + std::ofstream f(path); + f << "" + ""; + } + + ImportRegions regs = svg_to_regions(path, 1.0); + REQUIRE(regs.size() >= 1); + // Outer contour present with at least a few vertices. + REQUIRE(regs[0].size() >= 1); + REQUIRE(regs[0][0].size() >= 4); + + // Centred on the origin: bbox half-extent ~5 mm on each side. + double hi = 0.0; + for (const auto& region : regs) + for (const auto& contour : region) + for (const Vec2d& p : contour) + hi = std::max(hi, std::max(std::abs(p.x()), std::abs(p.y()))); + REQUIRE(hi > 3.0); // not collapsed + REQUIRE(hi < 8.0); // ~5 mm half-size after centring +} + +TEST_CASE("svg_to_regions rejects bad input gracefully", "[SketchImport]") +{ + REQUIRE(svg_to_regions("", 1.0).empty()); + REQUIRE(svg_to_regions("/tmp/snaporca_does_not_exist.svg", 1.0).empty()); + REQUIRE(svg_to_regions("/tmp/snaporca_test_square.svg", 0.0).empty()); // scale<=0 +} + +TEST_CASE("transform_regions moves and scales independently", "[SketchImport]") +{ + ImportRegions r = {{ {Vec2d(-1,-1), Vec2d(1,-1), Vec2d(1,1), Vec2d(-1,1)} }}; + ImportRegions t = transform_regions(r, Vec2d(10, 20), 2.0, 3.0); + REQUIRE(t.size() == 1); + REQUIRE(t[0][0].size() == 4); + // (-1,-1) -> (-1*2+10, -1*3+20) = (8, 17) + REQUIRE_THAT(t[0][0][0].x(), Catch::Matchers::WithinAbs(8.0, 1e-9)); + REQUIRE_THAT(t[0][0][0].y(), Catch::Matchers::WithinAbs(17.0, 1e-9)); + // (1,1) -> (1*2+10, 1*3+20) = (12, 23) + REQUIRE_THAT(t[0][0][2].x(), Catch::Matchers::WithinAbs(12.0, 1e-9)); + REQUIRE_THAT(t[0][0][2].y(), Catch::Matchers::WithinAbs(23.0, 1e-9)); + // identity is a no-op + ImportRegions id = transform_regions(r, Vec2d(0,0), 1.0, 1.0); + REQUIRE_THAT(id[0][0][1].x(), Catch::Matchers::WithinAbs(1.0, 1e-9)); +} + +TEST_CASE("text_to_regions vectorizes glyphs with counters", "[SketchImport]") +{ + // Locate the bundled font; resources_dir() may be unset under ctest, so + // fall back to a cwd-relative path (tests run from the repo root). + std::string font = resources_dir().empty() + ? std::string("resources/fonts/HarmonyOS_Sans_SC_Regular.ttf") + : resources_dir() + "/fonts/HarmonyOS_Sans_SC_Regular.ttf"; + { + std::ifstream probe(font); + if (!probe.good()) { + SUCCEED("bundled font not reachable in this environment; covered live on :10"); + return; + } + } + + // Bad input is rejected without throwing. + REQUIRE(text_to_regions("", 10.0, font).empty()); + REQUIRE(text_to_regions("A", 0.0, font).empty()); + + // 'A' has one triangular counter -> a region with an outer + 1 hole. + ImportRegions a = text_to_regions("A", 12.0, font); + REQUIRE(a.size() >= 1); + bool has_hole = false; + for (const auto& region : a) + if (region.size() >= 2) has_hole = true; + REQUIRE(has_hole); + + // Two letters produce more regions than one. + ImportRegions ab = text_to_regions("AB", 12.0, font); + REQUIRE(ab.size() >= a.size()); +} diff --git a/tests/libslic3r/test_sketchinference.cpp b/tests/libslic3r/test_sketchinference.cpp new file mode 100644 index 0000000000..d2ea35cf14 --- /dev/null +++ b/tests/libslic3r/test_sketchinference.cpp @@ -0,0 +1,77 @@ +#include + +#include "libslic3r/SketchInference.hpp" + +using namespace Slic3r; +using K = InferenceSnap::Kind; + +static SketchEntity line(Vec2d a, Vec2d b) +{ + SketchEntity e; e.type = SketchEntity::Type::Line; e.p0 = a; e.p1 = b; return e; +} +static SketchEntity circle(Vec2d c, double r) +{ + SketchEntity e; e.type = SketchEntity::Type::Circle; e.center = c; e.p0 = c; e.radius = r; return e; +} + +TEST_CASE("inference: cursor near a line endpoint snaps Coincident-able to it", "[inference]") +{ + std::vector ents = { line({0, 0}, {10, 0}) }; + auto s = infer_point_snap(ents, {10.3, 0.2}, 1.0); + REQUIRE(s.kind == K::Endpoint); + CHECK(s.entity == 0); + CHECK(s.role == SketchPointRole::P1); + CHECK((s.point - Vec2d(10, 0)).norm() == Approx(0.0).margin(1e-9)); +} + +TEST_CASE("inference: endpoint beats midpoint when both are in range", "[inference]") +{ + std::vector ents = { line({0, 0}, {2, 0}) }; + // Query equidistant-ish but closer to the endpoint: endpoint tier wins regardless. + auto s = infer_point_snap(ents, {1.9, 0.0}, 5.0); + CHECK(s.kind == K::Endpoint); + CHECK(s.role == SketchPointRole::P1); +} + +TEST_CASE("inference: midpoint of a line is detected", "[inference]") +{ + std::vector ents = { line({0, 0}, {10, 0}) }; + auto s = infer_point_snap(ents, {5.1, 0.1}, 0.5, /*include_origin=*/false); + REQUIRE(s.kind == K::Midpoint); + CHECK((s.point - Vec2d(5, 0)).norm() == Approx(0.0).margin(1e-9)); +} + +TEST_CASE("inference: circle centre and rim", "[inference]") +{ + std::vector ents = { circle({0, 0}, 5.0) }; + auto c = infer_point_snap(ents, {0.2, 0.1}, 1.0, false); + CHECK(c.kind == K::Center); + auto r = infer_point_snap(ents, {5.1, 0.0}, 1.0, false); + REQUIRE(r.kind == K::OnEdge); + CHECK((r.point - Vec2d(5, 0)).norm() == Approx(0.0).margin(1e-9)); +} + +TEST_CASE("inference: origin snap when nothing else is near", "[inference]") +{ + std::vector ents = { line({20, 20}, {30, 20}) }; + auto s = infer_point_snap(ents, {0.1, 0.1}, 1.0); + REQUIRE(s.kind == K::Origin); + CHECK(s.entity == -1); + CHECK((s.point - Vec2d(0, 0)).norm() == Approx(0.0).margin(1e-9)); +} + +TEST_CASE("inference: nothing in range returns None and the raw query", "[inference]") +{ + std::vector ents = { line({0, 0}, {10, 0}) }; + auto s = infer_point_snap(ents, {50, 50}, 1.0, /*include_origin=*/false); + CHECK(s.kind == K::None); + CHECK((s.point - Vec2d(50, 50)).norm() == Approx(0.0).margin(1e-9)); +} + +TEST_CASE("inference: axis inference flags horizontal / vertical segments", "[inference]") +{ + CHECK(infer_axis_constraint({0, 0}, {10, 0.05}).value() == SketchConstraintType::Horizontal); + CHECK(infer_axis_constraint({0, 0}, {0.05, 10}).value() == SketchConstraintType::Vertical); + CHECK_FALSE(infer_axis_constraint({0, 0}, {10, 10}).has_value()); // 45 deg + CHECK_FALSE(infer_axis_constraint({0, 0}, {0, 0}).has_value()); // degenerate +} diff --git a/tests/libslic3r/test_slvs_constraints.cpp b/tests/libslic3r/test_slvs_constraints.cpp new file mode 100644 index 0000000000..e6044e6bfc --- /dev/null +++ b/tests/libslic3r/test_slvs_constraints.cpp @@ -0,0 +1,113 @@ +#include + +#include "libslic3r/SketchSolver.hpp" +#include "libslic3r/SketchEngine.hpp" + +using namespace Slic3r; +using CT = SketchConstraintType; +using R = SketchPointRole; + +static SketchEntity line(Vec2d a, Vec2d b) +{ + SketchEntity e; e.type = SketchEntity::Type::Line; e.p0 = a; e.p1 = b; return e; +} +static SketchEntity circle(Vec2d c, double r) +{ + SketchEntity e; e.type = SketchEntity::Type::Circle; e.center = c; e.p0 = c; e.radius = r; return e; +} +static SketchEntityConstraintDef con(CT t, int ea, R ra, int eb, R rb, double v = 0.0) +{ + SketchEntityConstraintDef c; c.type = t; c.ea = ea; c.ra = ra; c.eb = eb; c.rb = rb; c.value = v; return c; +} + +TEST_CASE("slvs: distance + horizontal + fix solves a line length", "[slvs]") +{ + std::vector ents = { line({0, 0}, {5, 1}) }; + std::vector cons = { + con(CT::Fix, 0, R::P0, 0, R::P0), + con(CT::Horizontal, 0, R::P0, 0, R::P1), + con(CT::Distance, 0, R::P0, 0, R::P1, 10.0), + }; + auto res = sketch_solve(ents, cons); + REQUIRE(res.ok); + CHECK((ents[0].p1 - ents[0].p0).norm() == Approx(10.0).margin(1e-6)); + CHECK(ents[0].p0.x() == Approx(0.0).margin(1e-6)); + CHECK(ents[0].p0.y() == Approx(0.0).margin(1e-6)); + CHECK(ents[0].p1.y() == Approx(0.0).margin(1e-6)); // horizontal +} + +TEST_CASE("slvs: coincident joins two line endpoints (loop closes)", "[slvs]") +{ + std::vector ents = { line({0, 0}, {10, 0}), line({10.3, 0.2}, {10, 10}) }; + std::vector cons = { + con(CT::Coincident, 0, R::P1, 1, R::P0), + }; + auto res = sketch_solve(ents, cons); + REQUIRE(res.ok); + CHECK((ents[0].p1 - ents[1].p0).norm() == Approx(0.0).margin(1e-6)); +} + +TEST_CASE("slvs: parallel + perpendicular on lines", "[slvs]") +{ + std::vector ents = { line({0, 0}, {10, 1}), line({0, 5}, {10, 5.5}), line({0, 0}, {0.5, 10}) }; + std::vector cons = { + con(CT::Fix, 0, R::P0, 0, R::P0), + con(CT::Horizontal, 0, R::P0, 0, R::P1), + con(CT::Parallel, 0, R::P0, 1, R::P0), // line1 parallel to line0 + con(CT::Perpendicular, 0, R::P0, 2, R::P0), // line2 perpendicular to line0 + }; + auto res = sketch_solve(ents, cons); + REQUIRE(res.ok); + CHECK(ents[1].p1.y() - ents[1].p0.y() == Approx(0.0).margin(1e-6)); // line1 horizontal + CHECK(ents[2].p1.x() - ents[2].p0.x() == Approx(0.0).margin(1e-6)); // line2 vertical +} + +TEST_CASE("slvs: circle radius constraint", "[slvs]") +{ + std::vector ents = { circle({2, 2}, 3.0) }; + std::vector cons = { con(CT::Radius, 0, R::P0, -1, R::P0, 7.0) }; + auto res = sketch_solve(ents, cons); + REQUIRE(res.ok); + CHECK(ents[0].radius == Approx(7.0).margin(1e-6)); +} + +TEST_CASE("slvs: degrees of freedom reported", "[slvs]") +{ + // One free line with only a Fix on the start: 4 DoF total minus 2 (fix) = 2 remaining. + std::vector ents = { line({0, 0}, {3, 4}) }; + std::vector cons = { con(CT::Fix, 0, R::P0, 0, R::P0) }; + auto res = sketch_solve(ents, cons); + REQUIRE(res.ok); + CHECK(res.dof == 2); +} + +TEST_CASE("slvs: drag pulls a point while constraints hold", "[slvs]") +{ + // A vertical line of fixed length 10, P0 pinned at the origin. Dragging P1 toward + // (10,0) must keep the length (Distance constraint) but rotate the line so the end + // follows the cursor into positive x — the dragged param wins the under-constrained DoF. + std::vector ents = { line({0, 0}, {0, 10}) }; + std::vector cons = { + con(CT::Fix, 0, R::P0, 0, R::P0), + con(CT::Distance, 0, R::P0, 0, R::P1, 10.0), + }; + ents[0].p1 = Vec2d(10, 0); // user dropped the endpoint here + auto res = sketch_solve_drag(ents, cons, 0, R::P1); + REQUIRE(res.ok); + CHECK((ents[0].p1 - ents[0].p0).norm() == Approx(10.0).margin(1e-6)); // length held + CHECK(ents[0].p0.x() == Approx(0.0).margin(1e-6)); // P0 still pinned + CHECK(ents[0].p0.y() == Approx(0.0).margin(1e-6)); + CHECK(ents[0].p1.x() > 1.0); // end followed the drag toward +x (not stuck vertical) +} + +TEST_CASE("slvs: over-constrained / inconsistent is detected", "[slvs]") +{ + std::vector ents = { line({0, 0}, {5, 0}) }; + std::vector cons = { + con(CT::Fix, 0, R::P0, 0, R::P0), + con(CT::Fix, 0, R::P1, 0, R::P1), + con(CT::Distance, 0, R::P0, 0, R::P1, 99.0), // contradicts the pinned endpoints + }; + auto res = sketch_solve(ents, cons); + CHECK_FALSE(res.ok); // SLVS_RESULT_INCONSISTENT +}