mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-26 18:31:11 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
237cd10eb5 | ||
|
|
e77d179bbe | ||
|
|
d5aaa463c8 | ||
|
|
8c03985818 | ||
|
|
93b58a2034 | ||
|
|
6be6fdd7c7 | ||
|
|
521a30a45c | ||
|
|
35d5ff705b | ||
|
|
482fc1e719 | ||
|
|
d087941289 |
@@ -12,9 +12,9 @@ name: Daily OFL OTA Update
|
|||||||
# vendor-dispatch path is also what makes post_merge_profiles.yml call the OTA auto-publish API after
|
# vendor-dispatch path is also what makes post_merge_profiles.yml call the OTA auto-publish API after
|
||||||
# uploading - see post_merge_profiles.yml for both sides of that contract.
|
# uploading - see post_merge_profiles.yml for both sides of that contract.
|
||||||
#
|
#
|
||||||
# If at least one branch was dispatched this run, a final step clears OFL's pending-publish
|
# At the start of each run, the pending-publish table is cleared up to a captured
|
||||||
# table (POST /api/v1/ota/ofl/pending/clear) - the daily "published everything, reset" signal.
|
# timestamp (POST /api/v1/ota/ofl/pending/clear?timestamp=...). Changes merged after
|
||||||
# That table is populated only by this pipeline's own auto-publish calls.
|
# that timestamp remain pending for the next run.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
@@ -34,6 +34,32 @@ jobs:
|
|||||||
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' }}
|
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' }}
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
|
- name: Capture start timestamp and clear OFL pending queue
|
||||||
|
id: start
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
OTA_API_BASE_URL: ${{ vars.OTA_API_BASE_URL }}
|
||||||
|
OTA_API_KEY: ${{ secrets.OFL_OTA_PUBLISH_KEY }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
[ -n "$OTA_API_BASE_URL" ] || { echo "::error::vars.OTA_API_BASE_URL is not set"; exit 1; }
|
||||||
|
[ -n "$OTA_API_KEY" ] || { echo "::error::secrets.OFL_OTA_PUBLISH_KEY is not set"; exit 1; }
|
||||||
|
|
||||||
|
timestamp="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||||
|
echo "timestamp=$timestamp" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
resp_file="$RUNNER_TEMP/ota-pending-clear-response.json"
|
||||||
|
status="$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST \
|
||||||
|
"${OTA_API_BASE_URL%/}/api/v1/ota/ofl/pending/clear?timestamp=$timestamp" \
|
||||||
|
-H "Authorization: Bearer $OTA_API_KEY")"
|
||||||
|
body="$(cat "$resp_file")"
|
||||||
|
echo "$body"
|
||||||
|
|
||||||
|
if [ "$status" != "200" ]; then
|
||||||
|
echo "::error::OTA pending-clear call failed with HTTP $status"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
@@ -46,13 +72,12 @@ jobs:
|
|||||||
run: git fetch origin '+refs/heads/*:refs/remotes/origin/*'
|
run: git fetch origin '+refs/heads/*:refs/remotes/origin/*'
|
||||||
|
|
||||||
- name: Scan branches and publish changed OFL profiles
|
- name: Scan branches and publish changed OFL profiles
|
||||||
id: scan
|
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
SCAN_UNTIL: ${{ steps.start.outputs.timestamp }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
published_any=false
|
|
||||||
|
|
||||||
mapfile -t branches < <(
|
mapfile -t branches < <(
|
||||||
gh api "repos/${{ github.repository }}/branches" --paginate --jq '.[].name' \
|
gh api "repos/${{ github.repository }}/branches" --paginate --jq '.[].name' \
|
||||||
@@ -79,68 +104,36 @@ jobs:
|
|||||||
--jq '.workflow_runs[0].run_started_at // empty')"
|
--jq '.workflow_runs[0].run_started_at // empty')"
|
||||||
|
|
||||||
if [ -z "$since" ]; then
|
if [ -z "$since" ]; then
|
||||||
echo "No prior successful run for $branch; treating OFL as changed."
|
echo "No prior successful run for $branch; checking OFL changes up to $SCAN_UNTIL."
|
||||||
changed=true
|
changed_files="$(git log --until="$SCAN_UNTIL" --name-only --pretty=format: "origin/$branch" -- \
|
||||||
else
|
|
||||||
changed_files="$(git log --since="$since" --name-only --pretty=format: "origin/$branch" -- \
|
|
||||||
resources/profiles/OrcaFilamentLibrary resources/profiles/OrcaFilamentLibrary.json \
|
resources/profiles/OrcaFilamentLibrary resources/profiles/OrcaFilamentLibrary.json \
|
||||||
| sed '/^$/d')"
|
| sed '/^$/d')"
|
||||||
|
else
|
||||||
|
changed_files="$(git log --since="$since" --until="$SCAN_UNTIL" --name-only --pretty=format: "origin/$branch" -- \
|
||||||
|
resources/profiles/OrcaFilamentLibrary resources/profiles/OrcaFilamentLibrary.json \
|
||||||
|
| sed '/^$/d')"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -n "$changed_files" ]; then
|
if [ -n "$changed_files" ]; then
|
||||||
echo "OFL changed on $branch since $since:"
|
echo "OFL changed on $branch from ${since:-the beginning} through $SCAN_UNTIL:"
|
||||||
echo "$changed_files"
|
echo "$changed_files"
|
||||||
changed=true
|
changed=true
|
||||||
else
|
else
|
||||||
echo "No OFL changes on $branch since $since."
|
echo "No OFL changes on $branch through $SCAN_UNTIL."
|
||||||
changed=false
|
changed=false
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$changed" = true ]; then
|
if [ "$changed" = true ]; then
|
||||||
# Tolerate a per-branch failure (e.g. a pre-existing release branch
|
# Tolerate a per-branch failure (e.g. a pre-existing release branch
|
||||||
# whose post_merge_profiles.yml predates the vendor/auto_publish
|
# whose post_merge_profiles.yml predates the vendor/auto_publish
|
||||||
# inputs) rather than aborting the whole scan under set -e.
|
# inputs) rather than aborting the whole scan under set -e.
|
||||||
if gh workflow run post_merge_profiles.yml \
|
if ! gh workflow run post_merge_profiles.yml \
|
||||||
--repo "${{ github.repository }}" \
|
--repo "${{ github.repository }}" \
|
||||||
--ref "$branch" \
|
--ref "$branch" \
|
||||||
-f vendor="$VENDOR" -f auto_publish=true; then
|
-f vendor="$VENDOR" -f auto_publish=true; then
|
||||||
published_any=true
|
|
||||||
else
|
|
||||||
echo "::warning::failed to dispatch post_merge_profiles.yml for $branch - its post_merge_profiles.yml at this ref may predate the vendor/auto_publish inputs"
|
echo "::warning::failed to dispatch post_merge_profiles.yml for $branch - its post_merge_profiles.yml at this ref may predate the vendor/auto_publish inputs"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "published_any=$published_any" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Clear OFL pending queue
|
|
||||||
# Only when this run actually kicked off at least one publish - the
|
|
||||||
# daily reset is scoped to today's real activity, not called on a day
|
|
||||||
# where every branch reported no changes. Note "published_any" reflects
|
|
||||||
# a successful DISPATCH, not a confirmed live publish: gh workflow run
|
|
||||||
# is fire-and-forget, so this workflow never learns whether the
|
|
||||||
# dispatched post_merge_profiles.yml run actually reached its own
|
|
||||||
# auto-publish call. Acceptable since the table is populated only by
|
|
||||||
# our own auto-publish calls, not by anything else.
|
|
||||||
if: steps.scan.outputs.published_any == 'true'
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
OTA_API_BASE_URL: ${{ vars.OTA_API_BASE_URL }}
|
|
||||||
OTA_API_KEY: ${{ secrets.OFL_OTA_PUBLISH_KEY }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
[ -n "$OTA_API_BASE_URL" ] || { echo "::error::vars.OTA_API_BASE_URL is not set"; exit 1; }
|
|
||||||
[ -n "$OTA_API_KEY" ] || { echo "::error::secrets.OFL_OTA_PUBLISH_KEY is not set"; exit 1; }
|
|
||||||
|
|
||||||
resp_file="$RUNNER_TEMP/ota-pending-clear-response.json"
|
|
||||||
status="$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST \
|
|
||||||
"${OTA_API_BASE_URL%/}/api/v1/ota/ofl/pending/clear" \
|
|
||||||
-H "Authorization: Bearer $OTA_API_KEY")"
|
|
||||||
body="$(cat "$resp_file")"
|
|
||||||
echo "$body"
|
|
||||||
|
|
||||||
if [ "$status" != "200" ]; then
|
|
||||||
echo "::error::OTA pending-clear call failed with HTTP $status"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|||||||
+7
-2
@@ -56,9 +56,9 @@ You can do this in Environment Variables settings.
|
|||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 11.3
|
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 12.0 (the lowest Xcode 27 accepts)
|
||||||
if (NOT CMAKE_OSX_DEPLOYMENT_TARGET)
|
if (NOT CMAKE_OSX_DEPLOYMENT_TARGET)
|
||||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.3" CACHE STRING "Minimum OS X deployment version" FORCE)
|
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING "Minimum OS X deployment version" FORCE)
|
||||||
endif ()
|
endif ()
|
||||||
message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
|
message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
|
||||||
endif ()
|
endif ()
|
||||||
@@ -279,6 +279,11 @@ if (APPLE)
|
|||||||
endif()
|
endif()
|
||||||
SET(CMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.orcaslicer.OrcaSlicer")
|
SET(CMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.orcaslicer.OrcaSlicer")
|
||||||
|
|
||||||
|
# The macOS CI jobs build with Ninja (build_release_macos.sh -x), so the Xcode generator
|
||||||
|
# is not covered. Xcode adds -Wshorten-64-to-32 by default ("Implicit Conversion to 32 Bit
|
||||||
|
# Type"); Ninja/-Wall does not, and under -Werror it fails Xcode builds on code CI accepts.
|
||||||
|
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_64_TO_32_BIT_CONVERSION "NO")
|
||||||
|
|
||||||
message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}")
|
message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}")
|
||||||
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||||
set(CMAKE_INSTALL_RPATH "$ORIGIN")
|
set(CMAKE_INSTALL_RPATH "$ORIGIN")
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
|
|||||||
echo " -s: Build slicer only"
|
echo " -s: Build slicer only"
|
||||||
echo " -u: Build universal app only (requires existing arm64 and x86_64 app bundles)"
|
echo " -u: Build universal app only (requires existing arm64 and x86_64 app bundles)"
|
||||||
echo " -n: Nightly build"
|
echo " -n: Nightly build"
|
||||||
echo " -t: Specify minimum version of the target platform, default is 11.3"
|
echo " -t: Specify minimum version of the target platform, default is 12.0"
|
||||||
echo " -x: Use Ninja Multi-Config CMake generator, default is Xcode"
|
echo " -x: Use Ninja Multi-Config CMake generator, default is Xcode"
|
||||||
echo " -b: Build without reconfiguring CMake"
|
echo " -b: Build without reconfiguring CMake"
|
||||||
echo " -c: Set CMake build configuration, default is Release"
|
echo " -c: Set CMake build configuration, default is Release"
|
||||||
@@ -95,7 +95,7 @@ if [ -z "$DEPS_CMAKE_GENERATOR" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$OSX_DEPLOYMENT_TARGET" ]; then
|
if [ -z "$OSX_DEPLOYMENT_TARGET" ]; then
|
||||||
export OSX_DEPLOYMENT_TARGET="11.3"
|
export OSX_DEPLOYMENT_TARGET="12.0"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$CMAKE_IGNORE_PREFIX_PATH" ]; then
|
if [ -z "$CMAKE_IGNORE_PREFIX_PATH" ]; then
|
||||||
|
|||||||
Vendored
+2
-2
@@ -26,9 +26,9 @@ endif()
|
|||||||
|
|
||||||
cmake_minimum_required(VERSION 3.2)
|
cmake_minimum_required(VERSION 3.2)
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 11.3
|
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 12.0 (the lowest Xcode 27 accepts)
|
||||||
if (NOT CMAKE_OSX_DEPLOYMENT_TARGET)
|
if (NOT CMAKE_OSX_DEPLOYMENT_TARGET)
|
||||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.3" CACHE STRING "Minimum OS X deployment version" FORCE)
|
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING "Minimum OS X deployment version" FORCE)
|
||||||
endif ()
|
endif ()
|
||||||
message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
|
message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
|
||||||
|
|
||||||
|
|||||||
@@ -2,219 +2,136 @@
|
|||||||
|
|
||||||
## Why it exists
|
## Why it exists
|
||||||
|
|
||||||
The main window is a notebook of tabs. When the first frame appears, one tab is on
|
The main window is a notebook of tabs, and only one of them is on screen when the first
|
||||||
screen and the others are not; some of them are opened later in the session, some
|
frame appears. Others are opened later in the session, some never, and some only exist
|
||||||
never, and some only exist for certain printers. Building a tab before the first frame
|
for certain printers. Building every tab before the first frame makes each startup pay for
|
||||||
adds its cost to every startup, whether or not the tab is used.
|
tabs the user may never open.
|
||||||
|
|
||||||
This subsystem builds a tab's panel the first time the tab is shown. It also builds the
|
This subsystem builds a tab the first time it is shown, and builds the rest in small units
|
||||||
remaining tabs while the user is idle after startup, in units of tens of milliseconds,
|
while the user is idle after startup. Startup pays only for what the first frame shows,
|
||||||
so a click that lands in the middle of one waits for that unit and no longer. Startup
|
the other tabs are usually ready before anyone opens them, and a click that lands in the
|
||||||
pays only for what the first frame shows, and the other tabs are usually built before
|
middle of the idle build waits for one unit at most. Work that is not a tab, such as a
|
||||||
anyone opens them.
|
dialog or the 3D view's GL resources, uses the same machinery.
|
||||||
|
|
||||||
## The parts
|
## The parts
|
||||||
|
|
||||||
`src/slic3r/GUI/Lazy.hpp`, `LazyPage.hpp`, `StagedBuild.hpp` and `IdleScheduler.hpp/.cpp`
|
The parts are independent. A holder can hold anything a factory makes, a placeholder page
|
||||||
(with its wx-free `PrebuildQueue.hpp`) are independent. A holder can hold anything a
|
is a holder with a widget, a staged object can live outside a holder, and the scheduler
|
||||||
factory makes, a page is a holder with a placeholder widget, a staged object can live
|
knows none of them; it runs tasks, which the main window makes from the holders.
|
||||||
outside a holder, and the scheduler knows none of them; it runs tasks, which `MainFrame`
|
|
||||||
makes from the holders.
|
|
||||||
|
|
||||||
### Lazy<T>: the holder
|
### The holder: `Lazy<T>`
|
||||||
|
|
||||||
Holds an object that a factory makes on first use, under a name for the log and a place
|
A holder keeps one object and the factory that makes it. The rest of the app reads the
|
||||||
in the idle queue, both given by the owner. The header has no wx dependency; the busy
|
object if it exists, makes sure it exists now when about to show or navigate to it, or runs
|
||||||
cursor for an on-demand build lives in `Lazy.cpp` and is skipped when there is no app,
|
something once it exists. A type with one instance in the app gets these as statics
|
||||||
so the holder is unit-tested.
|
through `LazyInstance<T>`, so callers need no reference to the main window, and all of them
|
||||||
|
are harmless while no holder exists.
|
||||||
|
|
||||||
- `get()` is null until the object is completely built; `built()` says the same.
|
The holder guarantees that callers see the object only once it is completely built. A
|
||||||
- `ensure()` builds whatever is left now, under a busy cursor, and returns the object, or
|
build cannot re-enter itself, so a nested request finds nothing yet, and a factory that
|
||||||
null in the two cases below. A click on an unbuilt tab or a first open of a dialog goes
|
returns null or a unit that throws leaves the holder and the scheduler able to carry on.
|
||||||
through this, and it logs the units and time it took.
|
The holder does not own the object; its wx parent does, as for any window. The holder has
|
||||||
- `build_step()` runs one unit of construction and returns true while more remain. The
|
no wx dependency and is unit-tested.
|
||||||
first unit is the factory call, and each later unit is one `StagedBuild` step if the
|
|
||||||
type has them.
|
|
||||||
- `when_built(fn)` runs `fn(object)` now if it exists, otherwise once it is built.
|
|
||||||
- `pending()` says whether the idle prebuild has work here: not built, and the factory
|
|
||||||
has not returned null. A null factory result is logged and the holder stays unbuilt.
|
|
||||||
- A unit that pumps the event loop cannot re-enter the holder; a nested `build_step()`
|
|
||||||
does nothing and a nested `ensure()` returns null.
|
|
||||||
- After a unit throws, the holder and the scheduler still run the next one.
|
|
||||||
|
|
||||||
The holder does not own the object; its wx parent does, as for any window. A type with
|
### The placeholder page: `LazyPage<Panel>`
|
||||||
one instance in the app derives from `LazyInstance<T>`, which points at that instance's
|
|
||||||
holder (the holder registers itself, and a recreated `MainFrame`'s holder replaces the
|
|
||||||
old frame's) and gives the type the static entry points the rest of the app uses,
|
|
||||||
`T::if_built()`, `T::ensure()` and `T::when_built(fn)`. They return null, or do
|
|
||||||
nothing, while no holder exists, so a caller needs no `mainframe` check.
|
|
||||||
|
|
||||||
`built()` is an atomic flag, since a job worker reads it through the statics
|
The notebook needs a page object for a tab to exist and for tabs to be inserted and
|
||||||
(`MainFrame::get_calibration_curr_tab()`); it is set after the object is complete.
|
removed by pointer, and the placeholder is that object. It builds the real panel inside
|
||||||
|
itself the first time it is shown and forwards showing and hiding afterwards, so a panel's
|
||||||
|
own show handling stays its activation hook. Nothing builds while the main window is
|
||||||
|
hidden; the window's first show builds the start page. A page that is out of the book is
|
||||||
|
not prebuilt. A panel built while its page is hidden stays hidden, and gets the theming
|
||||||
|
the window applied before the panel existed.
|
||||||
|
|
||||||
`LazyBase` is the holder's type-free interface (`name()`, `built()`, `pending()`,
|
### Staged construction: `StagedBuild`
|
||||||
`build_step()`, `prebuild_order()`) and is what the scheduler side sees.
|
|
||||||
|
|
||||||
### LazyPage<Panel>: the placeholder
|
A constructor too big to be one unit builds a skeleton and queues the rest as steps, which
|
||||||
|
run one per unit. A child panel's steps can be forwarded to its parent, and the parent is
|
||||||
|
complete only once the child is. Nothing may use what a step builds before the last step
|
||||||
|
has run, so staged panels follow these constraints:
|
||||||
|
|
||||||
A `wxPanel` placed in the parent in place of the real panel, and a `Lazy<Panel>` whose
|
- members created in steps start out null, so a partly built panel can be destroyed;
|
||||||
factory makes the panel inside it (by default `new Panel(parent)`). The notebook needs a
|
- timers, event handlers and destructors that touch step content check that the panel is
|
||||||
page object for the tab to exist and for `show_device()` to insert and remove tabs by
|
complete first;
|
||||||
pointer, and the placeholder is that object. `MainFrame` creates every page once, named
|
- nothing takes focus while off screen, since a unit may run while the user is typing
|
||||||
after its `TAB_ID_*`, and keeps it for the frame's life; `show_device()` only moves
|
|
||||||
pages in and out of the book.
|
|
||||||
|
|
||||||
- `Show()` is forwarded to the panel, so a panel's own `Show()` override stays its
|
|
||||||
activation hook (refresh timers, machine sync) and `SelectPageByName()` works
|
|
||||||
unchanged. A show builds the panel unless the frame itself is still hidden, because
|
|
||||||
the book selects its first page as it is inserted; `MainFrame::Show()` shows the
|
|
||||||
current page again when the frame becomes visible, which builds it.
|
|
||||||
- `in_book()` says whether the parent notebook currently lists the page, and
|
|
||||||
`pending()` is that and not built, so a tab `show_device()` has taken out of the book
|
|
||||||
is not prebuilt.
|
|
||||||
- A panel built while its page is hidden stays hidden, and a completed panel gets the
|
|
||||||
dark-mode pass the frame ran before it existed.
|
|
||||||
|
|
||||||
The Compare presets dialog is a holder without a page. `MainFrame` keeps a
|
|
||||||
`Lazy<DiffPresetDialog>` whose factory constructs the dialog and binds its events, the
|
|
||||||
dialog derives from `LazyInstance`, and its callers use
|
|
||||||
`DiffPresetDialog::ensure()->show()` and `DiffPresetDialog::if_built()` like a tab's
|
|
||||||
callers do. Saving a preset refreshes the dialog only while it is shown, since `show()`
|
|
||||||
reloads the presets.
|
|
||||||
|
|
||||||
### StagedBuild: construction in units
|
|
||||||
|
|
||||||
A mixin for a panel whose constructor is too big to be one unit. The constructor builds
|
|
||||||
the skeleton (sizers, and the parts other code may touch) and queues the rest with
|
|
||||||
`add_build_step()`. `build_step()` runs one step, and `add_build_steps_of(child)`
|
|
||||||
forwards a child's steps so a nested panel is spread the same way; the parent is built
|
|
||||||
only once the child is, including steps the child queues later. Steps run in order, on
|
|
||||||
the main thread. A `Lazy<T>` recognises a staged type at compile time and runs its steps
|
|
||||||
one per unit.
|
|
||||||
|
|
||||||
Nothing may touch what a step builds before the last step has run. In practice:
|
|
||||||
|
|
||||||
- nothing paints the panel before it is complete, since a panel built at idle is hidden
|
|
||||||
with its page and one built on demand finishes inside `ensure()` before the event loop
|
|
||||||
runs again;
|
|
||||||
- members created in steps are initialised to null in the header, so a partially built
|
|
||||||
panel can be destroyed;
|
|
||||||
- timers and event handlers that use step content check `built()` first
|
|
||||||
(`MonitorPanel::update_all()`, `CalibrationPanel::update_all()`), and a child's steps
|
|
||||||
are queued before anything in the constructor can fire such a handler;
|
|
||||||
- a destructor that disconnects from step content checks `built()` first;
|
|
||||||
- a constructor or step does not take focus while the panel is off screen
|
|
||||||
(`IsShownOnScreen()` before `SetFocus()`), since it may run while the user is typing
|
|
||||||
elsewhere;
|
elsewhere;
|
||||||
- where a step's widgets must keep their place in a sizer that later steps also fill, the
|
- a widget added by a step keeps its place in the sizer through an empty slot the skeleton
|
||||||
constructor adds an empty slot sizer in that position and the step fills the slot
|
creates.
|
||||||
(`StatusBasePanel`).
|
|
||||||
|
|
||||||
### IdleScheduler: when to build
|
### The scheduler: `IdleScheduler` and `PrebuildQueue`
|
||||||
|
|
||||||
A task is any `LazyBase`: a name for the log, `pending()`, `build_step()` and
|
The queue holds tasks in order, and a slice runs units of the first pending task until
|
||||||
`prebuild_order()`. `PrebuildQueue` holds the tasks by order (equal order in the order
|
the task finishes, the time budget is spent, or input arrives. It has no wx dependency and
|
||||||
added) and runs a slice, the units of the first pending task until it completes, the
|
is unit-tested with a fake clock. A task whose work goes away, such as a tab removed from
|
||||||
budget is spent on the clock it is given, or the interrupt predicate says input arrived.
|
the book, is skipped, and becomes pending again if the work comes back.
|
||||||
It has no wx dependency and is unit-tested with a fake clock. A task whose work is gone
|
|
||||||
is passed over and stays in the queue, so a tab that `show_device()` removes and later
|
|
||||||
re-inserts is pending again.
|
|
||||||
|
|
||||||
`IdleScheduler` drives the queue with the real clock, the app's input timestamp and the
|
A slice runs only once the user has been idle for a short quiet time, and each slice is its
|
||||||
Windows queue check, and logs each unit and slice. Each slice is a timer message: a
|
own timer message, so paint, timers and input queued in between are handled before the
|
||||||
period of 250 ms while waiting for the user to go quiet, and a one-shot of zero after a
|
next slice. Posting slices as pending events would not do that, because wx drains every
|
||||||
slice that left work, so the event loop dispatches whatever it has queued (paint,
|
pending event before the next native message. On GTK a timer that is always due starves
|
||||||
timers, input) before the next slice runs. Chaining slices with `CallAfter` would not do
|
the lower-priority sources that repaint and deliver posted events, so slices are a few
|
||||||
this: wx drains pending events fully before the next native message, on every platform.
|
milliseconds apart. On Windows a slice also waits while the native queue holds input,
|
||||||
On GTK the one-shot is 5 ms, because a due GLib timeout runs ahead of the redraw and
|
not counting mouse moves, which Windows synthesizes whenever a window appears under the
|
||||||
idle sources that paint and deliver posted events.
|
cursor. A slice never runs inside a `wxYield()`, where it would build pages in the middle of
|
||||||
A slice runs only once the user has been idle for the quiet time, and the timer stops
|
the code that yielded. A unit cannot be interrupted, so the largest unit bounds how long a
|
||||||
itself once no task is pending. The tick period, quiet time and slice length are
|
click can wait.
|
||||||
constants in `IdleScheduler.cpp`. A unit cannot be interrupted once started, so the
|
When nothing is pending the timer stops and the subsystem costs nothing.
|
||||||
largest unit bounds click latency on Windows; on the other platforms a click also waits
|
|
||||||
for the rest of the slice. A unit that pumps the event loop lets the timer fire inside
|
|
||||||
its own slice, and that tick does nothing.
|
|
||||||
|
|
||||||
The tasks are made by their owners. `MainFrame::prebuild_pages_when_idle()` registers
|
The main window owns the scheduler because it owns what the tasks build, and clearing the
|
||||||
every `LazyPage` the frame created, in or out of the book (`pending()` is false for a
|
queue with the window keeps a task from outliving its object. Each owner provides its own
|
||||||
page out of the book), the Compare presets holder, and the Prepare sidebar's settings
|
tasks, such as a tab, a dialog, the Prepare tab's settings page one option group at a
|
||||||
page from `ParamsPanel::settings_page_prebuild()`, whose first unit selects the default
|
time, the Prepare page's layout at the size the book gives its pages, or the 3D view's GL
|
||||||
tab if none is selected yet and whose later units build one option group each.
|
resources.
|
||||||
`show_device()` only restarts the timer. `MainFrame` owns the scheduler because it owns
|
|
||||||
everything the tasks build, and clearing the queue with the frame is what keeps a task
|
|
||||||
from outliving its object.
|
|
||||||
|
|
||||||
Idle time comes from `GUI_App::FilterEvent`, which timestamps mouse and keyboard events
|
### The 3D view's GL resources
|
||||||
(`wxEVT_CATEGORY_USER_INPUT` minus command events, which all claim that category), and
|
|
||||||
`GUI_App::input_idle_ms()` reports it. On Windows a slice additionally refuses to start
|
|
||||||
when the message queue holds keyboard, button, touch or pen input. Mouse moves are
|
|
||||||
excluded because Windows synthesises one whenever a window appears under the cursor,
|
|
||||||
which every unit does. Other platforms use the timestamp alone.
|
|
||||||
|
|
||||||
Once every registered page is built the timer is stopped and the subsystem costs
|
OpenGL is loaded on the Prepare tab's canvas. When the start page is not Prepare, loading
|
||||||
nothing.
|
it is an idle task that makes the context current on the hidden canvas, so the start page
|
||||||
|
paints first and Prepare never appears. Loading it on a shown canvas under `Freeze()` holds
|
||||||
|
back the start page's paint, and on GTK `Freeze()` cannot hide the canvas, which is a
|
||||||
|
native child window or a Wayland subsurface drawn outside GTK. A hidden Windows child
|
||||||
|
window keeps its device context, macOS attaches the context to a hidden view, and GTK
|
||||||
|
creates the canvas's surface when the widget is realized, so on GTK the task realizes the
|
||||||
|
canvas first. If the context cannot be made current, the canvas's first render loads the
|
||||||
|
resources.
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
**What builds before the first frame.** The Prepare tab's plater, because `post_init()`
|
**Before the first frame.** Only the start page and the Prepare tab's plater are built
|
||||||
needs its GL canvas on screen to initialise OpenGL in every startup state, and the start
|
before the first frame. Everything else goes through a holder.
|
||||||
page the user configured. Home is built by the first `MainFrame::Show()`, Prepare's
|
|
||||||
settings page by selecting the tab. `post_init()` passes through the Prepare tab for GL
|
|
||||||
init under `Freeze()` with `MainFrame::select_prepare_for_gl_init()`, which changes the
|
|
||||||
selection without the page-changed event, so nothing else is built for that pass.
|
|
||||||
|
|
||||||
**Reaching a lazy object.** A caller uses the type's own statics. `T::if_built()` may
|
**Reaching a lazy object.** Callers use the type's statics. Reading it if built is for
|
||||||
return null and is for telling the object something it can live without (a rescale, a
|
things the object can live without, such as a rescale, a color change or a status refresh.
|
||||||
colour change, a status update). `T::ensure()` builds the object and is for navigating
|
Making sure it exists is for navigating to it or showing it. Running something once it is
|
||||||
to it or for a caller that is about to show it. A caller that tells the object something
|
built is for state it would not fetch for itself on construction. A panel that pulls its
|
||||||
it would not fetch for itself on construction uses `T::when_built()`, which keeps the
|
state when constructed only ever needs to be read if built.
|
||||||
message until the object exists. A panel that pulls its state when constructed (the Home
|
|
||||||
page requests the recent list on load, the Device tab reads the device manager on show)
|
|
||||||
is reached with `if_built()`; one that cannot pull gets `when_built()`.
|
|
||||||
|
|
||||||
**Unit size.** A unit cannot be interrupted, so it should stay within the slice length on
|
**Unit size.** A unit should fit in one slice on a fast machine. A constructor over that is
|
||||||
a fast machine. A constructor above that is staged. A single widget above it is the
|
staged, and a single widget over it is accepted unless the widget itself can be split.
|
||||||
floor unless the widget itself is split.
|
|
||||||
|
|
||||||
**Order.** Cheapest and most likely to be opened first, given where `MainFrame` creates
|
**Order.** Tasks run cheapest and most likely to be opened first. Each holder's order is
|
||||||
each holder. The settings page is 0 (the Prepare tab's own content), Home 10, Device 20
|
given where it is created, with gaps so a new task fits between its neighbors. A negative
|
||||||
(a Bambu user's usual second stop), Calibration 30, Multi-device 40, the web Device
|
order is never prebuilt, for something few sessions open that costs more to build unasked
|
||||||
view 50, Project 60 (a second WebView2 instance) and the Compare presets dialog 100;
|
than it saves.
|
||||||
steps of ten so a new tab takes a value between its neighbours without renumbering
|
|
||||||
them. `MainFrame::prebuild_pages_when_idle()` registers the tasks once, from
|
|
||||||
`post_init()`.
|
|
||||||
|
|
||||||
**Never prebuilt.** A holder with a negative order, for a tab that few sessions open
|
|
||||||
and that costs more to build unasked than it saves (the Design tab), and plugin-provided
|
|
||||||
tabs, which are Python-side and not lazy pages.
|
|
||||||
|
|
||||||
## Adopting it
|
## Adopting it
|
||||||
|
|
||||||
A lazy tab needs:
|
A lazy tab needs a panel type deriving from `LazyInstance`, a placeholder page the main
|
||||||
|
window creates once with its order and registers for the idle build, and every use of the
|
||||||
|
panel outside the main window going through the statics. Its constructor has to cope with
|
||||||
|
the main window already existing and the user being busy elsewhere, so it takes no focus
|
||||||
|
while off screen, and it does all of its own setup, since the main window does nothing to a
|
||||||
|
panel after creating it.
|
||||||
|
|
||||||
1. The panel derived from `LazyInstance<Panel>`, since a tab's panel has one instance.
|
To stage a heavy constructor, keep the skeleton in the constructor, move the rest into
|
||||||
2. A `LazyPage<Panel>*` member in `MainFrame`, created once in `init_tabpanel()` with
|
steps in its original order, and follow the staged-construction constraints. Measure the
|
||||||
its `TAB_ID_*` as the name, its place by the order rule (and a factory if
|
units; a step that is still one big widget is split inside the widget or accepted.
|
||||||
`new Panel(parent)` is not enough), added to `m_lazy_pages`, and used wherever the
|
|
||||||
tab is added to or looked up in the notebook.
|
|
||||||
3. Every use of the panel outside `MainFrame` going through one of the panel's statics,
|
|
||||||
chosen by the rule above. When converting an existing member, `grep` for it; the
|
|
||||||
compiler finds the rest.
|
|
||||||
4. A constructor that copes with the frame already existing and the user being busy
|
|
||||||
elsewhere, since `wxGetApp().mainframe` is set, the frame may be shown, and the user
|
|
||||||
may be typing when a lazy panel is built: no `SetFocus()` while off screen, and
|
|
||||||
whatever the constructor did through a `MainFrame` accessor before (a mode update, a
|
|
||||||
deferred URL) done in the constructor itself.
|
|
||||||
|
|
||||||
To stage a heavy constructor, inherit `StagedBuild`, keep the skeleton in the constructor,
|
## Verifying
|
||||||
move the rest into `add_build_step()` lambdas in the original order, and follow the
|
|
||||||
staged-panel rules above. Measure the units. A step that is still one big widget has to
|
|
||||||
be split inside that widget or accepted as the floor.
|
|
||||||
|
|
||||||
Verification is by log. `MainFrame::prebuild_pages_when_idle` lists the queue it
|
The log lists the queue when it is registered, reports each completed task at info level
|
||||||
registered, `IdleScheduler::tick` reports each completed task by name at info level and
|
and each slice and unit at debug level, and reports every on-demand build with its units
|
||||||
each slice and unit at debug level, and `Lazy::ensure` reports an object a user built
|
and time. A task's completion line counts only the slice it finished in. A run from the
|
||||||
on demand with the units and time it took. A run from the configured start
|
configured start page should show every registered task complete in order, with no unit
|
||||||
page shows every registered page complete, in order, with no unit longer than intended.
|
longer than intended. A click on a tab during the idle build should show an on-demand
|
||||||
A click on a tab mid-prebuild shows the finished panel with an `ensure` line for what was
|
build for what was left, with the slices resuming once the user is idle again.
|
||||||
left, and the slices resume for the remaining tasks once the user is idle again.
|
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# Multiline infill — High Level Design
|
||||||
|
|
||||||
|
## Purpose and scope
|
||||||
|
|
||||||
|
`fill_multiline` prints every sparse infill wall as N adjacent lines instead of
|
||||||
|
one, so a wall is `d1 = N * spacing` thick. Only internal sparse infill uses it.
|
||||||
|
Each pattern first builds its single-line centerlines at N times the usual line
|
||||||
|
spacing (so the density holds), and `multiline_fill()` then replaces each
|
||||||
|
centerline by the lines of that wall: the centerline itself when N is odd, and
|
||||||
|
closed outlines around it at every `spacing` out to `d1 / 2`. The outlines are
|
||||||
|
clipped to the fill region contracted by half a line width, then connected like
|
||||||
|
any other infill.
|
||||||
|
|
||||||
|
Outlines of centerlines that cross each other overlap at every crossing, which
|
||||||
|
over-extrudes the wall intersections. The line-crossing patterns Grid,
|
||||||
|
Triangles, Tri-hexagon and Cubic therefore build centerlines that never cross
|
||||||
|
(`FillRectilinear::fill_surface_trapezoidal()`), and so do Adaptive Cubic and
|
||||||
|
Support Cubic (`FillAdaptive`); the other patterns outline their usual
|
||||||
|
centerlines.
|
||||||
|
|
||||||
|
## Non-crossing centerlines
|
||||||
|
|
||||||
|
The crossing lines are resolved into x-monotone paths, the levels of the line
|
||||||
|
arrangement: walking along x, the k-th path is always the k-th line from the
|
||||||
|
bottom. At every crossing, the two paths bounce off each other instead of
|
||||||
|
passing through. Adjacent paths meet only at crossings, so their outlines touch
|
||||||
|
there and nowhere overlap.
|
||||||
|
|
||||||
|
Where two paths meet, each is cut short by a line perpendicular to the bisector
|
||||||
|
of its bend, `d1 / 2` from the crossing. The two cut segments are parallel and
|
||||||
|
`d1` apart, so the outermost lines of the two walls sit exactly `spacing` apart,
|
||||||
|
like the lines inside a wall. Where three lines meet at one point, the middle
|
||||||
|
path runs straight through and the outer two are cut `d1` from it.
|
||||||
|
|
||||||
|
Each pattern builds its rows along x in a rotated frame. Grid lines run at ±45°
|
||||||
|
there, and its rows are trapezoid waves that transpose on alternate layers. The three families of Triangles, Tri-hexagon and
|
||||||
|
Cubic run at 0°, 60° and 120°. Those rows rotate by 120° every layer about a
|
||||||
|
3-fold center of the arrangement, so each family takes every role in turn.
|
||||||
|
The pattern is phased on fixed positions, so it lines up across layers and
|
||||||
|
across the regions of one layer. Rounding the corners with
|
||||||
|
`sparse_infill_smooth_factor` happens before `multiline_fill()`.
|
||||||
|
|
||||||
|
## Cubic
|
||||||
|
|
||||||
|
Single-line Cubic draws the three families at the same spacing `h` and shifts
|
||||||
|
them with z: by `+dx`, `-dx` and `+dx`, `dx = z / sqrt(2)`. The multiline paths
|
||||||
|
follow the same lines. In the frame where one family is horizontal, the other two
|
||||||
|
cross in rows `h` apart, alternating by half a period, at height
|
||||||
|
`tau = -3 * dx (mod h)` above the horizontal line below them. The crossings split
|
||||||
|
every band between horizontal lines into up-pointing triangles of height `tau`,
|
||||||
|
down-pointing triangles of height `h - tau`, and hexagons. At `tau = 0` (and `h`)
|
||||||
|
all three families meet at common points, as in Triangles. At `tau = h / 2` the
|
||||||
|
triangles are equal, as in Tri-hexagon. The origin of that frame is always a
|
||||||
|
3-fold center, whatever z is, so the per-layer rotation keeps the lines in place.
|
||||||
|
|
||||||
|
Each band holds two paths that touch at its crossings: the upper one takes the
|
||||||
|
V below the crossing and runs along the top horizontal line, and the lower one
|
||||||
|
takes the inverted V above it and runs along the bottom line. Both are the same function
|
||||||
|
of `tau`, the lower one mirrored with `h - tau`. `cubic_upper_level()` builds one
|
||||||
|
period of the upper path as the lower envelope of five lines, clipped from below:
|
||||||
|
|
||||||
|
- the two slanted lines through the crossings,
|
||||||
|
- the horizontal line, lowered when the triangle above it is less than `1.5 * d1` high,
|
||||||
|
- the two chamfers where the path turns onto and off the horizontal line, `d1 / 2`
|
||||||
|
from those crossings,
|
||||||
|
- the flat cut into the V at the crossing.
|
||||||
|
|
||||||
|
The cut height `clamp(tau - d1 / 2, 0, h - d1) + d1` is what makes the pattern
|
||||||
|
continuous in z. While both triangles are at least `1.5 * d1` high, every
|
||||||
|
crossing is a pair of bends `d1 / 2` from it, as in Tri-hexagon. When a triangle
|
||||||
|
is thinner, its three paths stack like a triple crossing. The path through it
|
||||||
|
flattens toward its base line and lies on it once the triangle is under `d1 / 2`
|
||||||
|
high, and the paths beside it are pushed `d1` away. The layout thus reaches the
|
||||||
|
Triangles one where the families meet. Adjacent paths stay at least `d1` apart
|
||||||
|
at every `tau` and at every density up to 100%.
|
||||||
|
|
||||||
|
## Adaptive Cubic
|
||||||
|
|
||||||
|
Adaptive Cubic and Support Cubic take their lines from an octree of cubes
|
||||||
|
standing on a corner. On each layer every cube cuts its three mid-planes into
|
||||||
|
segments of the same three 60° families as Cubic, but the pattern is not
|
||||||
|
periodic. Smaller cubes near the surface add finer lines, and a finer line ends
|
||||||
|
where it meets the wall of its coarser cube, so the lines form crossings and
|
||||||
|
T-junctions. `FillAdaptive::multiline_paths()` builds the paths from these
|
||||||
|
segments directly, for each fill region and within `4 * d1` of it.
|
||||||
|
|
||||||
|
At a crossing the two paths bounce as in Cubic. At a T-junction the through line
|
||||||
|
runs straight on and the path of the ending line stops there. Every path still
|
||||||
|
runs left to right in the frame where one family is horizontal, and that family
|
||||||
|
rotates with the layer.
|
||||||
|
|
||||||
|
Every line of every cube size lies on one fine lattice, so crossings closer than
|
||||||
|
a few `d1` are the corners of one small triangle of that lattice, as in Cubic.
|
||||||
|
The cuts follow the Cubic rules without a closed formula:
|
||||||
|
|
||||||
|
- The two bends of a crossing are cut `d1` apart, `d1 / 2` each, perpendicular
|
||||||
|
to their bisector, so their walls touch. A cut goes no further than the path
|
||||||
|
end, and the other bend takes the rest of `d1`.
|
||||||
|
- At the tip of a small triangle, between the two slanted families, a cut also
|
||||||
|
goes no further than the neighbouring bend turning the other way, and the
|
||||||
|
path beyond that bend is kept a wall away from it. The bends onto the
|
||||||
|
horizontal family are not limited this way: pushing their paths apart would
|
||||||
|
open gaps between walls that should touch.
|
||||||
|
- A cut moves the path only where the cut line lies beyond it, near its bend.
|
||||||
|
The sharp bends between the two slanted families are cut after the bends onto
|
||||||
|
the horizontal family, so the tip of a small triangle wins, as in Cubic.
|
||||||
|
- A path stopping at a T-junction is trimmed until it is `d1` less half a line
|
||||||
|
spacing from every other path, so that its end overlaps the wall it stops on
|
||||||
|
by half a line and bonds to it. The paths are trimmed one at a time against
|
||||||
|
the others as already trimmed, so two ends facing each other meet instead of
|
||||||
|
both backing off. A path stopping on the line of another is trimmed before
|
||||||
|
that one, so it gives way and the other still reaches the line it stops on. A
|
||||||
|
second round trims every path again from its full length, so an end grows
|
||||||
|
back where the ends it gave way to were trimmed later, and a last round only
|
||||||
|
shortens them, keeping them that far apart. Paths shorter than `d1` are left
|
||||||
|
out.
|
||||||
|
- A line that ends on another less than `2 * d1` past a crossing stops at that
|
||||||
|
crossing instead, the shorter one where both do. The path along such a stub
|
||||||
|
would be trimmed away, leaving a hole between the walls that were cut to
|
||||||
|
touch it.
|
||||||
|
|
||||||
|
Short paths enclosed by coarser lines still print as closed outlines, but most
|
||||||
|
paths run on across several cells.
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
<script type="text/javascript" src="../include/jquery-2.1.1.min.js"></script>
|
<script type="text/javascript" src="../include/jquery-2.1.1.min.js"></script>
|
||||||
<script type="text/javascript" src="../include/json2.js"></script>
|
<script type="text/javascript" src="../include/json2.js"></script>
|
||||||
<script type="text/javascript" src="../include/globalapi.js"></script>
|
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="../include/swiper/swiper-bundle.min.css" />
|
<link rel="stylesheet" type="text/css" href="../include/swiper/swiper-bundle.min.css" />
|
||||||
<script type="text/javascript" src="../include/swiper/swiper-bundle.min.js"></script>
|
<script type="text/javascript" src="../include/swiper/swiper-bundle.min.js"></script>
|
||||||
@@ -18,6 +17,7 @@
|
|||||||
<link rel="stylesheet" type="text/css" href="model.css" />
|
<link rel="stylesheet" type="text/css" href="model.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="./css/dark.css" />
|
<link rel="stylesheet" type="text/css" href="./css/dark.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="../include/global.css" /> <!-- ORCA One for all-->
|
<link rel="stylesheet" type="text/css" href="../include/global.css" /> <!-- ORCA One for all-->
|
||||||
|
<script type="text/javascript" src="../include/globalapi.js"></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="test.js"></script>
|
<script type="text/javascript" src="test.js"></script>
|
||||||
<script type="text/javascript" src="model.js"></script>
|
<script type="text/javascript" src="model.js"></script>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#include "../AABBTreeLines.hpp"
|
||||||
#include "../ClipperUtils.hpp"
|
#include "../ClipperUtils.hpp"
|
||||||
#include "../ExPolygon.hpp"
|
#include "../ExPolygon.hpp"
|
||||||
#include "../Surface.hpp"
|
#include "../Surface.hpp"
|
||||||
@@ -14,7 +15,9 @@
|
|||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <functional>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
|
#include <tuple>
|
||||||
|
|
||||||
// Boost pool: Don't use mutexes to synchronize memory allocation.
|
// Boost pool: Don't use mutexes to synchronize memory allocation.
|
||||||
#define BOOST_POOL_NO_MT
|
#define BOOST_POOL_NO_MT
|
||||||
@@ -1318,6 +1321,564 @@ bool has_no_collinear_lines(const Polylines &polylines)
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Non-crossing centerlines for multiline adaptive cubic, see docs/HLSD/multiline-infill.md.
|
||||||
|
namespace noncrossing {
|
||||||
|
|
||||||
|
// y = x() * x + y() in the frame where the sweep family is horizontal.
|
||||||
|
using Lin = Vec2d;
|
||||||
|
|
||||||
|
static const Vec2d family_dir[3] { Vec2d(1., 0.), Vec2d(0.5, 0.5 * sqrt(3.)), Vec2d(0.5, -0.5 * sqrt(3.)) };
|
||||||
|
|
||||||
|
struct SweepLine
|
||||||
|
{
|
||||||
|
Vec2d a, b; // a.x() < b.x()
|
||||||
|
int family;
|
||||||
|
Lin lin;
|
||||||
|
std::vector<std::pair<double, int>> junctions; // (x, junction)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Junction
|
||||||
|
{
|
||||||
|
Vec2d p;
|
||||||
|
std::vector<int> lines;
|
||||||
|
std::vector<std::pair<int, int>> pairs; // (left, right) line of each path through, bottom-up
|
||||||
|
std::vector<std::pair<int, int>> bends; // (path, bend) of each pair, -1 where it runs straight
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LevelPath
|
||||||
|
{
|
||||||
|
std::vector<Vec2d> verts; // start, bends, end
|
||||||
|
std::vector<int> lines; // line of each piece
|
||||||
|
std::vector<int> junctions; // junction of each bend
|
||||||
|
std::vector<int> turn; // 1 turning up, -1 turning down
|
||||||
|
std::vector<std::vector<Lin>> cuts;
|
||||||
|
std::vector<std::pair<Lin, int>> pushes; // (line, bend) keeping a wall away from a neighbour's cut
|
||||||
|
int start_term { -1 }; // junction where the path stops on another line, or -1
|
||||||
|
int end_term { -1 };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Moves f onto line c (side 1: from below) wherever c lies beyond it, over the stretches overlapping [w0, w1].
|
||||||
|
static void clip_profile(std::vector<Vec2d> &f, const Lin &c, int side, double w0, double w1, double lim0, double lim1, bool cut_at_window, bool drop_past_limits)
|
||||||
|
{
|
||||||
|
const double r0 = std::max(lim0, f.front().x()), r1 = std::min(lim1, f.back().x());
|
||||||
|
if (r1 <= r0)
|
||||||
|
return;
|
||||||
|
const size_t ia = std::upper_bound(f.begin(), f.end(), r0, [](double x, const Vec2d &p) { return x < p.x(); }) - f.begin();
|
||||||
|
const size_t ib = std::lower_bound(f.begin() + ia, f.end(), r1, [](const Vec2d &p, double x) { return p.x() < x; }) - f.begin();
|
||||||
|
auto interpolate = [](const Vec2d &a, const Vec2d &b, double x) { return b.x() > a.x() ? a.y() + (x - a.x()) / (b.x() - a.x()) * (b.y() - a.y()) : b.y(); };
|
||||||
|
std::vector<Vec2d> local{ Vec2d(r0, interpolate(f[ia - 1], f[ia], r0)) };
|
||||||
|
local.insert(local.end(), f.begin() + ia, f.begin() + ib);
|
||||||
|
local.emplace_back(r1, interpolate(f[ib - 1], f[ib], r1));
|
||||||
|
|
||||||
|
const double tol = 1.;
|
||||||
|
auto beyond = [&c, side, tol](const Vec2d &p) { return side * (c.x() * p.x() + c.y() - p.y()) - tol; };
|
||||||
|
std::vector<std::pair<double, double>> stretches;
|
||||||
|
auto add = [&stretches](double x0, double x1) {
|
||||||
|
if (!stretches.empty() && stretches.back().second >= x0)
|
||||||
|
stretches.back().second = x1;
|
||||||
|
else
|
||||||
|
stretches.emplace_back(x0, x1);
|
||||||
|
};
|
||||||
|
for (size_t i = 1; i < local.size(); ++i) {
|
||||||
|
const Vec2d &p = local[i - 1], &q = local[i];
|
||||||
|
if (q.x() <= p.x())
|
||||||
|
continue;
|
||||||
|
const double bp = beyond(p), bq = beyond(q);
|
||||||
|
if (bp > 0. && bq > 0.)
|
||||||
|
add(p.x(), q.x());
|
||||||
|
else if (bp > 0. || bq > 0.) {
|
||||||
|
const double x = p.x() + bp / (bp - bq) * (q.x() - p.x());
|
||||||
|
if (bp > 0.)
|
||||||
|
add(p.x(), x);
|
||||||
|
else
|
||||||
|
add(x, q.x());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::vector<std::pair<double, double>> keep;
|
||||||
|
for (auto [x0, x1] : stretches) {
|
||||||
|
if (x1 < w0 || x0 > w1)
|
||||||
|
continue;
|
||||||
|
if (drop_past_limits && ((x0 <= r0 && r0 == lim0) || (x1 >= r1 && r1 == lim1)))
|
||||||
|
continue;
|
||||||
|
keep.emplace_back(cut_at_window ? std::max(x0, w0) : x0, cut_at_window ? std::min(x1, w1) : x1);
|
||||||
|
}
|
||||||
|
if (keep.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto y_local = [&](double x) {
|
||||||
|
size_t i = 1;
|
||||||
|
while (i + 1 < local.size() && local[i].x() < x)
|
||||||
|
++i;
|
||||||
|
return interpolate(local[i - 1], local[i], x);
|
||||||
|
};
|
||||||
|
std::vector<Vec2d> out(f.begin(), f.begin() + ia);
|
||||||
|
auto push = [&out, tol](double x, double y) {
|
||||||
|
if (out.empty() || x > out.back().x() || std::abs(y - out.back().y()) > 2. * tol)
|
||||||
|
out.emplace_back(x, y);
|
||||||
|
};
|
||||||
|
size_t k = 0;
|
||||||
|
for (const Vec2d &p : local) {
|
||||||
|
for (; k < keep.size() && keep[k].second < p.x(); ++k) {
|
||||||
|
const auto [x0, x1] = keep[k];
|
||||||
|
push(x0, y_local(x0));
|
||||||
|
push(x0, c.x() * x0 + c.y());
|
||||||
|
push(x1, c.x() * x1 + c.y());
|
||||||
|
push(x1, y_local(x1));
|
||||||
|
}
|
||||||
|
if (k < keep.size() && keep[k].first <= p.x() && p.x() <= keep[k].second)
|
||||||
|
continue;
|
||||||
|
push(p.x(), p.y());
|
||||||
|
}
|
||||||
|
for (; k < keep.size(); ++k) {
|
||||||
|
const auto [x0, x1] = keep[k];
|
||||||
|
push(x0, y_local(x0));
|
||||||
|
push(x0, c.x() * x0 + c.y());
|
||||||
|
push(x1, c.x() * x1 + c.y());
|
||||||
|
push(x1, y_local(x1));
|
||||||
|
}
|
||||||
|
for (size_t i = ib; i < f.size(); ++i)
|
||||||
|
push(f[i].x(), f[i].y());
|
||||||
|
f = std::move(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<Vec2d> path_points(const LevelPath &path, const std::vector<SweepLine> &lines, double reach)
|
||||||
|
{
|
||||||
|
std::vector<Vec2d> f = path.verts;
|
||||||
|
const int nb = int(path.junctions.size());
|
||||||
|
auto x_of = [&path](int b) { return path.verts[b + 1].x(); };
|
||||||
|
auto sharp = [&](int b) { return lines[path.lines[b]].family != 0 && lines[path.lines[b + 1]].family != 0; };
|
||||||
|
// The run of bends turning the same way as bend b, up to the neighbouring bends turning the other way.
|
||||||
|
auto window = [&](int b) {
|
||||||
|
int l = b - 1, r = b + 1;
|
||||||
|
while (l >= 0 && path.turn[l] == path.turn[b])
|
||||||
|
--l;
|
||||||
|
while (r < nb && path.turn[r] == path.turn[b])
|
||||||
|
++r;
|
||||||
|
return std::make_pair(l >= 0 ? x_of(l) : f.front().x(), r < nb ? x_of(r) : f.back().x());
|
||||||
|
};
|
||||||
|
// Sharp bends between the slanted lines go last, so they win at the tip of a small triangle.
|
||||||
|
for (int b = 0; b < nb; ++b)
|
||||||
|
if (!sharp(b)) {
|
||||||
|
const auto [w0, w1] = window(b);
|
||||||
|
for (const Lin &c : path.cuts[b])
|
||||||
|
clip_profile(f, c, path.turn[b], w0, w1, x_of(b) - reach, x_of(b) + reach, true, false);
|
||||||
|
}
|
||||||
|
for (int b = 0; b < nb; ++b)
|
||||||
|
if (sharp(b))
|
||||||
|
for (const Lin &c : path.cuts[b])
|
||||||
|
clip_profile(f, c, path.turn[b], x_of(b), x_of(b), x_of(b) - reach, x_of(b) + reach, false, true);
|
||||||
|
for (const auto &[c, b] : path.pushes) {
|
||||||
|
const auto [w0, w1] = window(b);
|
||||||
|
clip_profile(f, c, path.turn[b], w0, w1, x_of(b) - reach, x_of(b) + reach, true, true);
|
||||||
|
}
|
||||||
|
std::vector<Vec2d> pts;
|
||||||
|
for (const Vec2d &p : f) {
|
||||||
|
while (pts.size() >= 2 && std::abs(cross2(Vec2d(pts.back() - pts[pts.size() - 2]), Vec2d(p - pts.back()))) <=
|
||||||
|
1e-9 * (pts.back() - pts[pts.size() - 2]).norm() * (p - pts.back()).norm())
|
||||||
|
pts.pop_back();
|
||||||
|
pts.push_back(p);
|
||||||
|
}
|
||||||
|
return pts;
|
||||||
|
}
|
||||||
|
|
||||||
|
static double polyline_length(const std::vector<Vec2d> &pts)
|
||||||
|
{
|
||||||
|
double len = 0.;
|
||||||
|
for (size_t i = 1; i < pts.size(); ++i)
|
||||||
|
len += (pts[i] - pts[i - 1]).norm();
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Point at the given distance along pts, and the index of the segment it lies on.
|
||||||
|
static std::pair<Vec2d, size_t> point_along(const std::vector<Vec2d> &pts, double t)
|
||||||
|
{
|
||||||
|
for (size_t i = 1; i < pts.size(); ++i) {
|
||||||
|
const double len = (pts[i] - pts[i - 1]).norm();
|
||||||
|
if (t <= len)
|
||||||
|
return { pts[i - 1] + (len > 0. ? t / len : 0.) * (pts[i] - pts[i - 1]), i };
|
||||||
|
t -= len;
|
||||||
|
}
|
||||||
|
return { pts.back(), pts.size() - 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace noncrossing
|
||||||
|
|
||||||
|
Polylines multiline_paths(const Lines &lines_in, double d1, double end_overlap, int sweep, const BoundingBox &cover)
|
||||||
|
{
|
||||||
|
using namespace noncrossing;
|
||||||
|
const double eps = scale_(0.002);
|
||||||
|
const Eigen::Rotation2Dd to_sweep(-sweep * M_PI / 3.);
|
||||||
|
const BoundingBoxf box(cover.min.cast<double>(), cover.max.cast<double>());
|
||||||
|
|
||||||
|
// Lines in the sweep frame, collinear pieces merged.
|
||||||
|
struct Piece { double c, s0, s1; };
|
||||||
|
std::array<std::vector<Piece>, 3> pieces;
|
||||||
|
for (const Line &line : lines_in) {
|
||||||
|
Vec2d a = line.a.cast<double>(), b = line.b.cast<double>();
|
||||||
|
if (!Geometry::liang_barsky_line_clipping(a, b, box) || (b - a).norm() < 10. * eps)
|
||||||
|
continue;
|
||||||
|
a = to_sweep * a;
|
||||||
|
b = to_sweep * b;
|
||||||
|
const double angle = std::atan2(b.y() - a.y(), b.x() - a.x()) / (M_PI / 3.);
|
||||||
|
if (std::abs(angle - std::round(angle)) > 0.01)
|
||||||
|
// Not one of the three families.
|
||||||
|
return {};
|
||||||
|
const int f = (int(std::round(angle)) % 3 + 3) % 3;
|
||||||
|
const Vec2d &d = family_dir[f];
|
||||||
|
const Vec2d n(-d.y(), d.x());
|
||||||
|
pieces[f].push_back({ n.dot(a), std::min(d.dot(a), d.dot(b)), std::max(d.dot(a), d.dot(b)) });
|
||||||
|
}
|
||||||
|
std::vector<SweepLine> lines;
|
||||||
|
for (int f = 0; f < 3; ++f) {
|
||||||
|
std::vector<Piece> &ps = pieces[f];
|
||||||
|
const Vec2d &d = family_dir[f];
|
||||||
|
const Vec2d n(-d.y(), d.x());
|
||||||
|
const double slope = d.y() / d.x();
|
||||||
|
std::sort(ps.begin(), ps.end(), [](const Piece &l, const Piece &r) { return l.c < r.c; });
|
||||||
|
for (size_t i = 0; i < ps.size();) {
|
||||||
|
size_t j = i + 1;
|
||||||
|
while (j < ps.size() && ps[j].c - ps[i].c < eps)
|
||||||
|
++j;
|
||||||
|
std::sort(ps.begin() + i, ps.begin() + j, [](const Piece &l, const Piece &r) { return l.s0 < r.s0; });
|
||||||
|
double c = 0.;
|
||||||
|
for (size_t k = i; k < j; ++k)
|
||||||
|
c += ps[k].c / double(j - i);
|
||||||
|
double s0 = ps[i].s0, s1 = ps[i].s1;
|
||||||
|
for (size_t k = i + 1; k <= j; ++k) {
|
||||||
|
if (k < j && ps[k].s0 <= s1 + eps) {
|
||||||
|
s1 = std::max(s1, ps[k].s1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const Vec2d a = s0 * d + c * n;
|
||||||
|
lines.push_back({ a, s1 * d + c * n, f, Lin(slope, a.y() - slope * a.x()), {} });
|
||||||
|
if (k < j) {
|
||||||
|
s0 = ps[k].s0;
|
||||||
|
s1 = ps[k].s1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i = j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto along = [](const SweepLine &l, const Vec2d &p) { return family_dir[l.family].dot(p - l.a); };
|
||||||
|
auto length = [](const SweepLine &l) { return (l.b - l.a).norm(); };
|
||||||
|
|
||||||
|
// Crossings, including the ends of lines stopping on another line.
|
||||||
|
std::vector<Junction> junctions;
|
||||||
|
auto detect_junctions = [&]() {
|
||||||
|
struct Hit { Vec2d p; int i, j; };
|
||||||
|
std::vector<Hit> hits;
|
||||||
|
std::vector<int> order(lines.size());
|
||||||
|
std::iota(order.begin(), order.end(), 0);
|
||||||
|
std::sort(order.begin(), order.end(), [&lines](int l, int r) { return lines[l].a.x() < lines[r].a.x(); });
|
||||||
|
for (size_t oi = 0; oi < order.size(); ++oi) {
|
||||||
|
const SweepLine &li = lines[order[oi]];
|
||||||
|
for (size_t oj = oi + 1; oj < order.size() && lines[order[oj]].a.x() <= li.b.x() + eps; ++oj) {
|
||||||
|
const SweepLine &lj = lines[order[oj]];
|
||||||
|
if (li.family == lj.family)
|
||||||
|
continue;
|
||||||
|
const double x = (lj.lin.y() - li.lin.y()) / (li.lin.x() - lj.lin.x());
|
||||||
|
const Vec2d p(x, li.lin.x() * x + li.lin.y());
|
||||||
|
const double ti = along(li, p), tj = along(lj, p);
|
||||||
|
if (ti > -eps && ti < length(li) + eps && tj > -eps && tj < length(lj) + eps)
|
||||||
|
hits.push_back({ p, order[oi], order[oj] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::sort(hits.begin(), hits.end(), [](const Hit &l, const Hit &r) { return l.p.x() < r.p.x(); });
|
||||||
|
junctions.clear();
|
||||||
|
for (const Hit &hit : hits) {
|
||||||
|
int found = -1;
|
||||||
|
for (int k = int(junctions.size()) - 1; k >= 0 && junctions[k].p.x() > hit.p.x() - eps; --k)
|
||||||
|
if (std::abs(junctions[k].p.y() - hit.p.y()) < eps) {
|
||||||
|
found = k;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (found < 0) {
|
||||||
|
found = int(junctions.size());
|
||||||
|
junctions.push_back({ hit.p, {}, {}, {} });
|
||||||
|
}
|
||||||
|
std::vector<int> &jl = junctions[found].lines;
|
||||||
|
for (int li : { hit.i, hit.j })
|
||||||
|
if (std::find(jl.begin(), jl.end(), li) == jl.end())
|
||||||
|
jl.push_back(li);
|
||||||
|
}
|
||||||
|
for (SweepLine &l : lines)
|
||||||
|
l.junctions.clear();
|
||||||
|
for (int ji = 0; ji < int(junctions.size()); ++ji)
|
||||||
|
for (int li : junctions[ji].lines)
|
||||||
|
lines[li].junctions.emplace_back(junctions[ji].p.x(), ji);
|
||||||
|
for (SweepLine &l : lines)
|
||||||
|
std::sort(l.junctions.begin(), l.junctions.end());
|
||||||
|
};
|
||||||
|
detect_junctions();
|
||||||
|
auto has_arm = [&](int ji, int li, bool right) {
|
||||||
|
const double t = along(lines[li], junctions[ji].p);
|
||||||
|
return right ? t < length(lines[li]) - eps : t > eps;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A line ending on another just past a crossing stops at the crossing, where its stub would leave a hole.
|
||||||
|
auto crosses = [&](int ji, int li) { return has_arm(ji, li, false) && has_arm(ji, li, true); };
|
||||||
|
for (int pass = 0; pass < 3; ++pass) {
|
||||||
|
std::vector<bool> touched(lines.size(), false);
|
||||||
|
bool changed = false;
|
||||||
|
for (int ji = 0; ji < int(junctions.size()); ++ji) {
|
||||||
|
const Junction &J = junctions[ji];
|
||||||
|
if (J.lines.size() != 2 || touched[J.lines[0]] || touched[J.lines[1]] || !crosses(ji, J.lines[0]) || !crosses(ji, J.lines[1]))
|
||||||
|
continue;
|
||||||
|
// Shortest arm of each line from J to the junction where it ends on another line.
|
||||||
|
struct DeadArm { double length; bool at_b; int end; };
|
||||||
|
std::array<DeadArm, 2> dead;
|
||||||
|
dead.fill({ std::numeric_limits<double>::max(), false, -1 });
|
||||||
|
for (int k = 0; k < 2; ++k) {
|
||||||
|
const SweepLine &l = lines[J.lines[k]];
|
||||||
|
const size_t at = std::find_if(l.junctions.begin(), l.junctions.end(), [ji](const std::pair<double, int> &j) { return j.second == ji; }) - l.junctions.begin();
|
||||||
|
const double t = along(l, J.p);
|
||||||
|
if (at + 1 < l.junctions.size() && length(l) - along(l, junctions[l.junctions[at + 1].second].p) < eps)
|
||||||
|
dead[k] = { length(l) - t, true, l.junctions[at + 1].second };
|
||||||
|
if (at > 0 && along(l, junctions[l.junctions[at - 1].second].p) < eps && t < dead[k].length)
|
||||||
|
dead[k] = { t, false, l.junctions[at - 1].second };
|
||||||
|
}
|
||||||
|
const int k = dead[0].length <= dead[1].length ? 0 : 1;
|
||||||
|
if (dead[k].length >= 2. * d1)
|
||||||
|
continue;
|
||||||
|
SweepLine &l = lines[J.lines[k]];
|
||||||
|
(dead[k].at_b ? l.b : l.a) = J.p;
|
||||||
|
// Only the shortened line and those it ended on have stale junctions until the next pass.
|
||||||
|
for (int li : junctions[dead[k].end].lines)
|
||||||
|
touched[li] = true;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (!changed)
|
||||||
|
break;
|
||||||
|
detect_junctions();
|
||||||
|
}
|
||||||
|
|
||||||
|
// At every crossing the lines bounce off each other, so that every path keeps running left to right.
|
||||||
|
for (int ji = 0; ji < int(junctions.size()); ++ji) {
|
||||||
|
Junction &J = junctions[ji];
|
||||||
|
std::vector<int> left, right;
|
||||||
|
for (int li : J.lines)
|
||||||
|
if (has_arm(ji, li, false) && has_arm(ji, li, true))
|
||||||
|
left.push_back(li);
|
||||||
|
right = left;
|
||||||
|
std::sort(left.begin(), left.end(), [&lines](int l, int r) { return lines[l].lin.x() > lines[r].lin.x(); });
|
||||||
|
std::sort(right.begin(), right.end(), [&lines](int l, int r) { return lines[l].lin.x() < lines[r].lin.x(); });
|
||||||
|
for (size_t k = 0; k < left.size(); ++k)
|
||||||
|
J.pairs.emplace_back(left[k], right[k]);
|
||||||
|
J.bends.assign(J.pairs.size(), { -1, -1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<LevelPath> paths;
|
||||||
|
for (int li = 0; li < int(lines.size()); ++li) {
|
||||||
|
const SweepLine &l = lines[li];
|
||||||
|
const int start = !l.junctions.empty() && !has_arm(l.junctions.front().second, li, false) ? l.junctions.front().second : -1;
|
||||||
|
LevelPath path;
|
||||||
|
path.start_term = start;
|
||||||
|
path.verts.push_back(start >= 0 ? junctions[start].p : l.a);
|
||||||
|
path.lines.push_back(li);
|
||||||
|
int cur = li;
|
||||||
|
double x = path.verts.front().x();
|
||||||
|
for (;;) {
|
||||||
|
const auto &js = lines[cur].junctions;
|
||||||
|
const auto it = std::find_if(js.begin(), js.end(), [x, eps](const std::pair<double, int> &j) { return j.first > x + 0.25 * eps; });
|
||||||
|
if (it == js.end()) {
|
||||||
|
path.verts.push_back(lines[cur].b);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Junction &J = junctions[it->second];
|
||||||
|
const size_t k = std::find_if(J.pairs.begin(), J.pairs.end(), [cur](const std::pair<int, int> &p) { return p.first == cur; }) - J.pairs.begin();
|
||||||
|
if (k == J.pairs.size()) {
|
||||||
|
path.verts.push_back(J.p);
|
||||||
|
path.end_term = it->second;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (const int next = J.pairs[k].second; next != cur) {
|
||||||
|
J.bends[k] = { int(paths.size()), int(path.junctions.size()) };
|
||||||
|
path.verts.push_back(J.p);
|
||||||
|
path.lines.push_back(next);
|
||||||
|
path.junctions.push_back(it->second);
|
||||||
|
path.turn.push_back(lines[next].lin.x() > lines[cur].lin.x() ? 1 : -1);
|
||||||
|
cur = next;
|
||||||
|
}
|
||||||
|
x = it->first;
|
||||||
|
}
|
||||||
|
path.cuts.resize(path.junctions.size());
|
||||||
|
paths.push_back(std::move(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cut the two bends of a crossing d1 apart, no further than the neighbouring bends turning the other way.
|
||||||
|
for (const Junction &J : junctions) {
|
||||||
|
if (J.pairs.size() < 2 || J.bends.front().first < 0 || J.bends.back().first < 0)
|
||||||
|
continue;
|
||||||
|
const Vec2d n = (family_dir[lines[J.pairs.back().second].family] - family_dir[lines[J.pairs.back().first].family]).normalized();
|
||||||
|
auto cut = [&J, &n](double offset) {
|
||||||
|
const Vec2d q = J.p + offset * n;
|
||||||
|
const double s = -n.x() / n.y();
|
||||||
|
return Lin(s, q.y() - s * q.x());
|
||||||
|
};
|
||||||
|
LevelPath &lo = paths[J.bends.front().first], &hi = paths[J.bends.back().first];
|
||||||
|
const int lb = J.bends.front().second, hb = J.bends.back().second;
|
||||||
|
if (J.pairs.size() == 3) {
|
||||||
|
lo.cuts[lb].push_back(cut(-d1));
|
||||||
|
hi.cuts[hb].push_back(cut(d1));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Only the tip of a small triangle, between the two slanted families, stops at its neighbouring bends.
|
||||||
|
const bool sharp = lines[J.pairs.front().first].family != 0 && lines[J.pairs.front().second].family != 0;
|
||||||
|
auto room = [&](const LevelPath &P, int b, int away, double sign) {
|
||||||
|
double c = std::numeric_limits<double>::max();
|
||||||
|
for (int nb : { b - 1, b + 1 })
|
||||||
|
if (sharp && nb >= 0 && nb < int(P.junctions.size()) && P.turn[nb] == away)
|
||||||
|
c = std::min(c, sign * n.dot(junctions[P.junctions[nb]].p - J.p));
|
||||||
|
if (b == 0 && P.start_term >= 0)
|
||||||
|
c = std::min(c, sign * n.dot(P.verts.front() - J.p));
|
||||||
|
if (b + 1 == int(P.junctions.size()) && P.end_term >= 0)
|
||||||
|
c = std::min(c, sign * n.dot(P.verts.back() - J.p));
|
||||||
|
return std::max(c, 0.);
|
||||||
|
};
|
||||||
|
const double c_lo = room(lo, lb, 1, -1.), c_hi = room(hi, hb, -1, 1.);
|
||||||
|
double d_lo = 0.5 * d1;
|
||||||
|
if (d1 - c_hi <= c_lo)
|
||||||
|
d_lo = std::clamp(d_lo, d1 - c_hi, c_lo);
|
||||||
|
else if (c_lo + c_hi > 0.)
|
||||||
|
d_lo = d1 * c_lo / (c_lo + c_hi);
|
||||||
|
const double d_hi = d1 - d_lo;
|
||||||
|
lo.cuts[lb].push_back(cut(-d_lo));
|
||||||
|
hi.cuts[hb].push_back(cut(d_hi));
|
||||||
|
auto propagate = [&](const LevelPath &P, int b, int away, int step, double offset) {
|
||||||
|
for (int nb : { b - 1, b + 1 })
|
||||||
|
if (nb >= 0 && nb < int(P.junctions.size()) && P.turn[nb] == away) {
|
||||||
|
const Junction &W = junctions[P.junctions[nb]];
|
||||||
|
const int k = int(std::find_if(W.pairs.begin(), W.pairs.end(), [&](const std::pair<int, int> &p) { return p.first == P.lines[nb]; }) - W.pairs.begin()) + step;
|
||||||
|
if (k >= 0 && k < int(W.bends.size()) && W.bends[k].first >= 0)
|
||||||
|
paths[W.bends[k].first].pushes.emplace_back(cut(offset), W.bends[k].second);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (sharp) {
|
||||||
|
propagate(lo, lb, 1, -1, -d_lo - d1);
|
||||||
|
propagate(hi, hb, -1, 1, d_hi + d1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::vector<Vec2d>> geometry;
|
||||||
|
Linesf segments;
|
||||||
|
std::vector<std::pair<int, double>> segment_start; // path, distance along it
|
||||||
|
std::vector<std::pair<double, double>> kept; // stretch of each path left by the trimming
|
||||||
|
for (const LevelPath &path : paths) {
|
||||||
|
geometry.push_back(path.junctions.empty() ? std::vector<Vec2d>{ path.verts.front(), path.verts.back() } : path_points(path, lines, 4. * d1));
|
||||||
|
double along_path = 0.;
|
||||||
|
for (size_t i = 1; i < geometry.back().size(); ++i) {
|
||||||
|
segments.emplace_back(geometry.back()[i - 1], geometry.back()[i]);
|
||||||
|
segment_start.emplace_back(int(geometry.size()) - 1, along_path);
|
||||||
|
along_path += (geometry.back()[i] - geometry.back()[i - 1]).norm();
|
||||||
|
}
|
||||||
|
kept.emplace_back(0., along_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A path ending on another line stops end_overlap inside the walls of the others, as trimmed so far.
|
||||||
|
const double end_clearance = d1 - end_overlap;
|
||||||
|
AABBTreeLines::LinesDistancer<Linef> tree(segments);
|
||||||
|
auto clearance = [&](int pi, const Vec2d &q) {
|
||||||
|
double dist = std::numeric_limits<double>::max();
|
||||||
|
for (size_t s : tree.all_lines_in_radius(q, d1)) {
|
||||||
|
const auto [pj, start] = segment_start[s];
|
||||||
|
const Vec2d a = segments[s].a, d = segments[s].b - a;
|
||||||
|
const double len = d.norm(), t0 = std::max(0., kept[pj].first - start), t1 = std::min(len, kept[pj].second - start);
|
||||||
|
if (pj != pi && len > 0. && t0 <= t1)
|
||||||
|
dist = std::min(dist, line_alg::distance_to(Linef(a + t0 / len * d, a + t1 / len * d), q));
|
||||||
|
}
|
||||||
|
return dist;
|
||||||
|
};
|
||||||
|
// Returns the length trimmed off.
|
||||||
|
auto trim_front = [&](int pi, std::vector<Vec2d> &pts) {
|
||||||
|
const double total = polyline_length(pts), step = d1 / 32.;
|
||||||
|
double t = 0.;
|
||||||
|
while (t <= total && clearance(pi, point_along(pts, t).first) < end_clearance)
|
||||||
|
t += step;
|
||||||
|
if (t > total) {
|
||||||
|
pts.clear();
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
if (t == 0.)
|
||||||
|
return 0.;
|
||||||
|
for (double lo = std::max(0., t - step); t - lo > step / 256.;)
|
||||||
|
if (const double mid = 0.5 * (lo + t); clearance(pi, point_along(pts, mid).first) < end_clearance)
|
||||||
|
lo = mid;
|
||||||
|
else
|
||||||
|
t = mid;
|
||||||
|
const auto [q, seg] = point_along(pts, t);
|
||||||
|
pts.erase(pts.begin(), pts.begin() + (seg - 1));
|
||||||
|
pts.front() = q;
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A path stopping on the line of another path is trimmed first, so that it gives way to that path.
|
||||||
|
std::vector<std::vector<std::tuple<double, double, int>>> carried(lines.size()); // x range and path of each piece
|
||||||
|
for (int pi = 0; pi < int(paths.size()); ++pi)
|
||||||
|
for (size_t i = 0; i < paths[pi].lines.size(); ++i)
|
||||||
|
carried[paths[pi].lines[i]].emplace_back(paths[pi].verts[i].x(), paths[pi].verts[i + 1].x(), pi);
|
||||||
|
std::vector<std::vector<int>> stopping_on(paths.size());
|
||||||
|
for (int pi = 0; pi < int(paths.size()); ++pi)
|
||||||
|
for (const auto &[ji, own] : { std::make_pair(paths[pi].start_term, paths[pi].lines.front()), std::make_pair(paths[pi].end_term, paths[pi].lines.back()) })
|
||||||
|
if (ji >= 0)
|
||||||
|
for (int li : junctions[ji].lines)
|
||||||
|
if (li != own)
|
||||||
|
for (const auto &[x0, x1, pj] : carried[li])
|
||||||
|
if (pj != pi && x0 - eps <= junctions[ji].p.x() && junctions[ji].p.x() <= x1 + eps)
|
||||||
|
stopping_on[pj].push_back(pi);
|
||||||
|
std::vector<int> order;
|
||||||
|
std::vector<bool> visited(paths.size(), false);
|
||||||
|
std::function<void(int)> visit = [&](int pi) {
|
||||||
|
if (visited[pi])
|
||||||
|
return;
|
||||||
|
visited[pi] = true;
|
||||||
|
for (int child : stopping_on[pi])
|
||||||
|
visit(child);
|
||||||
|
order.push_back(pi);
|
||||||
|
};
|
||||||
|
for (int pi = 0; pi < int(paths.size()); ++pi)
|
||||||
|
visit(pi);
|
||||||
|
std::vector<std::vector<Vec2d>> trimmed(paths.size());
|
||||||
|
auto trim = [&](int pi) {
|
||||||
|
std::vector<Vec2d> &pts = trimmed[pi];
|
||||||
|
if (paths[pi].start_term >= 0)
|
||||||
|
kept[pi].first += trim_front(pi, pts);
|
||||||
|
if (paths[pi].end_term >= 0 && !pts.empty()) {
|
||||||
|
std::reverse(pts.begin(), pts.end());
|
||||||
|
kept[pi].second -= trim_front(pi, pts);
|
||||||
|
std::reverse(pts.begin(), pts.end());
|
||||||
|
}
|
||||||
|
if (pts.size() < 2 || polyline_length(pts) < d1) {
|
||||||
|
pts.clear();
|
||||||
|
kept[pi] = { 0., -1. };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Ends grow back where the ends they gave way to were trimmed later; the last pass only shortens them.
|
||||||
|
for (int pass = 0; pass < 3; ++pass)
|
||||||
|
for (int pi : order) {
|
||||||
|
if (pass < 2) {
|
||||||
|
trimmed[pi] = geometry[pi];
|
||||||
|
kept[pi] = { 0., polyline_length(geometry[pi]) };
|
||||||
|
} else if (trimmed[pi].empty())
|
||||||
|
continue;
|
||||||
|
trim(pi);
|
||||||
|
}
|
||||||
|
|
||||||
|
Polylines out;
|
||||||
|
const Eigen::Rotation2Dd to_world = to_sweep.inverse();
|
||||||
|
for (const std::vector<Vec2d> &pts : trimmed) {
|
||||||
|
if (pts.empty())
|
||||||
|
continue;
|
||||||
|
Polyline pl;
|
||||||
|
for (const Vec2d &p : pts) {
|
||||||
|
const Vec2d w = to_world * p;
|
||||||
|
pl.points.emplace_back(coord_t(std::round(w.x())), coord_t(std::round(w.y())));
|
||||||
|
}
|
||||||
|
out.emplace_back(std::move(pl));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
void Filler::_fill_surface_single(
|
void Filler::_fill_surface_single(
|
||||||
const FillParams ¶ms,
|
const FillParams ¶ms,
|
||||||
unsigned int thickness_layers,
|
unsigned int thickness_layers,
|
||||||
@@ -1371,6 +1932,17 @@ void Filler::_fill_surface_single(
|
|||||||
all_polylines.reserve(lines.size());
|
all_polylines.reserve(lines.size());
|
||||||
std::transform(lines.begin(), lines.end(), std::back_inserter(all_polylines), [](const Line& l) { return Polyline{ l.a, l.b }; });
|
std::transform(lines.begin(), lines.end(), std::back_inserter(all_polylines), [](const Line& l) { return Polyline{ l.a, l.b }; });
|
||||||
|
|
||||||
|
if (params.multiline > 1) {
|
||||||
|
const double d1 = scale_(this->spacing) * params.multiline;
|
||||||
|
BoundingBox cover = get_extents(expolygon);
|
||||||
|
cover.offset(coord_t(4. * d1));
|
||||||
|
// Rotate the family the paths run along with the layer, like the other multiline patterns.
|
||||||
|
const int sweep = int((this->layer_id / std::max(thickness_layers, 1u)) % 3);
|
||||||
|
// Line ends overlap the walls they stop on by half a line, so that they bond.
|
||||||
|
if (Polylines paths = multiline_paths(lines, d1, 0.5 * scale_(this->spacing), sweep, cover); !paths.empty())
|
||||||
|
all_polylines = std::move(paths);
|
||||||
|
}
|
||||||
|
|
||||||
// Apply multiline offset if needed
|
// Apply multiline offset if needed
|
||||||
multiline_fill(all_polylines, params, spacing);
|
multiline_fill(all_polylines, params, spacing);
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ FillAdaptive::OctreePtr build_octree(
|
|||||||
// If true, octree is densified below internal overhangs only.
|
// If true, octree is densified below internal overhangs only.
|
||||||
bool support_overhangs_only);
|
bool support_overhangs_only);
|
||||||
|
|
||||||
|
// Multiline infill: lines of the three families to non-crossing paths d1 apart, ends reaching end_overlap into walls.
|
||||||
|
Polylines multiline_paths(const Lines &lines, double d1, double end_overlap, int sweep, const BoundingBox &cover);
|
||||||
|
|
||||||
//
|
//
|
||||||
// Some of the algorithms used by class FillAdaptive were inspired by
|
// Some of the algorithms used by class FillAdaptive were inspired by
|
||||||
// Cura Engine's class SubDivCube
|
// Cura Engine's class SubDivCube
|
||||||
|
|||||||
@@ -3047,12 +3047,56 @@ bool FillRectilinear::fill_surface_by_multilines(const Surface *surface, FillPar
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upper level of a cubic band [0, h] over one period, from the crossing at (0, tau) to the one at (period, tau).
|
||||||
|
// See docs/HLSD/multiline-infill.md.
|
||||||
|
static std::vector<Vec2d> cubic_upper_level(double tau, double h, double period, double d1)
|
||||||
|
{
|
||||||
|
const double s3 = std::sqrt(3.);
|
||||||
|
const double y_cut = std::clamp(tau - 0.5 * d1, 0., h - d1) + d1;
|
||||||
|
const double y_flat = std::min(h, h + y_cut - 2. * d1);
|
||||||
|
const double x2 = (h - tau) / s3 + d1;
|
||||||
|
const double x3 = (h + tau) / s3 - d1;
|
||||||
|
// (slope, intercept) of the rising line, its chamfer, the horizontal line, the falling chamfer and line.
|
||||||
|
const std::array<Vec2d, 5> lines{ Vec2d(s3, tau), Vec2d(1. / s3, h - x2 / s3), Vec2d(0., y_flat),
|
||||||
|
Vec2d(-1. / s3, h + x3 / s3), Vec2d(-s3, tau + s3 * period) };
|
||||||
|
auto y_at = [&lines, y_cut](double x) {
|
||||||
|
double y = std::numeric_limits<double>::max();
|
||||||
|
for (const Vec2d &l : lines)
|
||||||
|
y = std::min(y, l.x() * x + l.y());
|
||||||
|
return std::max(y, y_cut);
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<double> xs;
|
||||||
|
for (size_t i = 0; i < lines.size(); ++i) {
|
||||||
|
if (lines[i].x() != 0.)
|
||||||
|
xs.emplace_back((y_cut - lines[i].y()) / lines[i].x());
|
||||||
|
for (size_t j = i + 1; j < lines.size(); ++j)
|
||||||
|
xs.emplace_back((lines[j].y() - lines[i].y()) / (lines[i].x() - lines[j].x()));
|
||||||
|
}
|
||||||
|
xs.erase(std::remove_if(xs.begin(), xs.end(), [period](double x) { return x <= 1. || x >= period - 1.; }), xs.end());
|
||||||
|
xs.insert(xs.end(), { 0., period });
|
||||||
|
std::sort(xs.begin(), xs.end());
|
||||||
|
xs.erase(std::unique(xs.begin(), xs.end(), [](double a, double b) { return b - a < 1.; }), xs.end());
|
||||||
|
|
||||||
|
std::vector<Vec2d> pts;
|
||||||
|
for (double x : xs) {
|
||||||
|
const Vec2d p(x, y_at(x));
|
||||||
|
if (pts.size() >= 2) {
|
||||||
|
const Vec2d &a = pts[pts.size() - 2], &b = pts.back();
|
||||||
|
if (std::abs((b.y() - a.y()) / (b.x() - a.x()) - (p.y() - b.y()) / (p.x() - b.x())) < EPSILON)
|
||||||
|
pts.pop_back();
|
||||||
|
}
|
||||||
|
pts.emplace_back(p);
|
||||||
|
}
|
||||||
|
return pts;
|
||||||
|
}
|
||||||
|
|
||||||
bool FillRectilinear::fill_surface_trapezoidal(
|
bool FillRectilinear::fill_surface_trapezoidal(
|
||||||
const Surface* surface,
|
const Surface* surface,
|
||||||
FillParams params,
|
FillParams params,
|
||||||
const std::initializer_list<SweepParams>& sweep_params,
|
const std::initializer_list<SweepParams>& sweep_params,
|
||||||
Polylines& polylines_out,
|
Polylines& polylines_out,
|
||||||
int Pattern_type) // 0=grid, 1=triangular, 2=stars
|
int Pattern_type) // 0=grid, 1=triangular, 2=stars, 3=cubic
|
||||||
{
|
{
|
||||||
assert(params.multiline > 1);
|
assert(params.multiline > 1);
|
||||||
|
|
||||||
@@ -3350,6 +3394,55 @@ bool FillRectilinear::fill_surface_trapezoidal(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 3: // Cubic
|
||||||
|
{
|
||||||
|
// Same z shifted lines as the single-line cubic; the slanted ones cross tau above the horizontal ones.
|
||||||
|
auto pos_mod = [](double a, double m) { const double r = std::fmod(a, m); return r < 0. ? r + m : r; };
|
||||||
|
const double h = 0.5 * std::sqrt(3.0) * period;
|
||||||
|
const double shift = scale_(std::sqrt(0.5) * this->z);
|
||||||
|
const double tau = pos_mod(-3. * shift, h);
|
||||||
|
const double y0 = pos_mod(-2. * shift, 2. * h);
|
||||||
|
|
||||||
|
std::array<std::vector<Vec2d>, 2> levels{ cubic_upper_level(h - tau, h, period, d1), cubic_upper_level(tau, h, period, d1) };
|
||||||
|
for (Vec2d &p : levels.front())
|
||||||
|
p.y() = h - p.y();
|
||||||
|
|
||||||
|
const size_t layer_mod = infill_layer_id % 3;
|
||||||
|
const double angle = layer_mod * 2.0 * M_PI / 3.0;
|
||||||
|
|
||||||
|
// Only cover the surface, seen in the frame the pattern is built in.
|
||||||
|
ExPolygon local = expolygon;
|
||||||
|
local.translate(-rotate_vector.second.x(), -rotate_vector.second.y());
|
||||||
|
if (layer_mod)
|
||||||
|
local.rotate(-angle);
|
||||||
|
BoundingBox cover = get_extents(local);
|
||||||
|
cover.offset(period);
|
||||||
|
|
||||||
|
const int64_t n_min = int64_t(std::floor((cover.min.y() - y0) / h)) - 1;
|
||||||
|
const int64_t n_max = int64_t(std::ceil((cover.max.y() - y0) / h)) + 1;
|
||||||
|
for (int64_t n = n_min; n <= n_max; ++n) {
|
||||||
|
const double x_off = (n & 1) ? 0.5 * period : 0.;
|
||||||
|
const double base = y0 + double(n) * h - tau;
|
||||||
|
const int64_t j_min = int64_t(std::floor((cover.min.x() - x_off) / period)) - 1;
|
||||||
|
const int64_t j_max = int64_t(std::ceil((cover.max.x() - x_off) / period));
|
||||||
|
for (const std::vector<Vec2d> &level : levels) {
|
||||||
|
Polyline row;
|
||||||
|
row.points.reserve(size_t(j_max - j_min + 1) * level.size());
|
||||||
|
for (int64_t j = j_min; j <= j_max; ++j)
|
||||||
|
for (size_t i = (j == j_min) ? 0 : 1; i < level.size(); ++i)
|
||||||
|
row.points.emplace_back(coord_t(std::round(x_off + double(j * period) + level[i].x())),
|
||||||
|
coord_t(std::round(base + level[i].y())));
|
||||||
|
polylines.emplace_back(std::move(row));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer_mod)
|
||||||
|
for (Polyline &pl : polylines)
|
||||||
|
pl.rotate(angle, Point(0, 0));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Handle unknown pattern type
|
// Handle unknown pattern type
|
||||||
break;
|
break;
|
||||||
@@ -3532,6 +3625,11 @@ Polylines FillStars::fill_surface(const Surface *surface, const FillParams ¶
|
|||||||
Polylines FillCubic::fill_surface(const Surface *surface, const FillParams ¶ms)
|
Polylines FillCubic::fill_surface(const Surface *surface, const FillParams ¶ms)
|
||||||
{
|
{
|
||||||
Polylines polylines_out;
|
Polylines polylines_out;
|
||||||
|
if (params.multiline > 1) {
|
||||||
|
if (!this->fill_surface_trapezoidal(surface, params, {}, polylines_out, 3))
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "FillCubic::fill_surface_trapezoidal() failed.";
|
||||||
|
return polylines_out;
|
||||||
|
}
|
||||||
coordf_t dx = sqrt(0.5) * z;
|
coordf_t dx = sqrt(0.5) * z;
|
||||||
if (! this->fill_surface_by_multilines(
|
if (! this->fill_surface_by_multilines(
|
||||||
surface, params,
|
surface, params,
|
||||||
|
|||||||
@@ -2256,13 +2256,45 @@ void PerimeterGenerator::process_no_bridge(Surfaces& all_surfaces, coord_t perim
|
|||||||
|
|
||||||
// ORCA:
|
// ORCA:
|
||||||
// Inner Outer Inner wall ordering mode perimeter order optimisation functions
|
// Inner Outer Inner wall ordering mode perimeter order optimisation functions
|
||||||
|
|
||||||
|
// Whether two Arachne lines touch: somewhere the gap between their centrelines is no more than the
|
||||||
|
// touching distance there. Each junction of one line is measured against the segments of the other,
|
||||||
|
// both ways, and the search stops at the first spot that touches.
|
||||||
|
// Arachne varies line width to fill the region (e.g. the odd centre line of a narrow wall is wider
|
||||||
|
// than nominal), so the touching distance is half the combined width at the closest points, not the
|
||||||
|
// nominal spacing. Widths are taken locally so a line widened in one place (a wedge tip, a wall
|
||||||
|
// transition) does not count as touching where it passes close to other perimeters. min_threshold keeps
|
||||||
|
// the nominal spacing threshold as the lower bound.
|
||||||
|
static bool arachne_lines_touch(const Arachne::ExtrusionLine &a, const Arachne::ExtrusionLine &b, double min_threshold)
|
||||||
|
{
|
||||||
|
auto one_way = [min_threshold](const Arachne::ExtrusionLine &from, const Arachne::ExtrusionLine &to) {
|
||||||
|
for (const Arachne::ExtrusionJunction &j : from.junctions) {
|
||||||
|
const Vec2d p = j.p.cast<double>();
|
||||||
|
for (size_t k = 0; k + 1 < to.junctions.size(); ++k) {
|
||||||
|
const Arachne::ExtrusionJunction &j0 = to.junctions[k];
|
||||||
|
const Arachne::ExtrusionJunction &j1 = to.junctions[k + 1];
|
||||||
|
const Vec2d s0 = j0.p.cast<double>();
|
||||||
|
const Vec2d seg = j1.p.cast<double>() - s0;
|
||||||
|
const double l2 = seg.squaredNorm();
|
||||||
|
const double t = l2 > 0. ? std::clamp((p - s0).dot(seg) / l2, 0., 1.) : 0.;
|
||||||
|
const double w = double(j0.w) + t * double(j1.w - j0.w); // width of `to` at the closest point
|
||||||
|
const double touch_distance = std::max(min_threshold, 0.5 * (double(j.w) + w));
|
||||||
|
if ((s0 + t * seg - p).norm() <= touch_distance)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
return one_way(a, b) || one_way(b, a);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Finds all perimeters touching a given set of reference lines, given as indexes.
|
* @brief Finds all perimeters touching a given set of reference lines, given as indexes.
|
||||||
*
|
*
|
||||||
* @param entities The list of PerimeterGeneratorArachneExtrusion entities.
|
* @param entities The list of PerimeterGeneratorArachneExtrusion entities.
|
||||||
* @param referenceIndices A set of indices representing the reference points.
|
* @param referenceIndices A set of indices representing the reference points.
|
||||||
* @param threshold_external The distance threshold to consider for proximity for a reference perimeter with inset index 0
|
* @param threshold_external The minimum touching distance for a reference perimeter with inset index 0
|
||||||
* @param threshold_internal The distance threshold to consider for proximity for a reference perimeter with inset index 1+
|
* @param threshold_internal The minimum touching distance for a reference perimeter with inset index 1+
|
||||||
* @param considered_inset_idx What perimeter inset index are we searching for (eg. if we are searching for first internal perimeters proximate to the current reference perimeter, this value should be set to 1 etc).
|
* @param considered_inset_idx What perimeter inset index are we searching for (eg. if we are searching for first internal perimeters proximate to the current reference perimeter, this value should be set to 1 etc).
|
||||||
* @return std::vector<int> A vector of indices representing the touching perimeters.
|
* @return std::vector<int> A vector of indices representing the touching perimeters.
|
||||||
*/
|
*/
|
||||||
@@ -2271,7 +2303,6 @@ std::vector<int> findAllTouchingPerimeters(const std::vector<PerimeterGeneratorA
|
|||||||
|
|
||||||
for (const int refIdx : referenceIndices) {
|
for (const int refIdx : referenceIndices) {
|
||||||
const auto& referenceEntity = entities[refIdx];
|
const auto& referenceEntity = entities[refIdx];
|
||||||
Points referencePoints = Arachne::to_points(*referenceEntity.extrusion);
|
|
||||||
for (size_t i = 0; i < entities.size(); ++i) {
|
for (size_t i = 0; i < entities.size(); ++i) {
|
||||||
// Skip already considered references and the reference entity
|
// Skip already considered references and the reference entity
|
||||||
if (referenceIndices.count(i) > 0) continue;
|
if (referenceIndices.count(i) > 0) continue;
|
||||||
@@ -2282,15 +2313,9 @@ std::vector<int> findAllTouchingPerimeters(const std::vector<PerimeterGeneratorA
|
|||||||
continue; // skip if they dont match
|
continue; // skip if they dont match
|
||||||
}
|
}
|
||||||
|
|
||||||
Points points = Arachne::to_points(*entity.extrusion);
|
// Add to touchingIndices if the lines touch.
|
||||||
double distance = MultiPoint::minimumDistanceBetweenLinesDefinedByPoints(referencePoints, points);
|
const double threshold = double(referenceEntity.extrusion->inset_idx == 0 ? threshold_external : threshold_internal);
|
||||||
// Add to touchingIndices if within threshold distance
|
if (arachne_lines_touch(*referenceEntity.extrusion, *entity.extrusion, threshold)) {
|
||||||
size_t threshold=0;
|
|
||||||
if(referenceEntity.extrusion->inset_idx == 0)
|
|
||||||
threshold = threshold_external;
|
|
||||||
else
|
|
||||||
threshold = threshold_internal;
|
|
||||||
if (distance <= threshold) {
|
|
||||||
touchingIndices.insert(i);
|
touchingIndices.insert(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ inline bool is_smoothable_infill_pattern(InfillPattern pattern, int multiline =
|
|||||||
case ipGrid:
|
case ipGrid:
|
||||||
case ipTriangles:
|
case ipTriangles:
|
||||||
case ipStars:
|
case ipStars:
|
||||||
|
case ipCubic:
|
||||||
return multiline > 1;
|
return multiline > 1;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1088,8 +1088,6 @@ wxDEFINE_EVENT(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED, SimpleEvent);
|
|||||||
wxDEFINE_EVENT(EVT_GLCANVAS_UPDATE_BED_SHAPE, SimpleEvent);
|
wxDEFINE_EVENT(EVT_GLCANVAS_UPDATE_BED_SHAPE, SimpleEvent);
|
||||||
wxDEFINE_EVENT(EVT_GLCANVAS_TAB, SimpleEvent);
|
wxDEFINE_EVENT(EVT_GLCANVAS_TAB, SimpleEvent);
|
||||||
wxDEFINE_EVENT(EVT_GLCANVAS_RESETGIZMOS, SimpleEvent);
|
wxDEFINE_EVENT(EVT_GLCANVAS_RESETGIZMOS, SimpleEvent);
|
||||||
wxDEFINE_EVENT(EVT_GLCANVAS_MOVE_SLIDERS, wxKeyEvent);
|
|
||||||
wxDEFINE_EVENT(EVT_GLCANVAS_JUMP_TO, wxKeyEvent);
|
|
||||||
wxDEFINE_EVENT(EVT_GLCANVAS_UNDO, SimpleEvent);
|
wxDEFINE_EVENT(EVT_GLCANVAS_UNDO, SimpleEvent);
|
||||||
wxDEFINE_EVENT(EVT_GLCANVAS_REDO, SimpleEvent);
|
wxDEFINE_EVENT(EVT_GLCANVAS_REDO, SimpleEvent);
|
||||||
wxDEFINE_EVENT(EVT_GLCANVAS_SWITCH_TO_OBJECT, SimpleEvent);
|
wxDEFINE_EVENT(EVT_GLCANVAS_SWITCH_TO_OBJECT, SimpleEvent);
|
||||||
@@ -7283,6 +7281,16 @@ void GLCanvas3D::_resize(unsigned int w, unsigned int h)
|
|||||||
m_last_w = w;
|
m_last_w = w;
|
||||||
m_last_h = h;
|
m_last_h = h;
|
||||||
|
|
||||||
|
set_imgui_scaling();
|
||||||
|
|
||||||
|
this->request_extra_frame();
|
||||||
|
|
||||||
|
// ensures that this canvas is current
|
||||||
|
_set_current();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GLCanvas3D::set_imgui_scaling()
|
||||||
|
{
|
||||||
float font_size = wxGetApp().em_unit();
|
float font_size = wxGetApp().em_unit();
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
@@ -7295,15 +7303,10 @@ void GLCanvas3D::_resize(unsigned int w, unsigned int h)
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if ENABLE_RETINA_GL
|
#if ENABLE_RETINA_GL
|
||||||
imgui->set_scaling(font_size, 1.0f, m_retina_helper->get_scale_factor());
|
wxGetApp().imgui()->set_scaling(font_size, 1.0f, m_retina_helper->get_scale_factor());
|
||||||
#else
|
#else
|
||||||
imgui->set_scaling(font_size, m_canvas->GetContentScaleFactor(), 1.0f);
|
wxGetApp().imgui()->set_scaling(font_size, m_canvas->GetContentScaleFactor(), 1.0f);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
this->request_extra_frame();
|
|
||||||
|
|
||||||
// ensures that this canvas is current
|
|
||||||
_set_current();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BoundingBoxf3 GLCanvas3D::_max_bounding_box(bool include_gizmos, bool include_bed_model, bool include_plates) const
|
BoundingBoxf3 GLCanvas3D::_max_bounding_box(bool include_gizmos, bool include_bed_model, bool include_plates) const
|
||||||
|
|||||||
@@ -185,8 +185,6 @@ wxDECLARE_EVENT(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED, SimpleEvent);
|
|||||||
wxDECLARE_EVENT(EVT_GLCANVAS_UPDATE_BED_SHAPE, SimpleEvent);
|
wxDECLARE_EVENT(EVT_GLCANVAS_UPDATE_BED_SHAPE, SimpleEvent);
|
||||||
wxDECLARE_EVENT(EVT_GLCANVAS_TAB, SimpleEvent);
|
wxDECLARE_EVENT(EVT_GLCANVAS_TAB, SimpleEvent);
|
||||||
wxDECLARE_EVENT(EVT_GLCANVAS_RESETGIZMOS, SimpleEvent);
|
wxDECLARE_EVENT(EVT_GLCANVAS_RESETGIZMOS, SimpleEvent);
|
||||||
wxDECLARE_EVENT(EVT_GLCANVAS_MOVE_SLIDERS, wxKeyEvent);
|
|
||||||
wxDECLARE_EVENT(EVT_GLCANVAS_JUMP_TO, wxKeyEvent);
|
|
||||||
wxDECLARE_EVENT(EVT_GLCANVAS_UNDO, SimpleEvent);
|
wxDECLARE_EVENT(EVT_GLCANVAS_UNDO, SimpleEvent);
|
||||||
wxDECLARE_EVENT(EVT_GLCANVAS_REDO, SimpleEvent);
|
wxDECLARE_EVENT(EVT_GLCANVAS_REDO, SimpleEvent);
|
||||||
wxDECLARE_EVENT(EVT_GLCANVAS_SWITCH_TO_OBJECT, SimpleEvent);
|
wxDECLARE_EVENT(EVT_GLCANVAS_SWITCH_TO_OBJECT, SimpleEvent);
|
||||||
@@ -1301,6 +1299,8 @@ public:
|
|||||||
Vec3d _mouse_to_3d(const Point& mouse_pos, float* z = nullptr);
|
Vec3d _mouse_to_3d(const Point& mouse_pos, float* z = nullptr);
|
||||||
|
|
||||||
bool make_current_for_postinit();
|
bool make_current_for_postinit();
|
||||||
|
// Sizes ImGui's fonts and style for this canvas; the fonts are rebuilt when the size changes.
|
||||||
|
void set_imgui_scaling();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool _is_shown_on_screen() const;
|
bool _is_shown_on_screen() const;
|
||||||
|
|||||||
@@ -857,7 +857,12 @@ void GUI_App::post_init()
|
|||||||
slow_bootup = true;
|
slow_bootup = true;
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", slow bootup, won't render gl here.";
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", slow bootup, won't render gl here.";
|
||||||
}
|
}
|
||||||
if (!switch_to_3d) {
|
// Starting on Home, the GL resources load at idle so Home paints first and Prepare is never
|
||||||
|
// shown.
|
||||||
|
const bool gl_at_idle = !starts_on_prepare() && is_editor();
|
||||||
|
if (!switch_to_3d && gl_at_idle) {
|
||||||
|
plater_->select_view_3D("3D");
|
||||||
|
} else if (!switch_to_3d) {
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", begin load_gl_resources";
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", begin load_gl_resources";
|
||||||
#ifndef __linux__
|
#ifndef __linux__
|
||||||
mainframe->Freeze();
|
mainframe->Freeze();
|
||||||
@@ -865,9 +870,6 @@ void GUI_App::post_init()
|
|||||||
plater_->canvas3D()->enable_render(false);
|
plater_->canvas3D()->enable_render(false);
|
||||||
mainframe->select_prepare_for_gl_init();
|
mainframe->select_prepare_for_gl_init();
|
||||||
plater_->select_view_3D("3D");
|
plater_->select_view_3D("3D");
|
||||||
// The first render happens before the queued new_project() sets the same view.
|
|
||||||
plater_->get_camera().select_view("topfront");
|
|
||||||
plater_->get_camera().requires_zoom_to_bed = true;
|
|
||||||
//BBS init the opengl resource here
|
//BBS init the opengl resource here
|
||||||
if (!plater_->canvas3D()->get_wxglcanvas()->IsShownOnScreen() ||
|
if (!plater_->canvas3D()->get_wxglcanvas()->IsShownOnScreen() ||
|
||||||
!plater_->canvas3D()->make_current_for_postinit()) {
|
!plater_->canvas3D()->make_current_for_postinit()) {
|
||||||
@@ -905,8 +907,6 @@ void GUI_App::post_init()
|
|||||||
}
|
}
|
||||||
if (starts_on_prepare())
|
if (starts_on_prepare())
|
||||||
mainframe->select_tab(TAB_ID_PREPARE);
|
mainframe->select_tab(TAB_ID_PREPARE);
|
||||||
else if (is_editor())
|
|
||||||
mainframe->select_tab(TAB_ID_HOME);
|
|
||||||
#ifndef __linux__
|
#ifndef __linux__
|
||||||
mainframe->Thaw();
|
mainframe->Thaw();
|
||||||
#endif
|
#endif
|
||||||
@@ -3423,6 +3423,10 @@ bool GUI_App::on_init_inner()
|
|||||||
}
|
}
|
||||||
BOOST_LOG_TRIVIAL(info) << "create the main window";
|
BOOST_LOG_TRIVIAL(info) << "create the main window";
|
||||||
mainframe = new MainFrame();
|
mainframe = new MainFrame();
|
||||||
|
// The first render can happen as soon as the frame is shown, before the queued
|
||||||
|
// new_project() sets the same view.
|
||||||
|
plater_->get_camera().select_view("topfront");
|
||||||
|
plater_->get_camera().requires_zoom_to_bed = true;
|
||||||
if (is_editor()) {
|
if (is_editor()) {
|
||||||
if (starts_on_prepare()) {
|
if (starts_on_prepare()) {
|
||||||
mainframe->select_tab(TAB_ID_PREPARE);
|
mainframe->select_tab(TAB_ID_PREPARE);
|
||||||
@@ -8201,10 +8205,12 @@ int GUI_App::input_idle_ms() const
|
|||||||
return int(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - m_last_input).count());
|
return int(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - m_last_input).count());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every wxCommandEvent claims the user-input category, so only real mouse and key events count.
|
// Every wxCommandEvent claims the user-input category, so only real mouse and key events count,
|
||||||
|
// plus main window resizes, since a border drag produces no mouse events.
|
||||||
int GUI_App::FilterEvent(wxEvent& event)
|
int GUI_App::FilterEvent(wxEvent& event)
|
||||||
{
|
{
|
||||||
if (!event.IsCommandEvent() && (event.GetEventCategory() & wxEVT_CATEGORY_USER_INPUT))
|
if ((!event.IsCommandEvent() && (event.GetEventCategory() & wxEVT_CATEGORY_USER_INPUT)) ||
|
||||||
|
(event.GetEventType() == wxEVT_SIZE && event.GetEventObject() == mainframe))
|
||||||
m_last_input = std::chrono::steady_clock::now();
|
m_last_input = std::chrono::steady_clock::now();
|
||||||
return Event_Skip;
|
return Event_Skip;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ public:
|
|||||||
bool is_editor() const { return m_app_mode == EAppMode::Editor; }
|
bool is_editor() const { return m_app_mode == EAppMode::Editor; }
|
||||||
bool is_gcode_viewer() const { return m_app_mode == EAppMode::GCodeViewer; }
|
bool is_gcode_viewer() const { return m_app_mode == EAppMode::GCodeViewer; }
|
||||||
bool is_recreating_gui() const { return m_is_recreating_gui; }
|
bool is_recreating_gui() const { return m_is_recreating_gui; }
|
||||||
// Milliseconds since the last mouse or keyboard event the app processed.
|
// Milliseconds since the last mouse or keyboard event the app processed, or main window resize.
|
||||||
int input_idle_ms() const;
|
int input_idle_ms() const;
|
||||||
int FilterEvent(wxEvent& event) override;
|
int FilterEvent(wxEvent& event) override;
|
||||||
// The Preferences "Default page" choice, stored as its index: 0 Home, 1 Prepare.
|
// The Preferences "Default page" choice, stored as its index: 0 Home, 1 Prepare.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
|
||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
|
#include <wx/evtloop.h>
|
||||||
|
|
||||||
#include "libslic3r/Utils.hpp"
|
#include "libslic3r/Utils.hpp"
|
||||||
|
|
||||||
@@ -23,12 +24,9 @@ constexpr int quiet_ms = 500;
|
|||||||
// queued meanwhile are handled first; a click waits at most a slice plus the unit that
|
// queued meanwhile are handled first; a click waits at most a slice plus the unit that
|
||||||
// overran it.
|
// overran it.
|
||||||
constexpr int slice_ms = 40;
|
constexpr int slice_ms = 40;
|
||||||
// On GTK a due timer runs ahead of repaints and posted events, so the next slice waits a few ms.
|
// Delay before the next slice; on GTK a due timer runs ahead of repaints and posted events,
|
||||||
#ifdef __WXGTK__
|
// and wxOSX rejects a 0 ms timer.
|
||||||
constexpr int next_slice_ms = 5;
|
constexpr int next_slice_ms = 5;
|
||||||
#else
|
|
||||||
constexpr int next_slice_ms = 0;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// True when unhandled keyboard, button, touch or pen input is queued; only Windows can ask.
|
// True when unhandled keyboard, button, touch or pen input is queued; only Windows can ask.
|
||||||
bool input_pending()
|
bool input_pending()
|
||||||
@@ -42,6 +40,13 @@ bool input_pending()
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// True inside a wxYield(), where a slice would build pages in the middle of the code that yielded.
|
||||||
|
bool yielding()
|
||||||
|
{
|
||||||
|
const wxEventLoopBase* loop = wxEventLoopBase::GetActive();
|
||||||
|
return loop != nullptr && loop->IsYielding();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
IdleScheduler::IdleScheduler(std::function<int()> input_idle_ms) : m_input_idle_ms(std::move(input_idle_ms))
|
IdleScheduler::IdleScheduler(std::function<int()> input_idle_ms) : m_input_idle_ms(std::move(input_idle_ms))
|
||||||
@@ -69,7 +74,7 @@ void IdleScheduler::tick()
|
|||||||
stop();
|
stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (m_input_idle_ms() < quiet_ms || input_pending()) {
|
if (m_input_idle_ms() < quiet_ms || input_pending() || yielding()) {
|
||||||
start();
|
start();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,8 +159,12 @@ void KBShortcutsDialog::fill_pages()
|
|||||||
|
|
||||||
if (wxGetApp().is_editor()) {
|
if (wxGetApp().is_editor()) {
|
||||||
page(_L("Global"), _L("Available anywhere in the window, even while typing in a text field."), ShortcutContext::Global, {
|
page(_L("Global"), _L("Available anywhere in the window, even while typing in a text field."), ShortcutContext::Global, {
|
||||||
fixed(Section::Application, { alt, "1-9, 0" }, L("Run a speed dial favorite while the dial is open")),
|
fixed(Section::SpeedDial, { alt, "1-9, 0" }, L("Run favorite 1 to 10")),
|
||||||
|
fixed(Section::SpeedDial, { ctrl, "B" }, L("Pin or unpin the selected action")),
|
||||||
|
// wx cycles notebook pages on Ctrl+Tab, which is Cmd+Tab on macOS and never arrives there.
|
||||||
|
#ifndef __APPLE__
|
||||||
fixed(Section::Application, { ctrl, key(L_CONTEXT("Tab", "Keyboard Shortcut")) }, L("Switch to the next main tab")),
|
fixed(Section::Application, { ctrl, key(L_CONTEXT("Tab", "Keyboard Shortcut")) }, L("Switch to the next main tab")),
|
||||||
|
#endif
|
||||||
});
|
});
|
||||||
|
|
||||||
page(_L("Prepare"), _L("Available while the 3D view on the Prepare tab has focus."), ShortcutContext::Plater, {
|
page(_L("Prepare"), _L("Available while the 3D view on the Prepare tab has focus."), ShortcutContext::Plater, {
|
||||||
@@ -454,7 +458,10 @@ ShortcutCaptureDialog::ShortcutCaptureDialog(wxWindow* parent, Shortcut shortcut
|
|||||||
capture_sizer->Add(m_chord_label, 0, wxALIGN_CENTER);
|
capture_sizer->Add(m_chord_label, 0, wxALIGN_CENTER);
|
||||||
capture_sizer->AddStretchSpacer();
|
capture_sizer->AddStretchSpacer();
|
||||||
capture->SetSizer(capture_sizer);
|
capture->SetSizer(capture_sizer);
|
||||||
capture->Bind(wxEVT_KEY_DOWN, &ShortcutCaptureDialog::on_key, this);
|
capture->Layout(); // the box is created at its final size, so nothing resizes it into laying the sizer out
|
||||||
|
// The hook runs before the window procedure, so Windows does not open its window menu
|
||||||
|
// over the dialog on Alt+Space.
|
||||||
|
Bind(wxEVT_CHAR_HOOK, &ShortcutCaptureDialog::on_key, this);
|
||||||
capture->Bind(wxEVT_CHAR, &ShortcutCaptureDialog::on_char, this);
|
capture->Bind(wxEVT_CHAR, &ShortcutCaptureDialog::on_char, this);
|
||||||
capture->Bind(wxEVT_LEFT_DOWN, [capture](wxMouseEvent&) { capture->SetFocus(); });
|
capture->Bind(wxEVT_LEFT_DOWN, [capture](wxMouseEvent&) { capture->SetFocus(); });
|
||||||
sizer->Add(capture, 0, wxLEFT | wxRIGHT | wxEXPAND, FromDIP(20));
|
sizer->Add(capture, 0, wxLEFT | wxRIGHT | wxEXPAND, FromDIP(20));
|
||||||
@@ -531,10 +538,12 @@ void ShortcutCaptureDialog::record(const KeyChord& chord)
|
|||||||
m_ok->Enable(false);
|
m_ok->Enable(false);
|
||||||
};
|
};
|
||||||
const bool global = (shortcut_info(m_shortcut).contexts & context_bit(ShortcutContext::Global)) != 0;
|
const bool global = (shortcut_info(m_shortcut).contexts & context_bit(ShortcutContext::Global)) != 0;
|
||||||
if (global && !chord.is_menu_accelerator()) {
|
if (chord.is_system_shortcut()) {
|
||||||
|
reject(_L("The system uses this shortcut, so it cannot be assigned."));
|
||||||
|
} else if (global && !chord.is_menu_accelerator()) {
|
||||||
reject(m_rejection);
|
reject(m_rejection);
|
||||||
} else if (const std::optional<Shortcut> owner = wxGetApp().shortcuts().step_owner(m_shortcut, chord); owner.has_value()) {
|
} else if (const std::optional<Shortcut> owner = wxGetApp().shortcuts().step_owner(m_shortcut, chord); owner.has_value()) {
|
||||||
reject(wxString::Format(_L("Already used as a step of %s."), _(shortcut_info(*owner).name)));
|
reject(wxString::Format(_L("Shift and Ctrl with this key belong to %s and cannot be assigned."), _(shortcut_info(*owner).name)));
|
||||||
} else {
|
} else {
|
||||||
m_conflicts = wxGetApp().shortcuts().conflicts(m_shortcut, chord);
|
m_conflicts = wxGetApp().shortcuts().conflicts(m_shortcut, chord);
|
||||||
m_status->SetForegroundColour(m_status_colour);
|
m_status->SetForegroundColour(m_status_colour);
|
||||||
|
|||||||
@@ -182,6 +182,16 @@ bool KeyChord::is_menu_accelerator() const
|
|||||||
return valid() && ((modifiers & (wxMOD_CONTROL | wxMOD_ALT | wxMOD_RAW_CONTROL)) != 0 || (!is_printable(key) && key != WXK_SPACE));
|
return valid() && ((modifiers & (wxMOD_CONTROL | wxMOD_ALT | wxMOD_RAW_CONTROL)) != 0 || (!is_printable(key) && key != WXK_SPACE));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only a chord the desktop acts on while still delivering it to the app belongs here.
|
||||||
|
bool KeyChord::is_system_shortcut() const
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
return modifiers == wxMOD_ALT && (key == WXK_F4 || key == WXK_SPACE);
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
std::string KeyChord::to_string() const
|
std::string KeyChord::to_string() const
|
||||||
{
|
{
|
||||||
if (!valid())
|
if (!valid())
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ struct KeyChord
|
|||||||
// True when Ctrl or Alt is held or the key is non-printable, the chords a menu can own without
|
// True when Ctrl or Alt is held or the key is non-printable, the chords a menu can own without
|
||||||
// swallowing typing in text fields.
|
// swallowing typing in text fields.
|
||||||
bool is_menu_accelerator() const;
|
bool is_menu_accelerator() const;
|
||||||
|
// True for a chord the desktop acts on although the app receives it, so a binding would
|
||||||
|
// take it from the system.
|
||||||
|
bool is_system_shortcut() const;
|
||||||
|
|
||||||
// Platform-neutral text ("Ctrl+Shift+S") for persistence and wx accelerator strings.
|
// Platform-neutral text ("Ctrl+Shift+S") for persistence and wx accelerator strings.
|
||||||
std::string to_string() const;
|
std::string to_string() const;
|
||||||
|
|||||||
@@ -81,6 +81,7 @@
|
|||||||
|
|
||||||
#ifdef __WXGTK__
|
#ifdef __WXGTK__
|
||||||
#include <gtk/gtk.h>
|
#include <gtk/gtk.h>
|
||||||
|
#include <wx/glcanvas.h>
|
||||||
#endif // __WXGTK__
|
#endif // __WXGTK__
|
||||||
#include <slic3r/GUI/CreatePresetsDialog.hpp>
|
#include <slic3r/GUI/CreatePresetsDialog.hpp>
|
||||||
|
|
||||||
@@ -510,6 +511,9 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
|
|||||||
wxQueueEvent(wxGetApp().plater(), new SimpleEvent(EVT_NOTICE_CHILDE_SIZE_CHANGED));
|
wxQueueEvent(wxGetApp().plater(), new SimpleEvent(EVT_NOTICE_CHILDE_SIZE_CHANGED));
|
||||||
|
|
||||||
fit_tab_labels(); // ORCA on resize
|
fit_tab_labels(); // ORCA on resize
|
||||||
|
// Restarts the idle build so a hidden Prepare page is laid out at the new size.
|
||||||
|
if (m_prebuild_started)
|
||||||
|
m_idle.start();
|
||||||
});
|
});
|
||||||
|
|
||||||
//BBS
|
//BBS
|
||||||
@@ -4008,13 +4012,81 @@ bool MainFrame::Show(bool show)
|
|||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MainFrame::GLResourcesPrebuild::built() const
|
||||||
|
{
|
||||||
|
return m_frame.m_plater != nullptr && m_frame.m_plater->canvas3D()->is_initialized() &&
|
||||||
|
m_frame.m_plater->get_partplate_list().icon_textures_loaded();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MainFrame::GLResourcesPrebuild::build_step()
|
||||||
|
{
|
||||||
|
GLCanvas3D* canvas = m_frame.m_plater->canvas3D();
|
||||||
|
#ifdef __WXGTK__
|
||||||
|
// wx creates a GTK canvas's GL surface when the widget is realized, so the context can be
|
||||||
|
// made current on it while hidden.
|
||||||
|
gtk_widget_realize(canvas->get_wxglcanvas()->GetHandle());
|
||||||
|
#endif
|
||||||
|
if (!canvas->make_current_for_postinit()) {
|
||||||
|
// The first render of the canvas loads everything instead.
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": cannot make the GL context current on the hidden canvas";
|
||||||
|
m_failed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
switch (m_step) {
|
||||||
|
case 0:
|
||||||
|
m_failed = !wxGetApp().init_opengl();
|
||||||
|
break;
|
||||||
|
case 1: {
|
||||||
|
const Size size = canvas->get_canvas_size();
|
||||||
|
wxGetApp().imgui()->set_display_size(float(std::max(1, size.get_width())), float(std::max(1, size.get_height())));
|
||||||
|
canvas->set_imgui_scaling();
|
||||||
|
// Builds the font atlas without leaving a frame open at the hidden canvas's size.
|
||||||
|
wxGetApp().imgui()->new_frame();
|
||||||
|
wxGetApp().imgui()->end_frame();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
// One texture per unit until none remain.
|
||||||
|
if (m_frame.m_plater->get_partplate_list().load_next_plate_texture())
|
||||||
|
return true;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
m_failed = !canvas->init();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Runs after init(), which sets the color mode the icons are drawn for.
|
||||||
|
m_frame.m_plater->get_partplate_list().load_icon_textures();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
++m_step;
|
||||||
|
return !m_failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MainFrame::PrepareLayoutPrebuild::built() const
|
||||||
|
{
|
||||||
|
// The book lays out the page it shows.
|
||||||
|
const wxWindow* page = m_frame.m_tabpanel != nullptr ? m_frame.m_tabpanel->GetCurrentPage() : nullptr;
|
||||||
|
return page == nullptr || page == m_frame.m_plater || page->GetSize() == m_laid_out_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MainFrame::PrepareLayoutPrebuild::build_step()
|
||||||
|
{
|
||||||
|
// Sized as the book sizes the page it selects, so the selection finds nothing to lay out.
|
||||||
|
const wxWindow* page = m_frame.m_tabpanel->GetCurrentPage();
|
||||||
|
m_laid_out_size = page->GetSize();
|
||||||
|
m_frame.m_plater->SetSize(page->GetRect());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// A page out of the book stays registered and is passed over; a negative order is never
|
// A page out of the book stays registered and is passed over; a negative order is never
|
||||||
// registered.
|
// registered.
|
||||||
void MainFrame::prebuild_pages_when_idle()
|
void MainFrame::prebuild_pages_when_idle()
|
||||||
{
|
{
|
||||||
m_idle.clear();
|
m_idle.clear();
|
||||||
|
m_idle.add(m_gl_prebuild);
|
||||||
if (m_param_panel)
|
if (m_param_panel)
|
||||||
m_idle.add(m_param_panel->settings_page_prebuild());
|
m_idle.add(m_param_panel->settings_page_prebuild());
|
||||||
|
m_idle.add(m_prepare_layout_prebuild);
|
||||||
for (LazyBase* page : m_lazy_pages)
|
for (LazyBase* page : m_lazy_pages)
|
||||||
if (page->prebuild_order() >= 0)
|
if (page->prebuild_order() >= 0)
|
||||||
m_idle.add(*page);
|
m_idle.add(*page);
|
||||||
|
|||||||
@@ -140,6 +140,38 @@ class MainFrame : public DPIFrame
|
|||||||
wxTimer* m_reset_title_text_colour_timer{ nullptr };
|
wxTimer* m_reset_title_text_colour_timer{ nullptr };
|
||||||
IdleScheduler m_idle;
|
IdleScheduler m_idle;
|
||||||
bool m_prebuild_started{ false };
|
bool m_prebuild_started{ false };
|
||||||
|
// Loads the Prepare canvas's GL resources while its page is hidden.
|
||||||
|
class GLResourcesPrebuild : public LazyBase
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit GLResourcesPrebuild(MainFrame& frame) : m_frame(frame) {}
|
||||||
|
const std::string& name() const override { return m_name; }
|
||||||
|
bool built() const override;
|
||||||
|
bool pending() const override { return !m_failed && !built(); }
|
||||||
|
bool build_step() override;
|
||||||
|
int prebuild_order() const override { return 0; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
MainFrame& m_frame;
|
||||||
|
std::string m_name{ "gl_resources" };
|
||||||
|
int m_step{ 0 };
|
||||||
|
bool m_failed{ false };
|
||||||
|
} m_gl_prebuild{ *this };
|
||||||
|
// Lays out the hidden Prepare page at the size the book gives its pages.
|
||||||
|
class PrepareLayoutPrebuild : public LazyBase
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit PrepareLayoutPrebuild(MainFrame& frame) : m_frame(frame) {}
|
||||||
|
const std::string& name() const override { return m_name; }
|
||||||
|
bool built() const override;
|
||||||
|
bool build_step() override;
|
||||||
|
int prebuild_order() const override { return 0; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
MainFrame& m_frame;
|
||||||
|
std::string m_name{ "prepare_layout" };
|
||||||
|
wxSize m_laid_out_size;
|
||||||
|
} m_prepare_layout_prebuild{ *this };
|
||||||
// Every LazyPage, in and out of the book; prebuild_pages_when_idle() registers them.
|
// Every LazyPage, in and out of the book; prebuild_pages_when_idle() registers them.
|
||||||
std::vector<LazyBase*> m_lazy_pages;
|
std::vector<LazyBase*> m_lazy_pages;
|
||||||
// The latest EVT_LOAD_PRINTER_URL, applied when the web Device view is built.
|
// The latest EVT_LOAD_PRINTER_URL, applied when the web Device view is built.
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ public:
|
|||||||
bool split_multi_line{false};
|
bool split_multi_line{false};
|
||||||
bool option_label_at_right{false};
|
bool option_label_at_right{false};
|
||||||
// BBS: new layout
|
// BBS: new layout
|
||||||
wxWindow * stb;
|
wxWindow * stb{ nullptr };
|
||||||
const wxString icon;
|
const wxString icon;
|
||||||
const wxString title;
|
const wxString title;
|
||||||
bool m_labels_hidden{false};
|
bool m_labels_hidden{false};
|
||||||
|
|||||||
@@ -122,8 +122,8 @@ class ParamsPanel : public wxPanel
|
|||||||
|
|
||||||
wxPanel* m_current_tab { nullptr };
|
wxPanel* m_current_tab { nullptr };
|
||||||
|
|
||||||
// Builds the selected page's option groups at idle; while no tab is selected yet,
|
// Builds the selected page's option groups at idle and then shows them for the mode;
|
||||||
// its first unit selects the default one.
|
// while no tab is selected yet, its first unit selects the default one.
|
||||||
class SettingsPagePrebuild : public LazyBase
|
class SettingsPagePrebuild : public LazyBase
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|||||||
+122
-120
@@ -799,67 +799,8 @@ void PartPlate::render_logo(bool bottom, bool render_cali)
|
|||||||
{
|
{
|
||||||
if (!m_partplate_list->render_bedtype_logo) {
|
if (!m_partplate_list->render_bedtype_logo) {
|
||||||
// render third-party printer texture logo
|
// render third-party printer texture logo
|
||||||
if (m_partplate_list->m_logo_texture_filename.empty()) {
|
if (!m_partplate_list->load_logo_texture())
|
||||||
m_partplate_list->m_logo_texture.reset();
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
//GLTexture* temp_texture = const_cast<GLTexture*>(&m_temp_texture);
|
|
||||||
|
|
||||||
if (m_partplate_list->m_logo_texture.get_id() == 0 || m_partplate_list->m_logo_texture.get_source() != m_partplate_list->m_logo_texture_filename) {
|
|
||||||
m_partplate_list->m_logo_texture.reset();
|
|
||||||
|
|
||||||
if (boost::algorithm::iends_with(m_partplate_list->m_logo_texture_filename, ".svg")) {
|
|
||||||
/*// use higher resolution images if graphic card and opengl version allow
|
|
||||||
GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size();
|
|
||||||
if (temp_texture->get_id() == 0 || temp_texture->get_source() != m_texture_filename) {
|
|
||||||
// generate a temporary lower resolution texture to show while no main texture levels have been compressed
|
|
||||||
if (!temp_texture->load_from_svg_file(m_texture_filename, false, false, false, max_tex_size / 8)) {
|
|
||||||
render_default(bottom, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
canvas.request_extra_frame();
|
|
||||||
}*/
|
|
||||||
|
|
||||||
// starts generating the main texture, compression will run asynchronously
|
|
||||||
GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size();
|
|
||||||
GLint logo_tex_size = (max_tex_size < 2048) ? max_tex_size : 2048;
|
|
||||||
if (!m_partplate_list->m_logo_texture.load_from_svg_file(m_partplate_list->m_logo_texture_filename, true, true, true, logo_tex_size)) {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % m_partplate_list->m_logo_texture_filename;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (boost::algorithm::iends_with(m_partplate_list->m_logo_texture_filename, ".png")) {
|
|
||||||
// generate a temporary lower resolution texture to show while no main texture levels have been compressed
|
|
||||||
/* if (temp_texture->get_id() == 0 || temp_texture->get_source() != m_logo_texture_filename) {
|
|
||||||
if (!temp_texture->load_from_file(m_logo_texture_filename, false, GLTexture::None, false)) {
|
|
||||||
render_default(bottom, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
canvas.request_extra_frame();
|
|
||||||
}*/
|
|
||||||
|
|
||||||
// starts generating the main texture, compression will run asynchronously
|
|
||||||
if (!m_partplate_list->m_logo_texture.load_from_file(m_partplate_list->m_logo_texture_filename, true, GLTexture::MultiThreaded, true)) {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % m_partplate_list->m_logo_texture_filename;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": can not load logo texture from %1%, unsupported format") % m_partplate_list->m_logo_texture_filename;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (m_partplate_list->m_logo_texture.unsent_compressed_data_available()) {
|
|
||||||
// sends to gpu the already available compressed levels of the main texture
|
|
||||||
m_partplate_list->m_logo_texture.send_compressed_data_to_gpu();
|
|
||||||
|
|
||||||
// the temporary texture is not needed anymore, reset it
|
|
||||||
//if (temp_texture->get_id() != 0)
|
|
||||||
// temp_texture->reset();
|
|
||||||
|
|
||||||
//canvas.request_extra_frame();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_logo_triangles.is_initialized())
|
if (m_logo_triangles.is_initialized())
|
||||||
render_logo_texture(m_partplate_list->m_logo_texture, m_logo_triangles, bottom);
|
render_logo_texture(m_partplate_list->m_logo_texture, m_logo_triangles, bottom);
|
||||||
@@ -4232,6 +4173,7 @@ Vec2d PartPlateList::compute_shape_position(int index, int cols)
|
|||||||
//generate icon textures
|
//generate icon textures
|
||||||
void PartPlateList::generate_icon_textures()
|
void PartPlateList::generate_icon_textures()
|
||||||
{
|
{
|
||||||
|
m_icon_textures_dark = m_is_dark;
|
||||||
// use higher resolution images if graphic card and opengl version allow
|
// use higher resolution images if graphic card and opengl version allow
|
||||||
GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size(), icon_size = max_tex_size / 8;
|
GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size(), icon_size = max_tex_size / 8;
|
||||||
std::string path = resources_dir() + "/images/";
|
std::string path = resources_dir() + "/images/";
|
||||||
@@ -4447,6 +4389,9 @@ void PartPlateList::release_icon_textures()
|
|||||||
PartPlateList::is_load_bedtype_textures = false;
|
PartPlateList::is_load_bedtype_textures = false;
|
||||||
PartPlateList::is_load_extruder_only_area_textures = false;
|
PartPlateList::is_load_extruder_only_area_textures = false;
|
||||||
PartPlateList::is_load_cali_texture = false;
|
PartPlateList::is_load_cali_texture = false;
|
||||||
|
m_next_bedtype_texture = 0;
|
||||||
|
m_next_extruder_only_area_texture = 0;
|
||||||
|
m_next_cali_texture = 0;
|
||||||
for (int i = 0; i < btCount; i++) {
|
for (int i = 0; i < btCount; i++) {
|
||||||
for (auto& part: bed_texture_info[i].parts) {
|
for (auto& part: bed_texture_info[i].parts) {
|
||||||
if (part.texture) {
|
if (part.texture) {
|
||||||
@@ -6002,12 +5947,7 @@ void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& pr
|
|||||||
plate_hover_action = hover_id % PartPlate::GRABBER_COUNT;
|
plate_hover_action = hover_id % PartPlate::GRABBER_COUNT;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool last_dark_mode_status = m_is_dark;
|
load_icon_textures();
|
||||||
if (m_is_dark != last_dark_mode_status) {
|
|
||||||
last_dark_mode_status = m_is_dark;
|
|
||||||
generate_icon_textures();
|
|
||||||
} else if(m_del_texture.get_id() == 0)
|
|
||||||
generate_icon_textures();
|
|
||||||
for (it = m_plate_list.begin(); it != m_plate_list.end(); it++) {
|
for (it = m_plate_list.begin(); it != m_plate_list.end(); it++) {
|
||||||
int current_index = (*it)->get_index();
|
int current_index = (*it)->get_index();
|
||||||
if (only_current && (current_index != m_current_plate))
|
if (only_current && (current_index != m_current_plate))
|
||||||
@@ -6149,6 +6089,8 @@ bool PartPlateList::set_shapes(const Pointfs &shape,
|
|||||||
}
|
}
|
||||||
is_load_bedtype_textures = false; //reload textures
|
is_load_bedtype_textures = false; //reload textures
|
||||||
is_load_extruder_only_area_textures = false; // reload textures
|
is_load_extruder_only_area_textures = false; // reload textures
|
||||||
|
m_next_bedtype_texture = 0;
|
||||||
|
m_next_extruder_only_area_texture = 0;
|
||||||
calc_bounding_boxes();
|
calc_bounding_boxes();
|
||||||
|
|
||||||
update_logo_texture_filename(texture_filename);
|
update_logo_texture_filename(texture_filename);
|
||||||
@@ -7089,53 +7031,120 @@ bool PartPlateList::init_extruder_only_area_info()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PartPlateList::load_bedtype_textures()
|
static GLint logo_texture_size()
|
||||||
{
|
{
|
||||||
if (PartPlateList::is_load_bedtype_textures) return;
|
return std::min<GLint>(OpenGLManager::get_gl_info().get_max_tex_size(), 2048);
|
||||||
|
|
||||||
init_bed_type_info();
|
|
||||||
GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size();
|
|
||||||
GLint logo_tex_size = (max_tex_size < 2048) ? max_tex_size : 2048;
|
|
||||||
for (int i = 0; i < (unsigned int)btCount; ++i) {
|
|
||||||
for (int j = 0; j < bed_texture_info[i].parts.size(); j++) {
|
|
||||||
std::string filename = resources_dir() + "/images/" + bed_texture_info[i].parts[j].filename;
|
|
||||||
if (boost::filesystem::exists(filename)) {
|
|
||||||
PartPlateList::bed_texture_info[i].parts[j].texture = new GLTexture();
|
|
||||||
if (!PartPlateList::bed_texture_info[i].parts[j].texture->load_from_svg_file(filename, true, true, true, logo_tex_size)) {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % filename;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % filename;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PartPlateList::is_load_bedtype_textures = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PartPlateList::load_extruder_only_area_textures() {
|
// Loads the texture of the next untried part across the parts of `infos`, in order, advancing
|
||||||
if (PartPlateList::is_load_extruder_only_area_textures) return;
|
// `next`; false once every part has been tried.
|
||||||
|
static bool load_next_part_texture(PartPlateList::BedTextureInfo* infos, size_t count, size_t& next, bool compress_and_filter)
|
||||||
auto ok = init_extruder_only_area_info();
|
{
|
||||||
if (!ok) {
|
size_t k = next;
|
||||||
PartPlateList::is_load_extruder_only_area_textures = true;
|
for (size_t i = 0; i < count; ++i) {
|
||||||
return;
|
if (k >= infos[i].parts.size()) {
|
||||||
|
k -= infos[i].parts.size();
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size();
|
++next;
|
||||||
GLint logo_tex_size = (max_tex_size < 2048) ? max_tex_size : 2048;
|
PartPlateList::BedTextureInfo::TexturePart& part = infos[i].parts[k];
|
||||||
for (int i = 0; i < (unsigned int) ExtruderOnlyAreaType::btAreaCount; ++i) {
|
const std::string filename = resources_dir() + "/images/" + part.filename;
|
||||||
for (int j = 0; j < extruder_only_area_info[i].parts.size(); j++) {
|
|
||||||
std::string filename = resources_dir() + "/images/" + extruder_only_area_info[i].parts[j].filename;
|
|
||||||
if (boost::filesystem::exists(filename)) {
|
if (boost::filesystem::exists(filename)) {
|
||||||
PartPlateList::extruder_only_area_info[i].parts[j].texture = new GLTexture();
|
part.texture = new GLTexture();
|
||||||
if (!PartPlateList::extruder_only_area_info[i].parts[j].texture->load_from_svg_file(filename, true, false, false, logo_tex_size)) {
|
if (!part.texture->load_from_svg_file(filename, true, compress_and_filter, compress_and_filter, logo_texture_size()))
|
||||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % filename;
|
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load texture from %1% failed!") % filename;
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % filename;
|
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load texture from %1% failed!") % filename;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PartPlateList::load_bedtype_textures()
|
||||||
|
{
|
||||||
|
while (load_next_bedtype_texture()) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PartPlateList::load_next_bedtype_texture()
|
||||||
|
{
|
||||||
|
if (PartPlateList::is_load_bedtype_textures)
|
||||||
|
return false;
|
||||||
|
if (m_next_bedtype_texture == 0)
|
||||||
|
init_bed_type_info();
|
||||||
|
if (load_next_part_texture(bed_texture_info, btCount, m_next_bedtype_texture, true))
|
||||||
|
return true;
|
||||||
|
PartPlateList::is_load_bedtype_textures = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PartPlateList::load_logo_texture()
|
||||||
|
{
|
||||||
|
if (m_logo_texture_filename.empty()) {
|
||||||
|
m_logo_texture.reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_logo_texture.get_id() != 0 && m_logo_texture.get_source() == m_logo_texture_filename) {
|
||||||
|
if (m_logo_texture.unsent_compressed_data_available())
|
||||||
|
// sends to gpu the already available compressed levels of the main texture
|
||||||
|
m_logo_texture.send_compressed_data_to_gpu();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_logo_texture.reset();
|
||||||
|
// starts generating the main texture, compression will run asynchronously
|
||||||
|
if (boost::algorithm::iends_with(m_logo_texture_filename, ".svg")) {
|
||||||
|
if (!m_logo_texture.load_from_svg_file(m_logo_texture_filename, true, true, true, logo_texture_size())) {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % m_logo_texture_filename;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (boost::algorithm::iends_with(m_logo_texture_filename, ".png")) {
|
||||||
|
if (!m_logo_texture.load_from_file(m_logo_texture_filename, true, GLTexture::MultiThreaded, true)) {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % m_logo_texture_filename;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": can not load logo texture from %1%, unsupported format") % m_logo_texture_filename;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PartPlateList::load_icon_textures()
|
||||||
|
{
|
||||||
|
if (!icon_textures_loaded())
|
||||||
|
generate_icon_textures();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PartPlateList::load_next_plate_texture()
|
||||||
|
{
|
||||||
|
if (!render_bedtype_logo) {
|
||||||
|
load_logo_texture();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return load_next_bedtype_texture() || load_next_cali_texture() || load_next_extruder_only_area_texture();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PartPlateList::load_extruder_only_area_textures()
|
||||||
|
{
|
||||||
|
while (load_next_extruder_only_area_texture()) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PartPlateList::load_next_extruder_only_area_texture()
|
||||||
|
{
|
||||||
|
if (PartPlateList::is_load_extruder_only_area_textures)
|
||||||
|
return false;
|
||||||
|
if (m_next_extruder_only_area_texture == 0 && !init_extruder_only_area_info()) {
|
||||||
PartPlateList::is_load_extruder_only_area_textures = true;
|
PartPlateList::is_load_extruder_only_area_textures = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (load_next_part_texture(extruder_only_area_info, (size_t) ExtruderOnlyAreaType::btAreaCount, m_next_extruder_only_area_texture, false))
|
||||||
|
return true;
|
||||||
|
PartPlateList::is_load_extruder_only_area_textures = true;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PartPlateList::init_cali_texture_info()
|
void PartPlateList::init_cali_texture_info()
|
||||||
@@ -7150,26 +7159,19 @@ void PartPlateList::init_cali_texture_info()
|
|||||||
|
|
||||||
void PartPlateList::load_cali_textures()
|
void PartPlateList::load_cali_textures()
|
||||||
{
|
{
|
||||||
if (PartPlateList::is_load_cali_texture) return;
|
while (load_next_cali_texture()) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PartPlateList::load_next_cali_texture()
|
||||||
|
{
|
||||||
|
if (PartPlateList::is_load_cali_texture)
|
||||||
|
return false;
|
||||||
|
if (m_next_cali_texture == 0)
|
||||||
init_cali_texture_info();
|
init_cali_texture_info();
|
||||||
GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size();
|
if (load_next_part_texture(&cali_texture_info, 1, m_next_cali_texture, true))
|
||||||
GLint logo_tex_size = (max_tex_size < 2048) ? max_tex_size : 2048;
|
return true;
|
||||||
for (int i = 0; i < (unsigned int)btCount; ++i) {
|
|
||||||
for (int j = 0; j < cali_texture_info.parts.size(); j++) {
|
|
||||||
std::string filename = resources_dir() + "/images/" + cali_texture_info.parts[j].filename;
|
|
||||||
if (boost::filesystem::exists(filename)) {
|
|
||||||
PartPlateList::cali_texture_info.parts[j].texture = new GLTexture();
|
|
||||||
if (!PartPlateList::cali_texture_info.parts[j].texture->load_from_svg_file(filename, true, true, true, logo_tex_size)) {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load cali texture from %1% failed!") % filename;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load cali texture from %1% failed!") % filename;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PartPlateList::is_load_cali_texture = true;
|
PartPlateList::is_load_cali_texture = true;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PartPlateList::on_extruder_count_changed(int extruder_count)
|
void PartPlateList::on_extruder_count_changed(int extruder_count)
|
||||||
|
|||||||
@@ -644,6 +644,7 @@ class PartPlateList : public ObjectBase
|
|||||||
std::string m_hover_tooltip;
|
std::string m_hover_tooltip;
|
||||||
|
|
||||||
bool m_is_dark = false;
|
bool m_is_dark = false;
|
||||||
|
bool m_icon_textures_dark = false;
|
||||||
|
|
||||||
int m_filament_count = 1;
|
int m_filament_count = 1;
|
||||||
|
|
||||||
@@ -943,12 +944,25 @@ public:
|
|||||||
bool calc_extruder_only_area(Rect &left_only_rect, Rect &right_only_rect);
|
bool calc_extruder_only_area(Rect &left_only_rect, Rect &right_only_rect);
|
||||||
void init_bed_type_info();
|
void init_bed_type_info();
|
||||||
bool init_extruder_only_area_info();
|
bool init_extruder_only_area_info();
|
||||||
|
// Each load_*_textures() loads whatever of its set is not loaded yet; each load_next_*()
|
||||||
|
// loads one texture and returns false once none remain.
|
||||||
void load_bedtype_textures();
|
void load_bedtype_textures();
|
||||||
|
bool load_next_bedtype_texture();
|
||||||
void load_extruder_only_area_textures();
|
void load_extruder_only_area_textures();
|
||||||
|
bool load_next_extruder_only_area_texture();
|
||||||
|
// Starts loading the printer's logo texture, or sends the levels compressed since; false when
|
||||||
|
// there is no logo to draw.
|
||||||
|
bool load_logo_texture();
|
||||||
|
|
||||||
void show_cali_texture(bool show = true);
|
void show_cali_texture(bool show = true);
|
||||||
void init_cali_texture_info();
|
void init_cali_texture_info();
|
||||||
void load_cali_textures();
|
void load_cali_textures();
|
||||||
|
bool load_next_cali_texture();
|
||||||
|
bool icon_textures_loaded() const { return m_del_texture.get_id() != 0 && m_icon_textures_dark == m_is_dark; }
|
||||||
|
void load_icon_textures();
|
||||||
|
// Loads the next bed-type, calibration or extruder-area texture, or the logo, which rendering
|
||||||
|
// otherwise loads on first use; false once none remain.
|
||||||
|
bool load_next_plate_texture();
|
||||||
|
|
||||||
void on_extruder_count_changed(int extruder_count);
|
void on_extruder_count_changed(int extruder_count);
|
||||||
|
|
||||||
@@ -960,6 +974,13 @@ public:
|
|||||||
BedTextureInfo bed_texture_info[btCount];
|
BedTextureInfo bed_texture_info[btCount];
|
||||||
BedTextureInfo cali_texture_info;
|
BedTextureInfo cali_texture_info;
|
||||||
BedTextureInfo extruder_only_area_info[(unsigned char) Slic3r::ExtruderOnlyAreaType::btAreaCount];
|
BedTextureInfo extruder_only_area_info[(unsigned char) Slic3r::ExtruderOnlyAreaType::btAreaCount];
|
||||||
|
|
||||||
|
private:
|
||||||
|
// The next part to load in each texture set, counted across the set's parts in order; reset
|
||||||
|
// with the set's is_load_* flag.
|
||||||
|
size_t m_next_bedtype_texture{ 0 };
|
||||||
|
size_t m_next_cali_texture{ 0 };
|
||||||
|
size_t m_next_extruder_only_area_texture{ 0 };
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace GUI
|
} // namespace GUI
|
||||||
|
|||||||
@@ -147,9 +147,11 @@ constexpr std::array<ShortcutInfo, size_t(Shortcut::Count)> shortcut_table = {{
|
|||||||
SHORTCUT(Search, "search", L("Search"), GLOBAL, { 'F', CTRL }),
|
SHORTCUT(Search, "search", L("Search"), GLOBAL, { 'F', CTRL }),
|
||||||
SHORTCUT(SwitchView, "switch_view", L("Switch between Prepare/Preview"), CANVAS, { WXK_TAB }),
|
SHORTCUT(SwitchView, "switch_view", L("Switch between Prepare/Preview"), CANVAS, { WXK_TAB }),
|
||||||
SHORTCUT(CollapseSidebar, "collapse_sidebar", L("Collapse/Expand the sidebar"), CANVAS, { WXK_TAB, SHIFT }),
|
SHORTCUT(CollapseSidebar, "collapse_sidebar", L("Collapse/Expand the sidebar"), CANVAS, { WXK_TAB, SHIFT }),
|
||||||
SHORTCUT(SpeedDial, "speed_dial", L("Open the speed dial"), GLOBAL, { WXK_SPACE }),
|
|
||||||
SHORTCUT(ReloadDevicePage, "reload_device_page", L("Reload the device page"), CANVAS, { WXK_F5 }),
|
SHORTCUT(ReloadDevicePage, "reload_device_page", L("Reload the device page"), CANVAS, { WXK_F5 }),
|
||||||
SHORTCUT(KeyboardShortcuts, "keyboard_shortcuts", L("Show keyboard shortcuts list"), CANVAS, { '?' }),
|
SHORTCUT(KeyboardShortcuts, "keyboard_shortcuts", L("Show keyboard shortcuts list"), CANVAS, { '?' }),
|
||||||
|
|
||||||
|
// Speed Dial
|
||||||
|
SHORTCUT(SpeedDial, "speed_dial", L("Open the Speed Dial"), GLOBAL, { WXK_SPACE }),
|
||||||
}};
|
}};
|
||||||
|
|
||||||
#undef SHORTCUT
|
#undef SHORTCUT
|
||||||
@@ -177,6 +179,7 @@ constexpr std::array<SectionInfo, size_t(ShortcutSection::Count)> section_table
|
|||||||
{ Shortcut::ViewDefault, L("Camera") },
|
{ Shortcut::ViewDefault, L("Camera") },
|
||||||
{ Shortcut::ShowLabels, L("Display") },
|
{ Shortcut::ShowLabels, L("Display") },
|
||||||
{ Shortcut::Preferences, L("Application") },
|
{ Shortcut::Preferences, L("Application") },
|
||||||
|
{ Shortcut::SpeedDial, L("Speed Dial") },
|
||||||
}};
|
}};
|
||||||
|
|
||||||
constexpr bool sections_follow_table_order()
|
constexpr bool sections_follow_table_order()
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ enum class Shortcut : uint8_t {
|
|||||||
// Display
|
// Display
|
||||||
ShowLabels, ShowWireframe, ToggleGcodeWindow, ToggleOneLayerMode,
|
ShowLabels, ShowWireframe, ToggleGcodeWindow, ToggleOneLayerMode,
|
||||||
// Application
|
// Application
|
||||||
Preferences, Search, SwitchView, CollapseSidebar, SpeedDial, ReloadDevicePage, KeyboardShortcuts,
|
Preferences, Search, SwitchView, CollapseSidebar, ReloadDevicePage, KeyboardShortcuts,
|
||||||
|
// Speed Dial
|
||||||
|
SpeedDial,
|
||||||
Count
|
Count
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,7 +68,7 @@ struct ShortcutInfo
|
|||||||
|
|
||||||
// Headings of the shortcuts dialog, in listing order.
|
// Headings of the shortcuts dialog, in listing order.
|
||||||
enum class ShortcutSection : uint8_t {
|
enum class ShortcutSection : uint8_t {
|
||||||
Project, SlicingAndPrinting, Selection, Editing, Objects, Placement, Gizmos, Sliders, PaintingTools, Camera, Display, Application,
|
Project, SlicingAndPrinting, Selection, Editing, Objects, Placement, Gizmos, Sliders, PaintingTools, Camera, Display, Application, SpeedDial,
|
||||||
Count
|
Count
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+12
-2
@@ -7165,12 +7165,18 @@ void Tab::restore_last_select_item()
|
|||||||
|
|
||||||
bool Tab::page_build_pending() const
|
bool Tab::page_build_pending() const
|
||||||
{
|
{
|
||||||
return m_active_page != nullptr && m_active_page->build_pending();
|
return m_active_page != nullptr && (m_active_page->build_pending() || m_active_page->visibility_pending());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Tab::page_build_step()
|
bool Tab::page_build_step()
|
||||||
{
|
{
|
||||||
return m_active_page != nullptr && m_active_page->build_step(m_mode);
|
if (m_active_page == nullptr)
|
||||||
|
return false;
|
||||||
|
if (m_active_page->build_pending())
|
||||||
|
m_active_page->build_step(m_mode);
|
||||||
|
else
|
||||||
|
m_active_page->update_visibility(m_mode, true);
|
||||||
|
return page_build_pending();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Tab::update_description_lines()
|
void Tab::update_description_lines()
|
||||||
@@ -8601,6 +8607,8 @@ void Page::update_visibility(ConfigOptionMode mode, bool update_contolls_visibil
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_show = ret_val;
|
m_show = ret_val;
|
||||||
|
if (update_contolls_visibility)
|
||||||
|
m_visibility_applied = true;
|
||||||
#ifdef __WXMSW__
|
#ifdef __WXMSW__
|
||||||
if (!m_show) return;
|
if (!m_show) return;
|
||||||
// BBS: fix field control position
|
// BBS: fix field control position
|
||||||
@@ -8654,6 +8662,7 @@ bool Page::activate_group(size_t i, ConfigOptionMode mode, std::function<void()>
|
|||||||
auto& group = m_optgroups[i];
|
auto& group = m_optgroups[i];
|
||||||
if (!group->activate(throw_if_canceled))
|
if (!group->activate(throw_if_canceled))
|
||||||
return false;
|
return false;
|
||||||
|
m_visibility_applied = false;
|
||||||
m_vsizer->Add(group->sizer, 0, wxEXPAND | (group->is_legend_line() ? (wxLEFT|wxTOP) : wxALL), m_parent->FromDIP(5)); // ORCA use less margin on parameters section
|
m_vsizer->Add(group->sizer, 0, wxEXPAND | (group->is_legend_line() ? (wxLEFT|wxTOP) : wxALL), m_parent->FromDIP(5)); // ORCA use less margin on parameters section
|
||||||
group->update_visibility(mode);
|
group->update_visibility(mode);
|
||||||
#if HIDE_FIRST_SPLIT_LINE
|
#if HIDE_FIRST_SPLIT_LINE
|
||||||
@@ -8688,6 +8697,7 @@ void Page::clear()
|
|||||||
for (auto group : m_optgroups)
|
for (auto group : m_optgroups)
|
||||||
group->clear();
|
group->clear();
|
||||||
m_page_title = NULL;
|
m_page_title = NULL;
|
||||||
|
m_visibility_applied = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Page::msw_rescale()
|
void Page::msw_rescale()
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class Page: public std::enable_shared_from_this<Page>// : public wxScrolledWindo
|
|||||||
// BBS: new layout
|
// BBS: new layout
|
||||||
wxStaticText* m_page_title;
|
wxStaticText* m_page_title;
|
||||||
bool m_show = true;
|
bool m_show = true;
|
||||||
|
bool m_visibility_applied = false;
|
||||||
public:
|
public:
|
||||||
//BBS: GUI refactor
|
//BBS: GUI refactor
|
||||||
Page(wxWindow* parent, const wxString& title, int iconID, wxPanel* tab_owner);
|
Page(wxWindow* parent, const wxString& title, int iconID, wxPanel* tab_owner);
|
||||||
@@ -98,6 +99,8 @@ public:
|
|||||||
bool build_pending() const;
|
bool build_pending() const;
|
||||||
// Builds the next option group that has no controls yet; true while some remain.
|
// Builds the next option group that has no controls yet; true while some remain.
|
||||||
bool build_step(ConfigOptionMode mode);
|
bool build_step(ConfigOptionMode mode);
|
||||||
|
// Whether the controls have not been shown or hidden for a mode since they were built.
|
||||||
|
bool visibility_pending() const { return !m_visibility_applied; }
|
||||||
void clear();
|
void clear();
|
||||||
void msw_rescale();
|
void msw_rescale();
|
||||||
void sys_color_changed();
|
void sys_color_changed();
|
||||||
@@ -443,8 +446,8 @@ public:
|
|||||||
// BBS: new layout
|
// BBS: new layout
|
||||||
void set_expanded(bool value);
|
void set_expanded(bool value);
|
||||||
void restore_last_select_item();
|
void restore_last_select_item();
|
||||||
// page_build_pending() says whether the selected page has groups without controls, and
|
// page_build_pending() says whether the selected page has groups without controls or controls
|
||||||
// page_build_step() builds one.
|
// not yet shown for the mode, and page_build_step() does the next of those.
|
||||||
bool page_build_pending() const;
|
bool page_build_pending() const;
|
||||||
bool page_build_step();
|
bool page_build_step();
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include <wx/webviewarchivehandler.h>
|
#include <wx/webviewarchivehandler.h>
|
||||||
#include <wx/webviewfshandler.h>
|
#include <wx/webviewfshandler.h>
|
||||||
|
#include <wx/weakref.h>
|
||||||
#if wxUSE_WEBVIEW_EDGE
|
#if wxUSE_WEBVIEW_EDGE
|
||||||
#include <wx/msw/webview_edge.h>
|
#include <wx/msw/webview_edge.h>
|
||||||
#elif defined(__WXMAC__)
|
#elif defined(__WXMAC__)
|
||||||
@@ -235,7 +236,9 @@ class FakeWebView : public wxWebView
|
|||||||
wxDEFINE_EVENT(EVT_WEBVIEW_RECREATED, wxCommandEvent);
|
wxDEFINE_EVENT(EVT_WEBVIEW_RECREATED, wxCommandEvent);
|
||||||
|
|
||||||
static std::vector<wxWebView*> g_webviews;
|
static std::vector<wxWebView*> g_webviews;
|
||||||
static std::vector<wxWebView*> g_delay_webviews;
|
// Webviews waiting for their script handler while another one is added; adding it yields, so a
|
||||||
|
// view can be destroyed while it waits.
|
||||||
|
static std::vector<wxWeakRef<wxWebView>> g_delay_webviews;
|
||||||
|
|
||||||
class WebViewRef : public wxObjectRefData
|
class WebViewRef : public wxObjectRefData
|
||||||
{
|
{
|
||||||
@@ -340,8 +343,9 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url)
|
|||||||
addScriptMessageHandler(webView);
|
addScriptMessageHandler(webView);
|
||||||
while (!g_delay_webviews.empty()) {
|
while (!g_delay_webviews.empty()) {
|
||||||
auto views = std::move(g_delay_webviews);
|
auto views = std::move(g_delay_webviews);
|
||||||
for (auto wv : views)
|
for (const wxWeakRef<wxWebView>& wv : views)
|
||||||
addScriptMessageHandler(wv);
|
if (wv)
|
||||||
|
addScriptMessageHandler(wv.get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#ifndef __WIN32__
|
#ifndef __WIN32__
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <functional>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
@@ -11,8 +12,10 @@
|
|||||||
#include "libslic3r/ClipperUtils.hpp"
|
#include "libslic3r/ClipperUtils.hpp"
|
||||||
#include "libslic3r/AABBTreeLines.hpp"
|
#include "libslic3r/AABBTreeLines.hpp"
|
||||||
#include "libslic3r/Fill/Fill.hpp"
|
#include "libslic3r/Fill/Fill.hpp"
|
||||||
|
#include "libslic3r/Fill/FillAdaptive.hpp"
|
||||||
#include "libslic3r/Flow.hpp"
|
#include "libslic3r/Flow.hpp"
|
||||||
#include "libslic3r/Geometry.hpp"
|
#include "libslic3r/Geometry.hpp"
|
||||||
|
#include "libslic3r/IntersectionPoints.hpp"
|
||||||
#include "libslic3r/Layer.hpp"
|
#include "libslic3r/Layer.hpp"
|
||||||
#include "libslic3r/Print.hpp"
|
#include "libslic3r/Print.hpp"
|
||||||
#include "libslic3r/PrintConfig.hpp"
|
#include "libslic3r/PrintConfig.hpp"
|
||||||
@@ -1196,6 +1199,231 @@ TEST_CASE("Trapezoidal grid infill rounds its corners only with more than one li
|
|||||||
REQUIRE(single_smooth.length == single_sharp.length);
|
REQUIRE(single_smooth.length == single_sharp.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Multiline cubic infill follows the cubic lines without crossing itself", "[Fill]")
|
||||||
|
{
|
||||||
|
const int multiline = GENERATE(2, 3);
|
||||||
|
const double spacing = 0.45;
|
||||||
|
const double density = 0.3;
|
||||||
|
const double wall = multiline * spacing;
|
||||||
|
CAPTURE(multiline);
|
||||||
|
|
||||||
|
const ExPolygon region{ Slic3r::Points{ Point::new_scale(0., 0.), Point::new_scale(40., 0.),
|
||||||
|
Point::new_scale(40., 40.), Point::new_scale(0., 40.) } };
|
||||||
|
auto fill = [®ion, spacing](int lines, double density, size_t layer_id, double z) {
|
||||||
|
std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("cubic"));
|
||||||
|
filler->spacing = spacing;
|
||||||
|
filler->angle = float(M_PI / 7.);
|
||||||
|
filler->layer_id = layer_id;
|
||||||
|
filler->z = z;
|
||||||
|
|
||||||
|
FillParams params;
|
||||||
|
params.density = float(density);
|
||||||
|
params.multiline = lines;
|
||||||
|
params.dont_adjust = true;
|
||||||
|
params.anchor_length_max = 0.f; // The bare pattern, without connections along the boundary.
|
||||||
|
Slic3r::Surface surface(stInternal, region);
|
||||||
|
return filler->fill_surface(&surface, params);
|
||||||
|
};
|
||||||
|
// Away from the boundary, where a line is clipped earlier than the side of its wall.
|
||||||
|
const Polygons inner = shrink(to_polygons(region), scale_(3.));
|
||||||
|
auto farthest = [&inner](const Polylines &from, const Polylines &to) {
|
||||||
|
const AABBTreeLines::LinesDistancer<Line> tree(to_lines(to));
|
||||||
|
double distance = 0.;
|
||||||
|
for (const Polyline &path : intersection_pl(from, inner))
|
||||||
|
for (const Point &point : path.equally_spaced_points(scale_(0.2)))
|
||||||
|
distance = std::max(distance, tree.distance_from_lines<false>(point));
|
||||||
|
return unscale<double>(distance);
|
||||||
|
};
|
||||||
|
|
||||||
|
// One z period of the pattern: sqrt(2) / 3 of the 3 * wall / density line spacing.
|
||||||
|
const double z_period = std::sqrt(2.) * wall / density;
|
||||||
|
const size_t layers = 30;
|
||||||
|
for (size_t layer_id = 0; layer_id < layers; ++layer_id) {
|
||||||
|
const double z = z_period * (layer_id + 0.5) / layers;
|
||||||
|
CAPTURE(layer_id, z);
|
||||||
|
const Polylines walls = fill(multiline, density, layer_id, z);
|
||||||
|
REQUIRE_FALSE(walls.empty());
|
||||||
|
CHECK(get_intersections(to_lines(walls)).empty());
|
||||||
|
// Long paths running out to the boundary, not loops around the cells.
|
||||||
|
CHECK(std::none_of(walls.begin(), walls.end(), [](const Polyline &path) { return path.first_point() == path.last_point(); }));
|
||||||
|
|
||||||
|
// Single lines at the same spacing: the walls are drawn along them.
|
||||||
|
const Polylines lines = fill(1, density / multiline, layer_id, z);
|
||||||
|
REQUIRE_FALSE(lines.empty());
|
||||||
|
CHECK(farthest(lines, walls) < 0.5 * wall);
|
||||||
|
CHECK(farthest(walls, lines) < 1.5 * wall);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Multiline adaptive cubic infill keeps its lines apart without closing them around the cells", "[Fill]")
|
||||||
|
{
|
||||||
|
const std::string pattern = GENERATE("adaptivecubic", "supportcubic");
|
||||||
|
const int multiline = GENERATE(2, 3);
|
||||||
|
CAPTURE(pattern, multiline);
|
||||||
|
|
||||||
|
// A sphere refines the octree all around, so the finer lines end on the coarser ones at every layer.
|
||||||
|
TriangleMesh sphere = Slic3r::Test::mesh(Slic3r::Test::TestMesh::sphere_50mm);
|
||||||
|
sphere.scale(0.3f);
|
||||||
|
Print print;
|
||||||
|
Slic3r::Test::init_and_process_print({sphere}, print,
|
||||||
|
{{"sparse_infill_pattern", pattern},
|
||||||
|
{"sparse_infill_density", "40%"},
|
||||||
|
{"fill_multiline", multiline},
|
||||||
|
{"infill_anchor", 0},
|
||||||
|
{"infill_anchor_max", 0},
|
||||||
|
{"layer_height", 0.3}});
|
||||||
|
|
||||||
|
size_t paths = 0, loops = 0;
|
||||||
|
for (const Layer *layer : print.objects().front()->layers()) {
|
||||||
|
Polylines printed;
|
||||||
|
Polygons sparse;
|
||||||
|
double spacing = 0.;
|
||||||
|
for (const LayerRegion *region : layer->regions()) {
|
||||||
|
for (const ExtrusionEntity *entity : region->fills.flatten().entities)
|
||||||
|
if (entity->role() == erInternalInfill)
|
||||||
|
entity->collect_polylines(printed);
|
||||||
|
for (const Surface &surface : region->fill_surfaces.surfaces)
|
||||||
|
if (surface.surface_type == stInternal)
|
||||||
|
append(sparse, shrink(to_polygons(surface.expolygon), scale_(1.)));
|
||||||
|
spacing = region->flow(frInfill).spacing();
|
||||||
|
}
|
||||||
|
if (printed.empty())
|
||||||
|
continue;
|
||||||
|
CAPTURE(layer->print_z);
|
||||||
|
paths += printed.size();
|
||||||
|
loops += std::count_if(printed.begin(), printed.end(), [](const Polyline &pl) { return pl.first_point() == pl.last_point(); });
|
||||||
|
CHECK(get_intersections(to_lines(printed)).empty());
|
||||||
|
|
||||||
|
// Neighbouring lines stay a line spacing apart, less the overlap of a line end with the wall it stops on.
|
||||||
|
// Pieces of one line that meet end to end are one line.
|
||||||
|
std::vector<size_t> line_of(printed.size());
|
||||||
|
std::iota(line_of.begin(), line_of.end(), 0);
|
||||||
|
std::function<size_t(size_t)> find = [&](size_t i) { return line_of[i] == i ? i : line_of[i] = find(line_of[i]); };
|
||||||
|
for (size_t i = 0; i < printed.size(); ++i)
|
||||||
|
for (size_t j = i + 1; j < printed.size(); ++j)
|
||||||
|
for (const Point &a : { printed[i].first_point(), printed[i].last_point() })
|
||||||
|
for (const Point &b : { printed[j].first_point(), printed[j].last_point() })
|
||||||
|
if ((a - b).cast<double>().norm() < SCALED_EPSILON)
|
||||||
|
line_of[find(i)] = find(j);
|
||||||
|
Lines lines;
|
||||||
|
std::vector<size_t> owner;
|
||||||
|
for (size_t i = 0; i < printed.size(); ++i)
|
||||||
|
for (const Line &line : printed[i].lines()) {
|
||||||
|
lines.push_back(line);
|
||||||
|
owner.push_back(find(i));
|
||||||
|
}
|
||||||
|
AABBTreeLines::LinesDistancer<Line> tree(lines);
|
||||||
|
double closest = spacing;
|
||||||
|
for (size_t i = 0; i < printed.size(); ++i)
|
||||||
|
for (const Point &p : printed[i].equally_spaced_points(scale_(0.1)))
|
||||||
|
if (contains(sparse, p))
|
||||||
|
for (size_t k : tree.all_lines_in_radius(p, scale_(spacing)))
|
||||||
|
if (owner[k] != find(i))
|
||||||
|
closest = std::min(closest, unscale<double>(lines[k].distance_to(p)));
|
||||||
|
CHECK(closest > 0.45 * spacing);
|
||||||
|
}
|
||||||
|
REQUIRE(paths > 0);
|
||||||
|
// The lines run on through the cells instead of each cell getting its own loops.
|
||||||
|
CHECK(loops < paths / 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Multiline adaptive cubic paths touch where they bounce off each other", "[Fill]")
|
||||||
|
{
|
||||||
|
const int sweep = GENERATE(0, 1, 2);
|
||||||
|
// Offset of the third family in walls, so the three meet in points or in small triangles.
|
||||||
|
const double shift = GENERATE(0., 0.1, 0.5, 1., 2.5, -0.5, -1.);
|
||||||
|
// Like finer octree lines ending on coarser ones, the 60 degree lines may start on the horizontal line through 0.
|
||||||
|
const bool starting = GENERATE(false, true);
|
||||||
|
CAPTURE(sweep, shift, starting);
|
||||||
|
|
||||||
|
const double d1 = scale_(0.8), pitch = scale_(8.), inner = scale_(12.);
|
||||||
|
Lines lines;
|
||||||
|
for (int k = 0; k < 3; ++k) {
|
||||||
|
const Vec2d dir(std::cos(k * M_PI / 3.), std::sin(k * M_PI / 3.)), normal(-dir.y(), dir.x());
|
||||||
|
for (int i = -6; i <= 6; ++i) {
|
||||||
|
const Vec2d mid = (i * pitch + (k == 2 ? shift * d1 : 0.)) * normal;
|
||||||
|
const double start = k == 1 && starting ? -mid.y() / dir.y() : -10. * pitch;
|
||||||
|
lines.emplace_back((mid + start * dir).cast<coord_t>(), (mid + 10. * pitch * dir).cast<coord_t>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const Polylines paths = FillAdaptive::multiline_paths(lines, d1, 0., sweep, BoundingBox(Point::new_scale(-20., -20.), Point::new_scale(20., 20.)));
|
||||||
|
REQUIRE_FALSE(paths.empty());
|
||||||
|
CHECK(get_intersections(to_lines(paths)).empty());
|
||||||
|
|
||||||
|
Lines pieces;
|
||||||
|
std::vector<size_t> owner;
|
||||||
|
for (size_t i = 0; i < paths.size(); ++i)
|
||||||
|
for (const Line &line : paths[i].lines()) {
|
||||||
|
pieces.push_back(line);
|
||||||
|
owner.push_back(i);
|
||||||
|
}
|
||||||
|
AABBTreeLines::LinesDistancer<Line> tree(pieces);
|
||||||
|
auto clearance = [&](size_t i) {
|
||||||
|
const Line &a = pieces[i];
|
||||||
|
double distance = std::numeric_limits<double>::max();
|
||||||
|
for (size_t j : tree.all_lines_in_radius(a.midpoint(), 0.5 * a.length() + 2. * d1))
|
||||||
|
if (owner[j] != owner[i]) {
|
||||||
|
const Line &b = pieces[j];
|
||||||
|
distance = std::min({ distance, a.distance_to(b.a), a.distance_to(b.b), b.distance_to(a.a), b.distance_to(a.b) });
|
||||||
|
}
|
||||||
|
return distance;
|
||||||
|
};
|
||||||
|
auto inside = [inner](const Point &p) { return std::abs(p.x()) < inner && std::abs(p.y()) < inner; };
|
||||||
|
|
||||||
|
double closest = std::numeric_limits<double>::max();
|
||||||
|
for (size_t i = 0; i < pieces.size(); ++i)
|
||||||
|
if (inside(pieces[i].midpoint()))
|
||||||
|
closest = std::min(closest, clearance(i));
|
||||||
|
CHECK(closest > 0.99 * d1);
|
||||||
|
|
||||||
|
// Each path at a crossing touches another one there, none stops short of it.
|
||||||
|
double widest = 0.;
|
||||||
|
for (size_t i = 0; i < lines.size(); ++i)
|
||||||
|
for (size_t j = i + 1; j < lines.size(); ++j)
|
||||||
|
if (Point crossing; line_alg::intersection(lines[i], lines[j], &crossing) && inside(crossing)) {
|
||||||
|
std::map<size_t, double> at;
|
||||||
|
for (size_t k : tree.all_lines_in_radius(crossing, 1.2 * d1))
|
||||||
|
at.emplace(owner[k], std::numeric_limits<double>::max());
|
||||||
|
for (size_t k : tree.all_lines_in_radius(crossing, 2. * d1))
|
||||||
|
if (auto it = at.find(owner[k]); it != at.end())
|
||||||
|
it->second = std::min(it->second, clearance(k));
|
||||||
|
for (const auto &path : at)
|
||||||
|
widest = std::max(widest, path.second);
|
||||||
|
}
|
||||||
|
CHECK(widest < 1.02 * d1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Multiline adaptive cubic paths reach the line they end on when another path ends on them", "[Fill]")
|
||||||
|
{
|
||||||
|
const int sweep = GENERATE(0, 1, 2);
|
||||||
|
// Where the 120 degree line starts on the horizontal one, in walls from the 60 degree line.
|
||||||
|
const double start = GENERATE(0.3, 0.6, 1., 2.);
|
||||||
|
CAPTURE(sweep, start);
|
||||||
|
|
||||||
|
const double d1 = scale_(0.8), overlap = 0.1 * d1, length = scale_(30.);
|
||||||
|
const Vec2d diagonal(0.5, 0.5 * std::sqrt(3.)), horizontal(1., 0.), steep(-0.5, 0.5 * std::sqrt(3.));
|
||||||
|
const Vec2d on_horizontal = start * d1 * horizontal;
|
||||||
|
const Lines lines{ Line((-length * diagonal).cast<coord_t>(), (length * diagonal).cast<coord_t>()),
|
||||||
|
Line(Point(0, 0), (length * horizontal).cast<coord_t>()),
|
||||||
|
Line(on_horizontal.cast<coord_t>(), (on_horizontal - length * steep).cast<coord_t>()) };
|
||||||
|
const Polylines paths = FillAdaptive::multiline_paths(lines, d1, overlap, sweep, BoundingBox(Point::new_scale(-40., -40.), Point::new_scale(40., 40.)));
|
||||||
|
|
||||||
|
// The end of the path along each line nearest to where that line starts.
|
||||||
|
auto end_along = [&paths](const Line &line) {
|
||||||
|
for (const Polyline &path : paths)
|
||||||
|
if (line.distance_to(path.first_point()) < SCALED_EPSILON && line.distance_to(path.last_point()) < SCALED_EPSILON)
|
||||||
|
return (path.first_point() - line.a).cast<double>().norm() < (path.last_point() - line.a).cast<double>().norm() ? path.first_point() : path.last_point();
|
||||||
|
return Point(std::numeric_limits<coord_t>::max(), 0);
|
||||||
|
};
|
||||||
|
const Point horizontal_end = end_along(lines[1]), steep_end = end_along(lines[2]);
|
||||||
|
REQUIRE(horizontal_end.x() != std::numeric_limits<coord_t>::max());
|
||||||
|
REQUIRE(steep_end.x() != std::numeric_limits<coord_t>::max());
|
||||||
|
// Both reach the overlap into the wall of the path they stop at, none stops short of it.
|
||||||
|
CHECK_THAT(line_alg::distance_to_infinite(lines[0], horizontal_end) / d1, Catch::Matchers::WithinAbs(0.9, 0.01));
|
||||||
|
CHECK(lines[1].distance_to(steep_end) / d1 < 0.91);
|
||||||
|
CHECK(get_intersections(to_lines(paths)).empty());
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("3D honeycomb infill rounds its octahedral waves with the smooth factor", "[Fill]")
|
TEST_CASE("3D honeycomb infill rounds its octahedral waves with the smooth factor", "[Fill]")
|
||||||
{
|
{
|
||||||
auto shape_for = [](const std::string &smooth_factor) {
|
auto shape_for = [](const std::string &smooth_factor) {
|
||||||
|
|||||||
@@ -120,6 +120,20 @@ TEST_CASE("Only modified or non-printable chords qualify as menu accelerators",
|
|||||||
CHECK(registry.accelerator(Shortcut::KeyboardShortcuts).empty());
|
CHECK(registry.accelerator(Shortcut::KeyboardShortcuts).empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Chords the desktop keeps for itself are recognized", "[Shortcuts]")
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
CHECK(KeyChord{ WXK_F4, wxMOD_ALT }.is_system_shortcut());
|
||||||
|
CHECK(KeyChord{ WXK_SPACE, wxMOD_ALT }.is_system_shortcut());
|
||||||
|
#else
|
||||||
|
CHECK_FALSE(KeyChord{ WXK_F4, wxMOD_ALT }.is_system_shortcut());
|
||||||
|
CHECK_FALSE(KeyChord{ WXK_SPACE, wxMOD_ALT }.is_system_shortcut());
|
||||||
|
#endif
|
||||||
|
CHECK_FALSE(KeyChord{ WXK_F4, wxMOD_ALT | wxMOD_SHIFT }.is_system_shortcut());
|
||||||
|
CHECK_FALSE(KeyChord{ WXK_F4, wxMOD_CONTROL }.is_system_shortcut());
|
||||||
|
CHECK_FALSE(KeyChord{ WXK_SPACE }.is_system_shortcut());
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("Chords convert to wx accelerator entries", "[Shortcuts]")
|
TEST_CASE("Chords convert to wx accelerator entries", "[Shortcuts]")
|
||||||
{
|
{
|
||||||
const wxAcceleratorEntry entry = KeyChord{ 'S', wxMOD_CONTROL | wxMOD_SHIFT }.to_accelerator_entry(42);
|
const wxAcceleratorEntry entry = KeyChord{ 'S', wxMOD_CONTROL | wxMOD_SHIFT }.to_accelerator_entry(42);
|
||||||
|
|||||||
Reference in New Issue
Block a user