Compare commits

..

253 Commits

Author SHA1 Message Date
ruevs 2afabadfa5 CI: Update actions to avoid warnings and Flatpak build error. 2024-06-26 17:03:35 +03:00
Kyle Dickerson 40a1fdc6af Implement basic kerning when rendering text.
Only old-style `kern` tables are supported--modern GPOS-based kerning is not supported.
2024-06-11 23:24:36 +03:00
ruevs cc64fed2d0 Change "show full file path in the window title" to "display the full path in the title bar"
To match the name of the option in Windows Explorer.
2024-06-11 23:24:03 +03:00
John Ingle 71ad5cbfab Add preference to toggle file path in title bar 2024-06-11 17:11:40 +03:00
mnml_ 8f40d8f4c2 add "tool" to make sentece sensical 2024-06-03 15:05:35 -04:00
Koen Schmeets 569ac27dd7 Temporarily disable snap 2024-04-21 17:59:54 +02:00
Koen Schmeets d0a201e0cb Maybe fix macOS notarization 2024-04-21 17:12:42 +02:00
Koen Schmeets 656afaa12d Try to build macOS with new app-specific-password secret 2024-04-21 16:38:02 +02:00
Koen Schmeets a17ee176de Try to use notarytool 2024-04-17 22:56:02 +03:00
ruevs 7310659a24 Update the year to 2024 in the About dialog and Windows version resource. 2024-04-17 19:36:21 +03:00
NotSure 7d379e7f3b Updated uk-UA locale; fixed typos; added translations 2024-01-03 13:11:29 -05:00
NotSure f25bf39fa7 fixed pt-line distance constraint bug on paste transformed 2024-01-03 13:11:00 -05:00
Adam Novak ddeab11615 Bypass desktop portal in Snap
This will fix https://github.com/solvespace/solvespace/issues/1067 by setting
GTK_USE_PORTAL=0 in the Snap package. This will use a file chooser from the
point of view of the application to open and save files, instead of using the
"desktop portal" where the chooser runs outside the snap and the file is
exposed dynamically into the snap at an automatically generated path.

This will allow files used in assemblies to be seen by SolveSpace as being at
their real paths, so the references to them will be saved as proper
relative-path references based on where the files actually are, instead of as
references to where the portal has exposed them.

The downside is that without the portal you can't point the application at
files that *aren't* exposed to the Snap.
2023-12-31 09:25:04 -05:00
phkahler 1f8b3f2589 Make arc default dimenision (radius vs diameter) a configuration option. 2023-12-29 11:13:22 -05:00
phkahler 1a71ca745a Dimesions on arcs defaut to radius instead of diameter 2023-12-29 11:13:22 -05:00
Rylie Pavlik 486e43a450 Codespell fix 2023-12-14 12:51:21 -05:00
Dhruv Gramopadhye 3e5f9834d7 Add allocate function as emscripten dep
Web version crashes when you try to make a constraint. After debugging,
addressed the issue with this PR. Emscripten does some optimization
stuff and ends up dropping certain functions, including the alloacte
function called in solvespace's C++ Unwrap method.

To reproduce/test bug:
- Open the web version
- Create a rectangle
- Create a length constraint on one the rectangle edges
2023-12-08 09:47:42 -05:00
Dhruv Gramopadhye 94618d4ce1
CI, macOS: Update dead links to libomp in dependency installer
Big Sur -> Ventura
2023-12-05 14:16:27 +02:00
ruevs 727b0153f6
Update CHANGELOG.md 2023-11-23 10:27:33 +02:00
phkahler 87d0e097bb CTRL-TAB un/hides the toolbar 2023-11-23 09:09:41 +02:00
ruevs d88ed3f2ee Adjust the test suite to match the new behaviour of Path::Join. 2023-11-22 11:12:06 -05:00
ruevs 6f7e45ba9f Fix invalid parts linkage in assemblies when the folder has only one char.
Fix a bug in the `Split` function that would drop a single character
directory name at the end of the path.

Because of the above bug when a directory containing an assembly file had
a name with only one character, the assembly file was saved incorrectly and
the path to the linked files was invalid.

For example if `assembly.slvs` was located in a directory called `a` and
links `subpart.slvs` in the same directory this would result in:

Group.impFileRel=a\subpart.slvs

Which resulted in the linked part not being found when opening next time.

Fixes: #1347
2023-11-22 11:12:06 -05:00
ruevs d5e8a8267c Fix opening of assembly groups with relative (empty) paths.
The `Join` method that merges paths used to return an empty path when
either of the paths was empty. This caused problems when opening files
with linked/assembled groups when the current path was relative - for
example when SolveSpace was ran from the command line with a drawing
files (in the same directory) as a parameter e.g. `solvespace assemby.slvs`.

See https://github.com/solvespace/solvespace/pull/1194 for detailed
discussion.

Fixes: #1292
2023-11-22 11:12:06 -05:00
ruevs 5edc1ec0fb Images are construction by default
With this change and the previous commit images will not be included in
3d groups by default.
2023-11-22 11:06:43 -05:00
ruevs 70382660c8 Do not include construction images in extrude/lathe/revolve/helix
To avoid unnecessary/annoying copies of images added in 2d sketches if they
are marked "construction" they will not be copied when creating solids.

Fixes: #1418
2023-11-22 11:06:43 -05:00
phkahler e7c0c1665f GTK: Eliminate direct references to gdk event struct members in prep for moving to GTK4. 2023-10-14 14:16:38 -04:00
ruevs 0d26ca17f7 UI: Make marquee selection of line segments precise
Before marquee selection worked by checking whether the Axis Aligned
Bounding Box of an entity and the selection marquee overlap. This selects
(slanted) line segments even though the marquee did not "touch" them.

To fix this for line segments actually check that the selection marque
intersects the line segment.
2023-10-14 18:43:40 +03:00
ruevs 9edf2bcc34 Win32: Fix for compiling with MinGW.org GCC-6.3.0-1
MinGW-w64 MinGW-Builds 13.1.0 on the other hand does not need this.
2023-09-29 16:02:20 +03:00
phkahler f399997976 Speed up view change animations 2023-09-28 19:37:10 -04:00
phkahler 1963a836ef Improve boolean difference operations...
...by fixing the logic in KeepRegion() to properly keep/discard
regions where two surfaces coincide.

Now difference works as well as intersection in this respect.

Change inSame to coincSame for clarity.
2023-09-22 17:32:26 +03:00
Victor Kustov 1919a18916 Fix language bug
Signed-off-by: Victor Kustov <ktrace@yandex.ru>
2023-09-20 09:15:20 -04:00
Koen Schmeets e6565c8118
Debug issue with notarization 2023-09-12 21:14:42 +02:00
vthriller beb473b94d Ask before overwriting existing file
Closes #1399
2023-09-12 12:08:26 -04:00
Tomáš Hübelbauer 36ecb85bb3 Update the issue template to show where to look for SolveSpace version on macOS
macOS SolveSpace has the Help menu but it lacks the About menu item in it and instead the way to find the SolveSpace version is to go to SolveSpace > About SolveSpace. This PR updates the issue template to show that.

I have also replaced the `...` with an ellipsis (`…`) as it IMO looks better (can revert this if it doesn't match the menu item on Windows/Linux) and updated the example version to the current SolveSpace version.
2023-08-14 08:06:57 -04:00
Yusuf Yavuzyigit 88e4d08324 Update test.yml with ubuntu-latest and macos-latest 2023-08-09 13:58:58 +03:00
Yusuf Yavuzyigit 95b00bd888
Fix CI for Ubuntu and MacOS (#1388)
Seems like ubuntu-18.04 and macos-10.15 are no longer hosted by GitHub. Switching to ubuntu-latest (at the time of writing: 22.04) and macos-latest (at the time of writing 12) works.

[GitHub hosted runners](https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources)
2023-08-08 18:28:32 +03:00
Paul Kahler 0e2fd2e6d9
Update CHANGELOG.md
Mention the new behavior of ESC while sketching.
2023-07-27 09:41:33 -04:00
phkahler b34d9a2f11 Delete the partially drawn entity when ESC is pressed on pending operations by using UndoUndo() 2023-07-26 20:42:18 -04:00
ruevs f0912d42b1 UI: Remove shortcut key collisions in the Sketch menu
"&Workplane" -> "Wor&kplane"
"&Image" -> "I&mage"
"Tangent &Arc at Point" -> "Ta&ngent Arc at Point"

Closes #1019
2023-07-14 16:32:33 +03:00
Matteo Scalia 3d04d4cc04 skip duplicate drilled holes 2023-07-07 08:49:24 -04:00
alufers 7ccb0ffac4 UI: Add Ctrl+Shift+S keyboard shortuct for Save As
Many desktop programs use this shortcut for the "Save As" dialog,
so it makes sense to add it here so that different variants of parts
can be quickly created.
2023-07-03 17:00:06 +03:00
phkahler fd25424ab5 small simplifications 2023-05-21 12:19:35 -04:00
phkahler 604335f1c9 rename the enums for surface classification in the SShell class. 2023-05-21 12:19:35 -04:00
liuxilu 2e2aceb5ed
Chinese transalation updated (#1365)
* revised zh_CN.po

* update PO-Revision-Date

* revised zh_CN.po

* revised zh_CN.po

* revert workaround for fixed crash #990
2023-05-04 19:28:00 +03:00
Alexandre Prokoudine aa0d25e6ae
Update Russian translation (#1355) 2023-04-05 00:02:29 +03:00
ruevs 659215d84c Translations: update Spanish
Mostly with Google translate :-)
2023-03-17 11:37:12 +02:00
ruevs 7161a2ab38 Translations: update Japanese
Japanese (ja_JP): updated by @verylowfreq
2023-03-17 10:13:49 +02:00
ruevs b69d565e9d Translations: update Turkish
Turkish (tr_TR): updated by @mhalil
2023-03-17 10:13:49 +02:00
ruevs 9e043c66ca UI: Fix typo in the "Angle" constraint hint. 2023-03-17 10:13:49 +02:00
ruevs dab6e173ee Translations: update all other languages
- French (fr_FR): updated by @progval
- Czech (cs_CZ): updated by @strzinek
- Turkish (tr_TR): updated by @mhalil
- German (de_DE): small update from @rurban
- All other languages: changed messages will revert to English until updated.
2023-03-17 10:13:49 +02:00
ruevs d63047d089 Translations: update English 2023-03-17 10:13:49 +02:00
ruevs 0305f9a56c Translations: update German and Russian
...to the best of my abilities. Both need a review from a native speaker.
2023-03-17 10:13:48 +02:00
ruevs 69ded9721f Update the year to 2023 in the About dialog and Windows version resource. 2023-03-17 10:12:12 +02:00
ruevs 25b5977962 Fix a crash when constraining a line segment symmetric
This was a regression Introduced in 3d3d5c7.
Fixes #1345
2023-02-28 22:49:36 +02:00
Blockers d6e970918f Fix MessageBox Avalanches due to Message blocking
This fixes #1320. The root cause for the avalanche of
messages is due to how the refresh timer system calls GenerateAll. When
GenerateAll is called by Refresh, if a Message occurs, that Message will
block GenerateAll. It _doesn't_ block subsequent calls from the timer to
Refresh (presumably from a separate thread). Because the
"scheduledGenerateAll" flag is not cleared until after the generation is
unblocked, each following refresh triggers another call to GenerateAll.
By reversing the flag clear and call, it breaks the cycle. I don't think
this matters for scheduledShowTW, but I updated it as well.
2023-02-03 11:52:16 -05:00
robnee 3c91bf7ca4 Add config option for "camera" rotation navigation
SS rotates the model when middle button dragging while some users expect
this operation to rotate the camera where left-right and up-down directions are
reversed instead.  This adds that option.
2023-02-03 11:50:55 -05:00
phkahler 302aebfd1a quiet some compiler warnings 2023-02-01 13:36:42 -05:00
phkahler e4fcb7de08 stop using deprecated gtk_show_uri on Linux 2023-02-01 13:36:42 -05:00
phkahler 0c28adc69e eliminate possible use before set warnings 2023-01-31 22:14:54 -05:00
Koen Schmeets 8117a21d59
Revert legacy snapstore key 2023-01-31 23:45:42 +01:00
phkahler 9ee9aa7609 Treat a linked group as mesh-only if it has a Mesh but no Shell. 2023-01-29 18:36:38 -05:00
ruevs b556daaf74 Tanslations: update main translation file 2023-01-18 17:44:08 +02:00
ruevs 3f11bbe47a Translations: change contact informatuion in "Report-Msgid-Bugs-To:"
to current maintainer.
2023-01-18 17:40:35 +02:00
lomatus 10c6c09541 Chinese translation updated. 2023-01-17 19:29:07 -05:00
ruevs 9282b403dd UI: Make the new "dimensions only mode" icons consistent with the rest.
Also optimize for size.
2023-01-17 19:16:36 -05:00
ruevs 60cd95d608 Dimension constraints only display mode improvements
Make the `TriStateButton` class "universal" and use it in place of the
`OccludedLinesButton` class which is now removed.

Fix the tool-tips on the constraint button to show what will come instead
of what is - just like the the occluded lines button. Also change the
text of the tool-tips wording to be more clear and consistent with other
buttons.

Small stylistic and code formatting changes.
2023-01-17 19:16:36 -05:00
77maxikov a0219b2228 dimonly fix 2023-01-17 19:16:36 -05:00
phkahler 4a34253a37 Don't try to drag points with pt-coincident constraint to a previous group. Fixes #1012 and makes dragging as done in one of the tutorials possible again. 2023-01-17 19:12:06 -05:00
ruevs b4be656f25
Update CHANGELOG.md 2023-01-11 22:58:30 +02:00
77maxikov 7b6a85da5a UI: Add constraining multiple points coincident 2023-01-11 22:35:07 +02:00
ruevs 6487fb890e UI: Adjust the menu items "Angle" and "Equal..." to match current state. 2023-01-11 22:27:07 +02:00
ruevs bacc0b66bd UI: Adjust the hints for creating constraints
...for equal circles/arcs and angles.
2023-01-11 22:18:19 +02:00
Paul Kahler 3ee3561153
Update CHANGELOG.md 2023-01-11 12:42:08 -05:00
Paul Kahler a1be8a8d6a
Update CHANGELOG.md 2023-01-11 11:54:51 -05:00
ruevs f22ebf2a54 UI: Adjust the hints for creating constraints
...to match the new multi(variadic) constraints.
2023-01-11 10:31:45 +02:00
phkahler 20e3d15f90 Use N for equal angle constraints. Allows 3 or 4 line segments to be set equal length all at once. 2023-01-10 16:17:57 -05:00
Koen Schmeets 10cb310f18
Try to use SNAPCRAFT_STORE_LEGACY_CREDENTIALS for now 2023-01-08 13:06:08 +01:00
Koen Schmeets 1827d154c8
Fix snap release (#1323) 2023-01-08 02:33:15 +03:00
phkahler a71e4bef81 allow equal angle constraints when 3 or 4 lines selected. Variadic constraints broke this feature by make equal length lines in those cases. 2023-01-07 15:35:50 -05:00
phkahler 3833dd0246 Allow point-on-face for up to 3 faces at once 2023-01-02 17:25:31 -05:00
ruevs 3609f8a7e9 Use a lambda to list selected faces when multiple faces are selected. 2023-01-02 17:25:31 -05:00
ruevs aee47a42c6 Clean up face selection code 2023-01-02 17:25:31 -05:00
ruevs adb2768154 Draw up to three selected faces 2023-01-02 17:25:31 -05:00
phkahler 7d5eaffa89 Add ability to select 3 faces 2023-01-02 17:25:31 -05:00
phkahler 105a350ccd change/fix some undo behavior in variadic constraints 2023-01-02 15:28:58 -05:00
phkahler 0db1f6bacd Fix H/V constraints on points and allow more than 2 points 2023-01-02 15:28:58 -05:00
phkahler a5809891d6 fix several crashes on constraint creeation 2023-01-02 15:28:58 -05:00
77maxikov 3d3d5c789d Sync multiconstraint with current state 2022-12-31 15:20:34 -05:00
ruevs 50cbecbe72 Web: Adjust the scroll wheel sensitivity for zooming. 2022-11-06 04:34:04 +02:00
ruevs 6fc84ae2ce Web: Remove the device pixel ratio workaround for Android tablets.
Afetr the `GetDevicePixelRatio` function was fixed to return `double`
`useWorkaround_devicePixelRatio` is not needed any more so remove it.

See the discussion in #1310 pull request for details.
2022-11-06 03:42:23 +02:00
ruevs c53c592dbe Platform: Fix GUI scaling on devices with non-integer pixel ratio
The fuction GetDevicePixelRatio now returns a `double` instead of an `int`.
This should allow the scaling of the GUI on devices where the pixel ratio
is non integer to work properly. For example a monitor on Windows where the
DPI is not a multiple of 96. It may help with the Web Emscripten port on
tablets and phones as well.

In addition on Windows the mouse wheel delta calculation is fixed.
2022-11-06 01:51:25 +02:00
ruevs 31ac8083ae ru_RU: Enhance russian translation
Besed on pull request #1290.
2022-11-05 19:53:18 +02:00
verylowfreq 1603402df2 Web: Improve file dialog. 2022-11-05 19:26:01 +02:00
verylowfreq 4981570844 Web: Improve touch support and layout. 2022-11-05 19:26:01 +02:00
verylowfreq b5cde57bb6 Web: Some fix for critical runtime error and cleanups. 2022-11-05 19:26:01 +02:00
verylowfreq cf597277fa Web: Initial support for touch devices. 2022-11-05 19:26:01 +02:00
verylowfreq 64948c4526 Web: Add opening/saving file support.
- Opening file is implemented as uploading.
- Saving file is implemented as downloading.
  - The filename is suffixed with current date and time.
2022-11-05 19:25:54 +02:00
ruevs 56b9d36030 Remove superfluous #include <iostream> introduced in 4fc0141 2022-11-05 19:15:13 +02:00
Eldritch Cheese 4fc0141a5e [Graphics][Bugfix] "Nearest Isometric" respects turntable navigation
Prior to this commit, the "Nearest Isometric" GUI command searched all
24 possible isometric views.  When using turntable navigation, this
could result in the z-axis no longer being oriented vertically.  This
commit restricts the views being searched while in turntable
navigation mode to those that follow this restriction.
2022-11-05 12:14:28 -04:00
TristeFigure d50e2b2a43 Fix "Constraintes" type in French locale 2022-11-05 12:12:39 -04:00
Val Lorentz 5c899617b4 fr_FR: Replace straight quotes with angular quotes
This is more common in French.
2022-11-05 12:12:21 -04:00
Val Lorentz d1d7ae690b fr_FR: Add missing translations, and review existing ones. 2022-11-05 12:12:21 -04:00
Val Lorentz fc55990f21 fr_FR: Complete menu translation and fix inconsistencies
Including translations of other parts of the UI refering to menus
with a different name.
2022-11-05 12:12:21 -04:00
Val Lorentz 014dd43cf4 fr_FR: Fix spacing before punctuation marks
';', ':', '!', and '?' should always follow a non-breaking space
2022-11-05 12:12:21 -04:00
Val Lorentz 08c787f749 fr_FR: Change list item prefix
1. Use regular spaces instead of non-breaking, like English
2. Use en dashes instead of stars, which are more common in French
   typography as bullet style
2022-11-05 12:12:21 -04:00
Adam Novak 3dc4d0e640 Allow all linkable files as missing files to fix #1297 2022-10-18 18:51:39 -04:00
Loïc Bartoletti 6b9e7b2eec CMake: Add FindCairo.cmake to fix build on FreeBSD 2022-10-18 13:24:44 -04:00
Val Lorentz 574fc0190a Fix inconsistent French translation of "Rotate Imported"
"Rotation Importation 90°" was the only noun group in this part
of the menu.

This replaces it with an infinitive group like the rest of the menu,
which is also easier to understand.
2022-10-11 13:55:13 -04:00
Val Lorentz a873cee637 Fix typos 2022-10-11 13:53:45 -04:00
Val Lorentz f9a7a96108 Fix French translations of "Step"
"Step" has plenty of meanings, and the wrong one was used here:
"espacement" roughly means "spacing", which is very confusing for the
"Step dimension" feature, and weird for "Step translation"/"Step rotation".

Instead, I chose to translate it as:

* "Pas-à-pas" for the former, which is a noun for lack of a verb that fully
  captures the meaning, which literally translates to "step-by-step".
  "Pas-à-pas" is also the common translation of "Step debugging" in
  programming.
* "Répétiter par" for the latter, which is a verb (+ adverb) which literally
  translates to "Repeat by" and is the most natural way to phrase this.

As a side-effect, I made a key binding consistent with English.
2022-10-11 13:53:45 -04:00
ruevs bce25bb0e2
README: Move the check out instructions to one place. (#1285) 2022-09-03 19:32:54 +03:00
verylowfreq f7415048a5 Web: Emscripten port updated to current tools. Add saving of options in local storage. 2022-08-21 14:13:22 +03:00
whitequark 5ca6d04e02 Add a very experimental Emscripten port.
Web: Emscripten port updated to current tools. Add saving of options in local storage.

U  Web: Emscripten port updated to current tools. Add saving of options in local storage.
2022-08-21 13:36:50 +03:00
Adam Strzelecki cf4defcd47
mac: Distinguish trackpad from Magic Mouse scroll (#1274)
This is the second attempt to distinguish trackpad scrolling that when used
should yield panning from Magic Mouse scrolling that should control zoom.

Since trackpad will begin with a touch prior to the scroll event, but Magic
Mouse not, we can use it to make sure we trigger panning only on a trackpad.

Previous "mac: Don't interpret single-touch scroll events as pan gestures"
that was flawed was reverted which ultimately lead to being unable to zoom using
Magic Mouse.
2022-08-18 00:15:14 +02:00
Maximilian Federle c65e31bece snap: Port to core22
- Migration according to https://forum.snapcraft.io/t/micro-howto-migrate-from-core20-to-core22/30188
- Also declare all snaps stable
2022-08-09 13:49:00 -04:00
ruevs bc4244e099 Win32, MSVC: Enable Multi-processor Compilation (/MP) with Visual Studio
Makes compiling from the Visual Studio IDE much faster when using the
solution and projects generated by cmake.

For the externals I hijack the `disable_warnings` function.
2022-07-08 18:43:16 -05:00
Ryan Pavlik ce6c4ddeb5 Update README to point to flathub now too. 2022-07-08 17:26:19 +03:00
Ryan Pavlik 29263a8d41 Update AppStream metadata. 2022-07-08 17:26:19 +03:00
Ryan Pavlik 6951c71785 Add a github action to build flatpaks 2022-07-08 17:26:19 +03:00
tinywrkb e6e217b7df flatpak: Keep CLI enabled 2022-07-08 17:26:19 +03:00
tinywrkb 1c5db4d564 flatpak: Cleanup: Update and move module specific value to module cleanup arrays 2022-07-08 17:26:19 +03:00
tinywrkb 2d19afaaef flatpak: Cosmetics
The sources array is usually at the end of a module.
Maybe nitpicking, but the module name will be used as a folder name, and camelcase for folder names is less common on Linux.
2022-07-08 17:26:19 +03:00
tinywrkb 63e420ed11 flatpak: finish-args: Drop unsupported JSON comment and cosmetics 2022-07-08 17:26:19 +03:00
tinywrkb c7dbd54e01 flatpak: libjson-c: Enable threading and disable static libs 2022-07-08 17:26:19 +03:00
tinywrkb 2fdcf228ff flatpak: gtkmm: Avoid building demos and tests 2022-07-08 17:26:19 +03:00
tinywrkb d5f4e6f200 flatpak: Use meson buildsystem where possible 2022-07-08 17:26:19 +03:00
tinywrkb 356e6759b3 flatpak: Add eigen module 2022-07-08 17:26:19 +03:00
tinywrkb 8314d74c59 flatpak: Update modules to latest versions 2022-07-08 17:26:19 +03:00
tinywrkb fef5cc4e4b flatpak: Add f-e-d-c properties to depends, retain comments, and cosmetics 2022-07-08 17:26:19 +03:00
tinywrkb 4a210bdf75 flatpak: Update runtime to 21.08 2022-07-08 17:26:19 +03:00
Ryan Pavlik c4522dbd0d flatpak: Update manifest.
We don't apparently need home dir access, the portal works fine.
2022-07-08 17:26:19 +03:00
Ryan Pavlik c2f65cac12 Fix whitespace and trailing newline in github actions files. 2022-07-08 17:26:19 +03:00
Ryan Pavlik b5333608e9 Stamp source tarballs with the commit hash 2022-07-05 10:06:59 -04:00
ruevs d6e1b23006 Win32: Allow 32 bit SolveSpace to access up to 4GB of RAM.
Link 32 bit SolveSpace for Windows with /LARGEADDRESSAWARE which allows
it to access up to 3GB of RAM on a properly configured 32 bit Windows and
up to 4GB on 64 bit.

See: https://msdn.microsoft.com/en-us/library/aa366778
https://docs.microsoft.com/en-us/cpp/build/reference/largeaddressaware-handle-large-addresses
https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#characteristics
https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc786709
https://docs.microsoft.com/en-us/windows/win32/memory/4-gigabyte-tuning

Fixes: #1261
2022-06-30 07:43:37 -05:00
strzinek 3b133bfd6d czech translations 2022-06-24 15:33:12 +02:00
verylowfreq c20c4dab30 Add ja-JP translation. 2022-06-23 08:42:37 -04:00
ruevs 1e1e655848 Show details for comments associated with points
Since 7e08b02de1 comments can be attached to
points.

Now when such a comment is selected the point (entity) it is associated to
will be shown in the text window together with it's coordinates and the
distance (offset) from the point to the comment.

Most of this was already prepared by Whitequark here
afa9e2890e
I just had to to remove the original 12 year old code by jwesthues that
showed "SELECTED: comment text" for comments.
(6750995ef0)

Fixes #1072
2022-06-14 14:07:00 -04:00
robnee b48ce240c7 Make scheduled/deferred task order deterministic
Fixes #920 #1143

Explanation from @robnee on Feb 7, 2021 in pull request #927

Solvespace uses two timers (generateAllTimer and showTWTimer) to defer tasks
until the event loop processing finishes. This helps coalesce multiple calls
into one. You can call scheduleGenerateAll multiple times while processing UI
messages but only trigger one GenerateAll. scheduleGenerateAll and
scheduleShowTW do their scheuduling by setting timers with durations of zero.
These timers fire (at least on Linux and Windows) some time after all other
events in the message queue have been processed. This works fine when
scheduling either one of these tasks. However, there is no guarantee in what
order the timers will fire (at least on Windows) regardless of which order the
scheduling calls are made. It's pretty easy to demonstrate (on some platforms)
by adding logging to the scheduling calls and timer callbacks.

In many cases TextWindow::Show depends on generateAll happening first. This
causes UI glitches where displays don't update and their contents are stale.
Since this behavior is not deterministic it's easy to imagine how this problem
could make certain bug reports difficult to reproduce and diagnose. #920 is a
good example. It also makes syncing up UI behavior across all platforms a
challenge.

Solving this in the platform domain is tricky. This is PR endeavors to make the
ordering of deferred calls to TextWindow::Show and generateAll deterministic.
It does this by replacing generateAllTimer and showTWTimer with a single
refreshTimer. Calls to scheduleGenerateAll and scheduleShowTW set flags to note
the requested operations and schedule the refreshTimer. A new callback function
SolveSpaceUI::Refresh can then check the flags and ensure that generateAll
happens first. It fixes #920. Moreover, this PR makes it easy to observe and
reproduce this problem reliably and across all platforms by simply reordering
the calls in the Refresh callback.

It's pretty clear that the ordering is important so some solution is needed, if
for no other reason than the sanity of the devs. I think this is a pretty good
solution as it spells out the ordering. If nothing else this PR is helpful in
further investigations.

@ruevs @phkahler I'd like to hear your thoughts.
2022-06-14 13:07:05 -04:00
phkahler be3489e3a0 Add some face and point types to describe screen 2022-06-11 20:47:20 -04:00
phkahler 3fe6349563 Add faces perpendicular and parallel constraints. 2022-06-11 20:46:58 -04:00
Paul Kahler 00418824f9
Update CHANGELOG.md 2022-06-02 12:14:16 -04:00
Ryan Pavlik 70bde63cb3 Fix Exec line of desktop files.
They needed %f to indicate how to pass the file we're claiming to handle.
2022-05-17 22:09:48 -05:00
Ryan Pavlik ec1c2289e5 Eigen includes are needed in more places.
Also remove erroneous redundant extlib/eigen mentions.
2022-05-17 22:09:48 -05:00
Paul Kahler dfbefa60f5
Update CHANGELOG.md 2022-05-12 16:42:47 -04:00
Paul Kahler a4416a4cae
Update CMakeLists.txt 2022-05-12 13:51:21 -04:00
Paul Kahler 0c09d9df0c
Update CMakeLists.txt 2022-05-12 09:57:47 -04:00
phkahler ab00823acc Update mimalloc version to 2.0.6 2022-05-07 17:25:48 -04:00
carelinus b334996c49 German translation completed 2022-05-04 15:18:40 -04:00
Maximilian Federle 6a6fffed0e
CI: Use upstream action to build snap & drop arm64 snap (#1232)
Snapd stopped working in Docker containers in commit
c6011693a8

Therefore, we cannot use the current build action,
which relies on Docker to implement cross-compilation.
Switching to the upstream build action avoids the problem at the cost
of losing the arm64 builds on amd64 hardware.
Those need to be reintroduced by some other mechanism,
e.g. via Launchpad, in the future.

Fixes #1229
2022-04-12 20:12:36 -07:00
Paul Kahler 0cec15c97d
Update README.md 2022-04-01 09:20:08 -04:00
Paul Kahler 2e4a4dee52
Update README.md 2022-03-31 15:34:07 -04:00
Paul Kahler 7e2aba466a
Update README.md 2022-03-30 09:39:28 -04:00
dustinhartlyn fc16cdb370
Fix for zoom in/out error (#1221)
* minor fix open/save dialogue on windows 

On windows 10 the open/save dialogue box has an minor error, and I believe I fixed it. 

When "Open" is selected from the menu, the title of the dialogue box says "SolveSpace - Save File" and the entered file name is "united". My fix correctly titles the dialoged box, and leaves the address bar blank when a file is being opened because "united" is only needed as a default name when a file being saved. 

I found that class FileDialogImplWin32 from guiwin.cpp contains two if statements for "isSaveDialog". This is redundant. I removed the first where the title was originally set, but not working. I then set the title in the second if statement and moved the 'if isEmpty'' to this section.

* Update guiwin.cpp

replaced tabs with spaces

* Created ZoomToMouse function in graphicswin.cpp which referances the mouse position directly. Simplified MouseScroll in mouse.cpp to point to this function instead of altering zoom directly. Also pointed zoom commpand from keyboard and menu to ZoomToMouse so that it works avoids different behavior.

* clean up some comments
2022-02-28 14:22:35 -05:00
Koen Schmeets b429141c28 Revert "mac: Don't interpret single-touch scroll events as pan gestures"
This reverts commit 91db627a81.
2022-02-28 09:45:49 -05:00
phkahler c5ea9a44e1
Move shell functions out of surface.cpp and into shell.cpp (#1220) 2022-02-14 10:26:12 -06:00
ruevs 2383a39636 UI: Display the circumference of a circle in the property browser
Closes #1211
2022-02-09 11:29:07 +02:00
ruevs d8b5281fc9 MacOS: Update the year to 2022 in the About dialog. 2022-02-06 23:40:45 +02:00
Maximilian Federle 43795f5dac CI: Create tarballs with submodules included for releases
Closes #1028
2022-02-06 11:34:05 -05:00
ruevs 026f936989 A workplane can now be defined from a workplane defined by point and normal
It is now possible to create a "New Group | Sketch in New Workplane" from
an existing workplane defined using a point and a nowmal. Before we used to
hit the `ssassert(false, "Unexpected workplane subtype");`.

This makes 4308dc136b more complete and
is related to #1120, #1048 and
https://github.com/solvespace/solvespace/pull/1054
2022-02-03 15:06:07 -05:00
phkahler 465506670c Fix normal issue, when a workplane is created from a point and normal we were only storing the numeric normal so the plane (and subsequent extrusion) wouldn't follow if the sketch was on the face of a revolve. Now we store the handle to the defining normal it predef.entityB so it can update properly on regeneration. 2022-02-03 15:06:07 -05:00
Mustafa Halil 7fa72500c8
Turkish translation tr_TR.po updated (#1203) 2022-02-02 13:52:14 -05:00
ruevs a1b2db5e18 Translations update
Two new strings:
"&Go to GitHub commit"
"Text-formated STL files are not currently supported"
2022-02-01 16:28:52 +02:00
phkahler 61cc28f8b2
Check for text STL when linking (#1197)
* Check for text STL when linking

* Use memcmp in STL import when checking for ASCII format

* Add message box for unsupported STL file type.

Co-authored-by: ruevs <dpr@ruevs.com>
2022-01-27 13:20:35 -05:00
ruevs 859df9f43d README: add libomp dependency for building on macOS 2022-01-25 11:49:09 -05:00
phkahler b399d9a1ec add normals to linked STLs 2022-01-20 09:53:17 -05:00
ruevs 5cb299b2be Update CHANGELOG.md 2022-01-16 17:01:08 -05:00
ruevs f94fc89587 UI: Add a link to the GitHub commit in the Help menu
The "Go to GitHub commit" item in the Help menu opens the URL
https://github.com/solvespace/solvespace/commits/5efc148
and allows the user to see the date of the commit that SolveSpace was
built from.

Closes #1172
2022-01-16 13:08:07 -05:00
ruevs bb7a6cbbba Revert "CMake: use git rev-parse to get GIT_COMMIT_HASH"
This reverts commit f1e47e6554.
Since unfortunately it breaks the addition of the git commit hash
to the version string when building from the VisualStudio IDE.
I presume this happens because `git` is not of the "path" of the
build environment. The version string ends up "3.0~" only.
2022-01-16 13:08:07 -05:00
herrgahr f1e47e6554 CMake: use git rev-parse to get GIT_COMMIT_HASH
The old approach of reading .git/HEAD does not work when using git
worktrees, where the folder layout looks roughly like:

solvespace.git/                      - bare clone (.git dir)
solvespace.git/work                  - example worktree containing master
solvespage.git/worktrees/work/       - .git dir of worktree
solvespage.git/worktrees/work/HEAD   - actual HEAD ref for master

First attempt was to just get GIT_ROOT from `git rev-parse --git-dir` but
that wasn't enough, since:

1. GIT_ROOT points to solvespage.git/worktrees/work/
2. GIT_ROOT/HEAD points to refs/heads/master
3. GIT_ROOT/refs/heads/master does not exist but the old implementation
   would want to use this to get the sha

so we need two invocations of git rev-parse

1. `git rev-parse --git-dir` to get GIT_DIR
    needed for setting GIT_DEPENDS
2. `git rev-parse HEAD` to get the sha of the worktree's HEAD
2022-01-11 09:56:55 -05:00
Ryan Pavlik 5efc148074 Add some gitignore stuff 2022-01-09 00:22:36 +02:00
ruevs 79a6463856 Make vectors/normals shown in the Property Browser into active links
This makes appropriate vectors/normals explorable/selectable and closes #165.

This is a clean implementation that follows the style of the UI code for
the text window. Previous unmerged attempts from the above issue are:
eb3db32059
782a5bbbe6
a77cedbfd8
41e55b3d79
2022-01-09 00:22:36 +02:00
ruevs 3eaa99cb25 Make all points shown in the Property Browser into active links
This makes all points explorable/selectable and fixes #165 partially.

This is a clean implementation that follows the style of the UI code for
the text window. Previous unmerged attempts from the above issue are:
eb3db32059
782a5bbbe6
a77cedbfd8
41e55b3d79
2022-01-09 00:22:36 +02:00
Ryan Pavlik 3d482f0d52 Fix which outer size we use.
Should be the same right now, but this is clearer.
2022-01-06 19:55:35 -05:00
ruevs 9ba7ab5544 Hide edit boxes before closing the property browser.
When the text window/property browser is closed while and edit box
is active its window remained open. This is incorrect and probably
causes a hang on Linux described in #1168. So hide the edit control
when closing.
2022-01-06 18:47:54 -05:00
ruevs 3136493a6a Load 16bit PNG images correctly by re-scaling to 8bit
Fixes #1110
2022-01-06 15:19:06 +02:00
ruevs 892477ee43 Update the year in the About dialog 2022-01-04 12:32:50 +02:00
ruevs cc4307a2a9 Win32: Update the year in the Windows version resource 2022-01-04 12:32:50 +02:00
luzpaz ae4337f066
Fix various typos (#1178)
Found via `codespell -q 3 -S ./res/locales,./extlib -L asign,ba,hsi,mata,pinter,tothe,wser`

Co-authored-by: Maximilian Federle <max.federle@gmail.com>
2022-01-04 12:28:19 +02:00
Maximilian Federle 39ca23f38e
snap: add g++ as build package (#1175)
It got dropped in 34efb6de77 by mistake.
2022-01-02 20:50:02 +01:00
Maximilian Federle 34efb6de77
CMake Fixes + Snap port to core20 (#1174)
* CMake: use PROJECT_VERSION instead of solvespace_*_VERSION

In 006539b, solvespace_MAJOR_VERSION etc. were removed.
However, these variables were still referenced in some places.
Solution: Use PROJECT_VERSION instead.

* CMake: re-add link directories for solvespace target

006539b removed the call to link_directories for gtkmm, jsonc & fontconfig.
This leads to linking errors if those libraries are in "non-standard"
paths.
Fix this by introducing a target specific target_link_directories call.

Fixes #1173

* snap: port to core20 & adapt to CMake changes

Moving to core20 was long overdue anyway, and
the recent CMake changes necessitated some fixes.
Also switch to LZO compression for (way) better cold start
performance.
2022-01-02 20:04:57 +01:00
Koen Schmeets b71c728262 Try to fix snap release by adding libcairo2-dev dependency 2022-01-02 13:56:14 +01:00
Koen Schmeets 8f049c5a14 Fix build by removing unused var (and accepting new apple terms and conditions) 2022-01-02 01:56:03 +01:00
phkahler 74d7db879e
Update README.md 2022-01-01 15:18:27 -05:00
phkahler 5315a69a1e Increase MAX_UNKNOWNS in the solver from 1024 to 2048 2021-12-31 15:16:52 -05:00
Ryan Pavlik 18dc8ee12c Small simplifications 2021-12-31 14:40:47 -05:00
Ryan Pavlik 8ab70c2c8d Fix formatting 2021-12-31 14:40:47 -05:00
Ryan Pavlik 6edeb66e3d Don't hold the sparse matrices in a pointer at all. 2021-12-31 14:40:47 -05:00
Ryan Pavlik 42f5d3ab0d Banish an unneeded use of our custom containers. 2021-12-31 14:40:47 -05:00
Koen Schmeets 2521dcadc4 Fix loop type 2021-12-31 14:40:47 -05:00
Ryan Pavlik 605b48e6c0 Use unique_ptr in main system jacobian. 2021-12-31 14:40:47 -05:00
EvilSpirit c0f075671b Eigen library integration into solver.
Co-authored-by: Ryan Pavlik <ryan.pavlik@collabora.com>
Co-authored-by: Koen Schmeets <hello@koenschmeets.nl>
2021-12-31 14:40:47 -05:00
Koen Schmeets 4ad5d42a24 Add missing eigen submodule in some scripts 2021-12-31 14:40:47 -05:00
Koen Schmeets c66e6cbacc Add eigen extlib to install command 2021-12-31 14:40:47 -05:00
Ryan Pavlik dca5ce607d Add Eigen to build. 2021-12-31 14:40:47 -05:00
Ryan Pavlik ac91809a96 Add Eigen submodule at 3.4.0 tag 2021-12-31 14:40:47 -05:00
EvilSpirit e528fabbe4 Add an early out in writing jacobian.
Split from previous large 'Eigen library integration' commit.
2021-12-30 13:59:58 -05:00
EvilSpirit aec46b608e Add a method to Expr.
(Split out from earlier "Eigen library integration" commit)
2021-12-30 13:59:58 -05:00
EvilSpirit 3ba40230dd Some optimization
1. We are making FoldConstants first, so we are copying less amount of data in DeepCopyWithParamsAsPointers.
2. Since we already perform DeepCopyWithParamsAsPointers, PartialWrt already produces params as pointers
2021-12-30 13:59:58 -05:00
EvilSpirit 2f31673708 Decrease WriteJacobian complexity 2021-12-30 13:59:58 -05:00
EvilSpirit 4d58f95b43 Suppress dof calculation flag 2021-12-30 13:59:58 -05:00
EvilSpirit 7f86a78472 Dof calculation when redundant is allowed. 2021-12-30 13:59:58 -05:00
EvilSpirit 708a08f04b SolveBySubstitution of linear complexity. 2021-12-30 13:59:58 -05:00
Ryan Pavlik 974175dfbc Fix CMake warnings 2021-12-30 13:59:58 -05:00
Ryan Pavlik 006539b945 Clean up/simplify build 2021-12-30 13:59:58 -05:00
Ryan Pavlik a1e18b83cb Normalize namespaces: includes all at global/root namespace.
Should improve the quality of suggestions, etc. we get from tooling.
2021-12-24 11:29:57 -05:00
Ryan Pavlik 6d40eface2 importidf: Fix uninitialized variable 2021-12-24 11:29:57 -05:00
Ryan Pavlik 71c6492d6d readme: Clean up, fix nearly all markdownlint complaints 2021-12-22 15:06:14 -05:00
Ryan Pavlik b86e0dec84 readme: remove outdated build instructions 2021-12-22 15:06:14 -05:00
ruevs e07b082eb8 Fix hang when trying to display characters missing from the embedded font
When SolveSpace tries to display an Unicode code point (e.g. U+EA00 )
that does not exist in the embedded vector font (unifont.hex.gz) it hangs
in an endless loop in
`const BitmapFont::Glyph &BitmapFont::GetGlyph(char32_t codepoint)`.

The reason is that the binary search through the text file unifont-8.0.01.hex
that does the "lazy loading" of glyphs does not end.

Here is a short excerpt from the file:

```
D7FE:00007FFE63866DF66DEE6DDE63DE7FFE7FFE61866FBE638E6FBE6F867FFE0000
D7FF:00007FFE63866DF66DEE6DDE63DE7FFE7FFE61866FBE638E6FBE6FBE7FFE0000
F900:0080108810881FF8100800007FFE00000FF008100FF00810042002447FFE0000
F901:00047FFE008010841FFE10841FFC10841FFC10840880050003000CC0703E0000
```

When searching for `0xEA00` after some iterations of the while loop
2450010bbf/src/resource.cpp (L567)
both `first` and `last` end up pointing to F900. After that on each
consecutive iteration of the loop `last` ends up pointing to the LF (0x0A)
just before F900
2450010bbf/src/resource.cpp (L585)
and then `mid` ends up back on F900 here
2450010bbf/src/resource.cpp (L570)
and this will repeat enlessly.

The solution is to do
```
                    first++;
```
here
2450010bbf/src/resource.cpp (L591),
which will make `first==last` and change whe while loop contition to `while(first < last) {` thiw will
allow the while loop to exit.

Tested with
- 0xEA00 - non existent, not found in 16 iterations.
- 0xF900 - exists but takes exactly 16 iterations of the binary search to finish
- 0x0000 - found in 16 iterations
- 0xFFFD - the replacement Unicode code point for non-existing glyphs. Also the end of the font.
- 0xFFFFF - a codepoint beyond the end, not found and does not cause an exception

The lazy parsing of the vector front was introduced here.
645c2d90ac

Fixes #1150
2021-12-21 09:49:08 -05:00
ruevs 2450010bbf
Update REAMDE.md - point IRC to Libera chat.
... change Solvespace.com URL to https.
2021-12-12 20:12:20 +02:00
Tom Sutcliffe 91db627a81 mac: Don't interpret single-touch scroll events as pan gestures
To handle the Magic Mouse, whose single-finger touch gestures should be
interpreted as scrollwheel events rather than pan gestures.
2021-12-10 10:25:13 -05:00
phkahler 85f6ec4144 fix STL linking issue. Model was disappearing after the link group. 2021-11-20 19:48:17 -05:00
ruevs df3ef2ab0e GUI: Flexible vertical space above the toolbar
If the main window is not high enough allow the default 32 pixel padding
between the menu bar and the toolbar to shrink down to zero.

This allows the main window height to be a minimum of 688 pixels (on
Windows 10) so it is possible to capture 720P video tutorials.

Fixes #1130
2021-11-04 20:08:54 -04:00
Maximilian Federle eb17248bd5 CI: Replace edge releases with links to artifacts
Re-creating the edge release for every push
to master creates many superfluous release notifications.

Stop creating those releases and provide users with direct
links to the workflow artifacts instead via the
nightly.link GitHub app (https://github.com/apps/nightly-link).

Fixes #1103
2021-10-29 16:47:38 -04:00
Simon Wells 2a722c16b8 add gdk.h for GDK_WINDOWING_ defines 2021-10-23 20:59:52 -04:00
Simon Wells 267c002975 modify the spaceware code to also work on wayland
use the recommended compile-time and run-time checks for x11 and wayland
2021-10-23 20:59:52 -04:00
MX_Master a45e84a2ff + safe height gcode parameter 2021-10-23 20:01:29 -04:00
OlesyaGerasimenko 2cd0ee4b33 Update Russian translation 2021-10-04 20:52:24 +03:00
phkahler bb1938903b update pot file and locales.txt and CMakeLists.txt for spanish. 2021-09-26 16:45:49 -04:00
andesfreedesign 4afa810173 Add Spanish / Argentina translation 2021-09-21 11:18:34 -04:00
Maximilian Federle 6bc63e92b0 snap: Fetch tags for snap builds in CI & mention stable channel in README
The snaps use git describe to determine
their grade (stable/devel). Fetch the tags to
make this possible.

Point users to the official release in the stable channel in README.md.
2021-09-03 18:44:21 -04:00
phkahler 0eab7f783b move perspective, lighting, and explode distance from configuration screen to view screen. 2021-08-29 13:54:38 -04:00
tomsci 8cfe1d4bd7
mac: Remove spurious view menu items (#1101)
Which are either not applicable for SolveSpace (the tabs ones) or are
already handled in the platform-independent code (the fullscreen item).
2021-08-28 22:09:48 +02:00
phkahler 4bf9df2385
Update CHANGELOG.md 2021-08-27 19:38:26 -04:00
tomsci e1b0784b31
mac: Support for pan, zoom and rotate trackpad gestures (#1093)
* mac: Support for pan, zoom and rotate trackpad gestures

Currently SolveSpace is nearly unusable on a mac if you only have a
buttonless trackpad and not a mouse, because there's no way to pan
(ie right-click-drag) or rotate (ie middle-click-drag). You can zoom,
but only by using two-finger-drag up and down, which ends up getting
interpreted as a scrollwheel event.

This change makes the app behave much more like any other mac app, by
adding 2-finger-drag pan gesture support and pinch-gesture zooming, and
3D rotate using shift-2-finger-drag.

I've also added support for the rotate two-finger trackpad gesture,
which rotates directly around the screen Z axis (rather than in all 3
dimensions) which is actually something I've found myself wanting to do
with the mouse but afaik there's no equivalent way of achieving that.

While I was there, I fixed a bugette in convertMouseEvent which was
incorrectly translating the NSEvent coordinates, and then fixing up the
fact that the sign of the y-coordinate was wrong as a result. Using the
convertPoint API correctly means that fixup is not required because
convertPoint handles it for you.

* Don't do trackpad gestures on anything except the toplevel window

* mac: Fix non-functional scrollbar on text window

Which has not worked quite right since the last major refactor.

* Don't pass right-button drags to the toolbar

This improves the behaviour of trackpad pan/rotate on mac which uses
simulated right-button events.

* Don't pass cmd/ctrl modifier through on trackpad pan/rotate MouseEvents
2021-08-27 01:58:33 +02:00
tomsci 31a709e2c8
mac: Support external quit requests (#1099)
By no longer always returning NSTerminateCancel in
applicationShouldTerminate.

And implement applicationWillTerminate to ensure the cleanup code in
SolveSpaceUI::Exit() is always called.
2021-08-26 14:03:28 +02:00
Tom Sutcliffe 7e823df94a Correct which group is forced to mesh when linking an STL file
By making IsForcedToMesh() always return true for STL link groups,
rather than trying to set forceToMesh=true during the import phase.

STL link groups are now always shown as "model already forced to
triangle mesh" in the details screen, but also (unlike when the model
is forced to mesh by a parent group) show the '∆' icon in the group
list.
2021-08-24 13:09:19 -07:00
Tom Sutcliffe f71c527e23 Add a "∆" suffix to groups which have "force to triangle mesh" ticked. 2021-08-24 13:09:19 -07:00
phkahler f47cf65f41
Update CHANGELOG.md 2021-08-21 20:27:47 -04:00
Tom Sutcliffe b87987922f Darken disabled gray to 50% and document it. 2021-08-21 20:19:30 -04:00
Tom Sutcliffe 4db3e90b81 Show suppressed groups in gray in the text window 2021-08-21 20:19:30 -04:00
Koen Schmeets 0a3504c30a CI, NFC: Update libomp installation approach on macOS in the GitHub action (#1094) 2021-08-18 13:35:08 +03:00
Tom Sutcliffe 5edb2eebf6 Add "Show Exploded View" menu option
Where each entity in the active workplane sketch is projected a
different amount normal to the workplane, to allow inspection and
easier selection of entities that entirely overlap each other and are
thus otherwise difficult to see or select.

The distance between the exploded "layers" can be controlled in the
configuration page. Negative distances mean the layers are projected in
the opposite direction, relative to the workplane normal.
2021-08-17 17:48:25 +03:00
phkahler 3e595002fe
Update CHANGELOG.md 2021-08-15 18:22:12 -04:00
Tom Sutcliffe e86eb65985 Update feet and inches format to match architectural convention 2021-08-15 18:18:41 -04:00
Tom Sutcliffe 959cf5ba75 Fix MmToString calls that should have editable=true set 2021-08-15 18:18:41 -04:00
Tom Sutcliffe 41e3668f89 Make feet and inches show fractions of an inch, rounded to nearest 1/64
Taking care to round appropriately so you don't end up with things like
35.999 coming out as 2' 12" and similar.
2021-08-15 18:18:41 -04:00
Tom Sutcliffe 2fb6119de8 Add option for displaying dimensions in feet and inches 2021-08-15 18:18:41 -04:00
Tom Sutcliffe 645febfcd6 Set OSX minimum supported version 2021-08-15 12:28:46 -04:00
Tom Sutcliffe c19bd8cc99 Remove unused variable 2021-08-14 20:34:26 -04:00
Tom Sutcliffe b65a0be3d6 Fix/silence mac build warnings
As per Xcode 12.4 you can at least do a warning-free incremental build
with these changes. There are still plenty of warnings in a full build
(mostly from thirdparty components) but with these changes you can at
least develop on mac and see if/when you've added any new warnings when
doing incremental builds.
2021-08-14 20:34:26 -04:00
Tom Sutcliffe 56719415de Don't reset showFaces every time a group is activated
Instead store the state separately for drawing and non-drawing group
types, and set showFaces to one of those, whenever a group is activated.
2021-08-08 13:24:47 -04:00
phkahler 1b8e1dec65
Update CHANGELOG.md 2021-07-31 13:22:40 -04:00
phkahler 06a1f8031d Add optional helix pitch constraint. 2021-07-31 13:15:19 -04:00
phkahler f6bb0a2d35 Add an ALL filter for linking files that includes slvs, emn, and stl 2021-07-28 21:15:33 -04:00
phkahler 2afd6103d9 Add STL linking with bounding box and edge verticies. Experimental. 2021-07-28 20:36:20 -04:00
phkahler a97b77c1e5
Update CHANGELOG.md 2021-07-17 18:38:57 -04:00
luz paz 37da0f3341 Fix various typos
Found via `codespell -q 3 -S ./res/locales,./extlib -L asign,ba,hsi,mata,tothe`
2021-07-06 10:37:58 -04:00
app4soft ddb76324af Update CHANGELOG.md
Fix typo
2021-07-01 14:28:08 -04:00
phkahler 002b12484e
Update CHANGELOG.md
Add some post 3.0 improvements
2021-06-30 21:21:45 -04:00
Eric Chan 37de364257 Addition of ArcLength Ratio and ArcLength Difference constraints to Constraints list 2021-06-28 11:31:53 -04:00
Maxipaille 4308dc136b Fix "Sketch in New Workplane" point & normal to set correct orientation of workplane
Temporary disable other ways because of wrong implementation
2021-06-27 13:17:09 -04:00
Olivier JANIN 3ccf7845f5 Improve "Sketch in New Workplane" by adding two way of construction
- point and normal
- point and face
2021-06-27 13:17:09 -04:00
115 changed files with 20089 additions and 5822 deletions

View File

@ -1,6 +1,6 @@
### System information
- **SolveSpace version:** <!--e.g. 3.0~3dd2fc00; go to Help → About...-->
- **SolveSpace version:** <!--e.g. 3.1~70bde63c; go to Help → About… / SolveSpace → About SolveSpace (macOS)-->
- **Operating system:** <!--e.g. Debian testing-->
### Expected behavior

View File

@ -14,13 +14,13 @@ CMAKE_GENERATOR="Unix Makefiles"
CMAKE_PREFIX_PATH=""
if [ "$2" = "arm64" ]; then
OSX_ARCHITECTURE="arm64"
CMAKE_PREFIX_PATH="/tmp/libomp-arm64/libomp/11.0.1"
CMAKE_PREFIX_PATH=$(find /tmp/libomp-arm64/libomp -depth 1)
git apply cmake/libpng-macos-arm64.patch || echo "Could not apply patch, probably already patched..."
mkdir build-arm64 || true
cd build-arm64
elif [ "$2" = "x86_64" ]; then
OSX_ARCHITECTURE="x86_64"
CMAKE_PREFIX_PATH="/tmp/libomp-x86_64/libomp/11.0.1"
CMAKE_PREFIX_PATH=$(find /tmp/libomp-x86_64/libomp -depth 1)
mkdir build || true
cd build
else

View File

@ -1,14 +1,16 @@
#!/bin/sh -xe
if [ "$1" = "ci" ]; then
curl -L https://bintray.com/homebrew/bottles/download_file?file_path=libomp-11.0.1.arm64_big_sur.bottle.tar.gz --output /tmp/libomp-arm64.tar.gz
armloc=$(brew fetch --bottle-tag=arm64_ventura libomp | grep -i downloaded | grep tar.gz | cut -f2 -d:)
x64loc=$(brew fetch --bottle-tag=ventura libomp | grep -i downloaded | grep tar.gz | cut -f2 -d:)
cp $armloc /tmp/libomp-arm64.tar.gz
mkdir /tmp/libomp-arm64 || true
tar -xzvf /tmp/libomp-arm64.tar.gz -C /tmp/libomp-arm64
curl -L https://bintray.com/homebrew/bottles/download_file?file_path=libomp-11.0.1.big_sur.bottle.tar.gz --output /tmp/libomp-x86_64.tar.gz
cp $x64loc /tmp/libomp-x86_64.tar.gz
mkdir /tmp/libomp-x86_64 || true
tar -xzvf /tmp/libomp-x86_64.tar.gz -C /tmp/libomp-x86_64
else
brew install libomp
fi
git submodule update --init extlib/cairo extlib/freetype extlib/libdxfrw extlib/libpng extlib/mimalloc extlib/pixman extlib/zlib
git submodule update --init extlib/cairo extlib/freetype extlib/libdxfrw extlib/libpng extlib/mimalloc extlib/pixman extlib/zlib extlib/eigen

View File

@ -7,4 +7,4 @@ sudo apt-get install -q -y \
libfontconfig1-dev libgtkmm-3.0-dev libpangomm-1.4-dev libgl-dev \
libgl-dev libglu-dev libspnav-dev
git submodule update --init extlib/libdxfrw extlib/mimalloc
git submodule update --init extlib/libdxfrw extlib/mimalloc extlib/eigen

View File

@ -58,34 +58,29 @@ hdiutil create -srcfolder "${app}" "${dmg}"
# sign the .dmg
codesign -s "${MACOS_DEVELOPER_ID}" --timestamp --options runtime -f --deep "${dmg}"
# notarize and store request uuid in variable
notarize_uuid=$(xcrun altool --notarize-app --primary-bundle-id "${bundle_id}" --username "${MACOS_APPSTORE_USERNAME}" --password "${MACOS_APPSTORE_APP_PASSWORD}" --file "${dmg}" 2>&1 | grep RequestUUID | awk '{print $3'})
if ! command -v xcrun >/dev/null || ! xcrun --find notarytool >/dev/null; then
echo "Notarytool is not present in the system. Notarization has failed."
exit 1
fi
echo $notarize_uuid
# Submit the package for notarization
notarization_output=$(
xcrun notarytool submit "${dmg}" \
--apple-id "hello@koenschmeets.nl" \
--password "${MACOS_APPSTORE_APP_PASSWORD}" \
--team-id "8X77K9NDG3" \
--wait 2>&1)
# wait a bit so we don't get errors during checking
sleep 10
success=0
for (( ; ; ))
do
echo "Checking progress..."
progress=$(xcrun altool --notarization-info "${notarize_uuid}" -u "${MACOS_APPSTORE_USERNAME}" -p "${MACOS_APPSTORE_APP_PASSWORD}" 2>&1)
# echo "${progress}"
if [ $? -ne 0 ] || [[ "${progress}" =~ "Invalid" ]] ; then
echo "Error with notarization. Exiting"
break
fi
if [[ "${progress}" =~ "success" ]]; then
success=1
break
else
echo "Not completed yet. Sleeping for 10 seconds"
fi
sleep 10
done
if [ $? -eq 0 ]; then
# Extract the operation ID from the output
operation_id=$(echo "$notarization_output" | awk '/RequestUUID/ {print $NF}')
echo "Notarization submitted. Operation ID: $operation_id"
exit 0
else
echo "Notarization failed. Error: $notarization_output"
exit 1
fi
fi
# staple
xcrun stapler staple "${dmg}"

View File

@ -12,17 +12,19 @@ jobs:
cancel_previous_runs:
runs-on: ubuntu-latest
name: Cancel Previous Runs
permissions:
actions: write
steps:
- uses: styfle/cancel-workflow-action@0.8.0
- uses: styfle/cancel-workflow-action
with:
access_token: ${{ github.token }}
test_ubuntu:
needs: [cancel_previous_runs]
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
name: Test Ubuntu
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Dependencies
run: .github/scripts/install-ubuntu.sh
- name: Build & Test
@ -33,7 +35,7 @@ jobs:
runs-on: windows-2019
name: Test Windows
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Dependencies
run: .github/scripts/install-windows.sh
shell: bash
@ -43,10 +45,10 @@ jobs:
test_macos:
needs: [cancel_previous_runs]
runs-on: macos-10.15
runs-on: macos-latest
name: Test macOS
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Dependencies
run: .github/scripts/install-macos.sh ci
- name: Build & Test
@ -57,7 +59,7 @@ jobs:
name: Build Release Windows
runs-on: windows-2019
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Dependencies
run: .github/scripts/install-windows.sh
shell: bash
@ -65,7 +67,7 @@ jobs:
run: .github/scripts/build-windows.sh release
shell: bash
- name: Upload artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: windows
path: build/bin/RelWithDebInfo/solvespace.exe
@ -75,7 +77,7 @@ jobs:
name: Build Release Windows (OpenMP)
runs-on: windows-2019
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Dependencies
run: .github/scripts/install-windows.sh
shell: bash
@ -83,7 +85,7 @@ jobs:
run: .github/scripts/build-windows.sh release openmp
shell: bash
- name: Upload artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: windows-openmp
path: build/bin/RelWithDebInfo/solvespace-openmp.exe
@ -91,9 +93,9 @@ jobs:
build_release_macos:
needs: [test_ubuntu, test_windows, test_macos]
name: Build Release macOS
runs-on: macos-10.15
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Dependencies
run: .github/scripts/install-macos.sh ci
- name: Build & Test
@ -107,121 +109,56 @@ jobs:
MACOS_APPSTORE_USERNAME: ${{ secrets.MACOS_APPSTORE_USERNAME }}
MACOS_DEVELOPER_ID: ${{ secrets.MACOS_DEVELOPER_ID }}
- name: Upload artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: macos
path: build/bin/SolveSpace.dmg
deploy_snap_amd64:
needs: [test_ubuntu, test_windows, test_macos]
name: Deploy AMD64 Snap
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set Up Source
run: rsync --filter=":- .gitignore" -r ./ pkg/snap/solvespace-snap-src
- name: Build Snap
id: build
uses: diddlesnaps/snapcraft-multiarch-action@v1
with:
path: pkg/snap
- name: Upload & Release to Edge
if: github.event_name == 'push'
uses: snapcore/action-publish@v1
with:
store_login: ${{ secrets.SNAPSTORE_LOGIN }}
snap: ${{ steps.build.outputs.snap }}
release: edge
- name: Upload & Release to Beta + Edge
if: github.event_name == 'release'
uses: snapcore/action-publish@v1
with:
store_login: ${{ secrets.SNAPSTORE_LOGIN }}
snap: ${{ steps.build.outputs.snap }}
release: edge,beta
deploy_snap_arm64:
needs: [test_ubuntu, test_windows, test_macos]
name: Deploy ARM64 Snap
runs-on: ubuntu-latest
steps:
- uses: docker/setup-qemu-action@v1
with:
image: tonistiigi/binfmt@sha256:df15403e06a03c2f461c1f7938b171fda34a5849eb63a70e2a2109ed5a778bde
- uses: actions/checkout@v2
- name: Set Up Source
run: rsync --filter=":- .gitignore" -r ./ pkg/snap/solvespace-snap-src
- name: Build Snap
id: build
uses: diddlesnaps/snapcraft-multiarch-action@v1
with:
path: pkg/snap
architecture: arm64
- name: Upload & Release to Edge
if: github.event_name == 'push'
uses: snapcore/action-publish@v1
with:
store_login: ${{ secrets.SNAPSTORE_LOGIN }}
snap: ${{ steps.build.outputs.snap }}
release: edge
- name: Upload & Release to Beta + Edge
if: github.event_name == 'release'
uses: snapcore/action-publish@v1
with:
store_login: ${{ secrets.SNAPSTORE_LOGIN }}
snap: ${{ steps.build.outputs.snap }}
release: edge,beta
update_edge_release:
name: Update Edge Release
needs: [build_release_windows, build_release_windows_openmp, build_release_macos]
if: github.event_name == 'push' && !cancelled()
runs-on: ubuntu-latest
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- name: Delete Old Edge Release
uses: dev-drprasad/delete-tag-and-release@v0.2.0
with:
delete_release: true
tag_name: edge
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Wait
shell: bash
run: sleep 60
- name: Create New Edge Release
id: create_release
uses: softprops/action-gh-release@35d938cf01f60fbe522917c81be1e892074f6ad6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: edge
name: Edge
prerelease: true
draft: false
body: ${{ github.event.head_commit.message }}
# deploy_snap_amd64:
# needs: [test_ubuntu, test_windows, test_macos]
# name: Deploy AMD64 Snap
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - name: Fetch Tags
# run: git fetch --force --tags
# - name: Set Up Source
# run: rsync --filter=":- .gitignore" -r ./ pkg/snap/solvespace-snap-src
# - name: Build Snap
# uses: snapcore/action-build@v1
# id: build
# with:
# path: pkg/snap
# - name: Upload & Release to Edge
# if: github.event_name == 'push'
# uses: snapcore/action-publish@v1
# env:
# SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPSTORE_LOGIN }}
# with:
# snap: ${{ steps.build.outputs.snap }}
# release: edge
# - name: Upload & Release to Beta + Edge
# if: github.event_name == 'release'
# uses: snapcore/action-publish@v1
# with:
# store_login: ${{ secrets.SNAPSTORE_LOGIN }}
# snap: ${{ steps.build.outputs.snap }}
# release: edge,beta
upload_release_assets:
name: Upload Release Assets
needs: [build_release_windows, build_release_windows_openmp, build_release_macos, update_edge_release]
if: "!cancelled()"
needs: [build_release_windows, build_release_windows_openmp, build_release_macos]
if: "!cancelled() && github.event_name == 'release'"
runs-on: ubuntu-latest
steps:
- name: Download All Workflow Artifacts
uses: actions/download-artifact@v2
uses: actions/download-artifact@v4
- name: Get Release Upload URL
id: get_upload_url
env:
event_name: ${{ github.event_name }}
event: ${{ toJson(github.event) }}
edge_upload_url: ${{ needs.update_edge_release.outputs.upload_url }}
run: |
if [ "$event_name" = "release" ]; then
upload_url=$(echo "$event" | jq -r ".release.upload_url")
else
upload_url="$edge_upload_url"
fi
upload_url=$(echo "$event" | jq -r ".release.upload_url")
echo "::set-output name=upload_url::$upload_url"
echo "Upload URL: $upload_url"
- name: Upload solvespace.exe

56
.github/workflows/source-tarball.yml vendored Normal file
View File

@ -0,0 +1,56 @@
name: Source Tarball
on:
release:
types:
- created
jobs:
create_tarball:
name: Create & Upload Tarball
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
fetch-depth: 0
- name: Pack Tarball
id: pack_tarball
run: |
version="${GITHUB_REF#refs/tags/v}"
dir_name="solvespace-${version}"
archive_name="${dir_name}.tar.xz"
archive_path="${HOME}/${archive_name}"
commit_sha="$GITHUB_SHA"
sed -e 's/^\(include(GetGitCommitHash)\)/#\1/' \
-e 's/^# \(set(GIT_COMMIT_HASH\).*/\1 '"$commit_sha"')/' \
-i CMakeLists.txt
echo "::set-output name=archive_name::${archive_name}"
echo "::set-output name=archive_path::${archive_path}"
cd ..
tar \
--exclude-vcs \
--transform "s:^solvespace:${dir_name}:" \
-cvaf \
${archive_path} \
solvespace
- name: Get Release Upload URL
id: get_upload_url
env:
event: ${{ toJson(github.event) }}
run: |
upload_url=$(echo "$event" | jq -r ".release.upload_url")
echo "::set-output name=upload_url::$upload_url"
echo "Upload URL: $upload_url"
- name: Upload Tarball
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.get_upload_url.outputs.upload_url }}
asset_path: ${{ steps.pack_tarball.outputs.archive_path }}
asset_name: ${{ steps.pack_tarball.outputs.archive_name }}
asset_content_type: binary/octet-stream

View File

@ -12,10 +12,10 @@ on:
jobs:
test_ubuntu:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
name: Test Ubuntu
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Dependencies
run: .github/scripts/install-ubuntu.sh
- name: Build & Test
@ -25,7 +25,7 @@ jobs:
runs-on: windows-2019
name: Test Windows
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Dependencies
run: .github/scripts/install-windows.sh
shell: bash
@ -34,11 +34,28 @@ jobs:
shell: bash
test_macos:
runs-on: macos-10.15
runs-on: macos-latest
name: Test macOS
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Dependencies
run: .github/scripts/install-macos.sh ci
- name: Build & Test
run: .github/scripts/build-macos.sh debug arm64 && .github/scripts/build-macos.sh debug x86_64
test_flatpak:
name: Test Flatpak x86_64
runs-on: ubuntu-latest
container:
image: bilelmoussaoui/flatpak-github-actions:freedesktop-21.08
options: --privileged
steps:
- uses: actions/checkout@v4
with:
submodules: true
fetch-depth: 0
- uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with:
bundle: "solvespace.flatpak"
manifest-path: "pkg/flatpak/com.solvespace.SolveSpace.json"
cache-key: flatpak-builder-${{ github.sha }}

5
.gitignore vendored
View File

@ -14,3 +14,8 @@
/obj-*/
/*.slvs
.vscode/
# Visual Studio
out/
.vs/
CMakeSettings.json

3
.gitmodules vendored
View File

@ -23,3 +23,6 @@
[submodule "extlib/mimalloc"]
path = extlib/mimalloc
url = https://github.com/microsoft/mimalloc
[submodule "extlib/eigen"]
path = extlib/eigen
url = https://gitlab.com/libeigen/eigen.git

View File

@ -1,6 +1,98 @@
Changelog
=========
3.x - development
---
Geometric Modelling Kernel (NURBS)
* Improve the difference boolean operations.
Constraints (new and improved):
* Add Parallel and Perpendicular constraints for 2 faces.
* The equal angle constraint is moved to the `N` shortcut and menu item to allow equal length (`Q`) to be applied to three or four lines.
Allow these constraints to be applied to more entities at once:
* More than two line Segments - equal length.
* More than two Arcs and/or circles - equal diameter/radius.
* Any number of Lines - horizontal or vertical.
* More than two points - horizontal or vertical.
* Point on face can be applied to a point and 1-3 faces at once.
* More than two points coincident.
Sketching
* `Image` sketch elements are not copied in 3d groups (extrude, lathe, revolve, helix) by default. `Toggle Construction` for an image to get the old behavior.
Translations (now in 10 languages!)
* Added Czech cs_CZ.
* Added Japanese ja_JP.
* Update translation for French fr_FR, Russian ru_RU and Chinese zh_CN.
Other User interface changes:
* `CTRL+Tab` hides/shows the toolbar.
* Marquee selection of line segments is now precise.
* Speed up the animation when moving the view, for example when pressing `F2` or `F3`.
* Pressing ESC while drawing a sketch entity now deletes the entity rather than completing it.
* `CTRL+Shift+S` shortcut for "Save As..."
* New option "use camera mouse navigation" for camera (instead of the default model) rotation navigation.
* Sketches can be displayed with only dimensions visible (the button controlling visibility of constraints in the Property Browser has a new state).
* More entity types described in the text screens.
Other
* Merged and improved the experimental Web version (Emscripten port).
* Better Flatpack support.
* Several bug fixes and usability improvements.
* Allow 32 bit SolveSpace to access up to 4GB of RAM to allow working on larger projects.
3.1
---
Constraints:
* Arcs length ratio and difference.
* Arc & Line length ratio and difference.
* Allow comments to be associated with point entities.
Sketching:
* Support for pan, zoom and rotate trackpad gestures on macOS
* Add "exploded view" to sketches via "\\" key. Shows sketch elements separated
by a configurable distance perpendicular to the sketch plane.
* Added Feet-Inches as a unit of measure. Inputs are still in inches.
But the display shows feet, inches, and fraction of an inch.
* Added an optional "pitch" parameter to helix extrusions (in the text window)
* Allow use of Point & Normal to define "sketch-in-new-workplane".
* Update "Property Browser" live while dragging the sketch.
MISC:
* Add a link to the GitHub commit from which SolveSpace was built in the Help
menu.
* Make all points, vectors and normals shown in the Property Browser into
active links. This makes them explorable and selectable.
* Load 16bit PNG images correctly by re-scaling to 8bit.
* Fixed hang when trying to display characters missing from the embedded font.
* The main window vertical size can be as small as the toolbar.
* Configurable "SafeHeight" parameter instead of the fixed 5mm for G-code export.
* Add Spanish / Argentina translation.
* Move "perspective factor", "lighting direction" and "explode distance" from
the "configuration" screen to the "view" screen.
* Add a "∆" suffix to groups which have "force to triangle mesh" ticked
* Gray the group name in the text window for groups with suppressed solid model.
* Added the ability to Link STL files.
* When linking circuit boards (IDF .emn files) show keepout regions as construction entities.
Performance:
* Speed up sketches with many constraints by roughly 8x by using the Eigen
library in the solver. The maximum unknowns increased from 1024 to 2048.
* Add a "suppress dof calculation" setting to groups - increases performance for
complex sketches.
* More changes to the ID list implementation.
3.0
---

View File

@ -8,10 +8,9 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR)
" mkdir build && cd build && cmake ..")
endif()
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH}
list(APPEND CMAKE_MODULE_PATH
"${CMAKE_SOURCE_DIR}/cmake/")
cmake_policy(SET CMP0048 OLD)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
@ -25,6 +24,11 @@ if(CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++")
endif()
if (APPLE)
# Docs say this must be set before the first project() call
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.12" CACHE STRING "macOS minimum supported version")
endif()
# project
# NOTE TO PACKAGERS: The embedded git commit hash is critical for rapid bug triage when the builds
@ -34,10 +38,10 @@ include(GetGitCommitHash)
# and instead uncomment the following, adding the complete git hash of the checkout you are using:
# set(GIT_COMMIT_HASH 0000000000000000000000000000000000000000)
set(solvespace_VERSION_MAJOR 3)
set(solvespace_VERSION_MINOR 0)
string(SUBSTRING "${GIT_COMMIT_HASH}" 0 8 solvespace_GIT_HASH)
project(solvespace LANGUAGES C CXX ASM)
project(solvespace
VERSION 3.1
LANGUAGES C CXX ASM)
set(ENABLE_GUI ON CACHE BOOL
"Whether the graphical interface is enabled")
@ -51,8 +55,11 @@ set(ENABLE_SANITIZERS OFF CACHE BOOL
"Whether to enable Clang's AddressSanitizer and UndefinedBehaviorSanitizer")
set(ENABLE_OPENMP OFF CACHE BOOL
"Whether geometric operations will be parallelized using OpenMP")
set(ENABLE_LTO OFF CACHE BOOL
set(ENABLE_LTO OFF CACHE BOOL
"Whether interprocedural (global) optimizations are enabled")
option(FORCE_VENDORED_Eigen3
"Whether we should use our bundled Eigen even in the presence of a system copy"
OFF)
set(OPENGL 3 CACHE STRING "OpenGL version to use (one of: 1 3)")
@ -84,6 +91,10 @@ endif()
if(MINGW)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -static-libgcc")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static-libgcc -static-libstdc++")
# Link 32 bit SolveSpace with --large-address-aware which allows it to access
# up to 3GB on a properly configured 32 bit Windows and up to 4GB on 64 bit.
# See https://msdn.microsoft.com/en-us/library/aa366778
set(CMAKE_EXE_LINKER_FLAGS "-Wl,--large-address-aware")
endif()
# Ensure that all platforms use 64-bit IEEE floating point operations for consistency;
@ -109,7 +120,12 @@ endif()
if(ENABLE_OPENMP)
find_package( OpenMP REQUIRED )
if(OPENMP_FOUND)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}")
add_library(slvs_openmp INTERFACE)
target_compile_options(slvs_openmp INTERFACE ${OpenMP_CXX_FLAGS})
target_link_libraries(slvs_openmp INTERFACE
${OpenMP_CXX_LIBRARIES})
target_include_directories(slvs_openmp INTERFACE SYSTEM
${OpenMP_CXX_INCLUDE_DIRS})
message(STATUS "found OpenMP, compiling with flags: " ${OpenMP_CXX_FLAGS} )
endif()
endif()
@ -170,6 +186,10 @@ if(APPLE)
set(CMAKE_FIND_FRAMEWORK LAST)
endif()
if(EMSCRIPTEN)
set(M_LIBRARY "" CACHE STRING "libm (not necessary)" FORCE)
endif()
message(STATUS "Using in-tree libdxfrw")
add_subdirectory(extlib/libdxfrw)
@ -181,7 +201,22 @@ set(MI_BUILD_TESTS OFF CACHE BOOL "")
add_subdirectory(extlib/mimalloc EXCLUDE_FROM_ALL)
set(MIMALLOC_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/extlib/mimalloc/include)
if(WIN32 OR APPLE)
if(NOT FORCE_VENDORED_Eigen3)
find_package(Eigen3 CONFIG)
endif()
if(FORCE_VENDORED_Eigen3 OR NOT EIGEN3_INCLUDE_DIRS)
message(STATUS "Using in-tree Eigen")
set(EIGEN3_FOUND YES)
set(EIGEN3_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/eigen)
else()
message(STATUS "Using system Eigen: ${EIGEN3_INCLUDE_DIRS}")
endif()
if(NOT EXISTS "${EIGEN3_INCLUDE_DIRS}")
message(FATAL_ERROR "Eigen 3 not found on system or in-tree")
endif()
if(WIN32 OR APPLE OR EMSCRIPTEN)
# On Win32 and macOS we use vendored packages, since there is little to no benefit
# to trying to find system versions. In particular, trying to link to libraries from
# Homebrew or macOS system libraries into the .app file is highly likely to result
@ -242,7 +277,7 @@ else()
find_package(ZLIB REQUIRED)
find_package(PNG REQUIRED)
find_package(Freetype REQUIRED)
pkg_check_modules(CAIRO REQUIRED cairo)
find_package(Cairo REQUIRED)
endif()
# GUI dependencies
@ -276,7 +311,8 @@ if(ENABLE_GUI)
elseif(APPLE)
find_package(OpenGL REQUIRED)
find_library(APPKIT_LIBRARY AppKit REQUIRED)
set(util_LIBRARIES ${APPKIT_LIBRARY})
elseif(EMSCRIPTEN)
# Everything is built in
else()
find_package(OpenGL REQUIRED)
find_package(SpaceWare)
@ -347,9 +383,19 @@ if(MSVC)
# Same for the (C99) __func__ special variable; we use it only in C++ code.
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /D__func__=__FUNCTION__")
# Multi-processor Compilation
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP")
# We rely on these /we flags. They correspond to the GNU-style flags below as
# follows: /w4062=-Wswitch
set(WARNING_FLAGS "${WARNING_FLAGS} /we4062")
# Link 32 bit SolveSpace with /LARGEADDRESSAWARE which allows it to access
# up to 3GB on a properly configured 32 bit Windows and up to 4GB on 64 bit.
# See https://msdn.microsoft.com/en-us/library/aa366778
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /LARGEADDRESSAWARE")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /LARGEADDRESSAWARE")
endif()
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")

313
README.md
View File

@ -1,172 +1,242 @@
# SolveSpace
<img src="res/freedesktop/solvespace-scalable.svg" width="70" height="70" alt="SolveSpace Logo" align="left">
SolveSpace
==========
[![Build Status](https://github.com/solvespace/solvespace/workflows/CD/badge.svg)](https://github.com/solvespace/solvespace/actions)
[![solvespace](https://snapcraft.io/solvespace/badge.svg)](https://snapcraft.io/solvespace)
[![solvespace](https://snapcraft.io/solvespace/trending.svg?name=0)](https://snapcraft.io/solvespace)
This repository contains the source code of [SolveSpace][], a parametric
2d/3d CAD.
2d/3d CAD tool.
[solvespace]: http://solvespace.com
[solvespace]: https://solvespace.com
Community
---------
## Community
The official SolveSpace [website][sswebsite] has [tutorials][sstutorial],
[reference manual][ssref] and a [forum][ssforum]; there is also an official
IRC channel [#solvespace at irc.freenode.net][ssirc].
IRC channel [#solvespace at web.libera.chat][ssirc].
[sswebsite]: http://solvespace.com/
[ssref]: http://solvespace.com/ref.pl
[sstutorial]: http://solvespace.com/tutorial.pl
[ssforum]: http://solvespace.com/forum.pl
[ssirc]: https://webchat.freenode.net/?channels=solvespace
[ssirc]: https://web.libera.chat/#solvespace
Installation
------------
## Installation
### Via official binary packages
### Via Official Packages
_Official_ release binary packages for macOS (>=10.6 64-bit) and Windows (>=Vista 32-bit) are
available via [GitHub releases][rel]. These packages are automatically built by
the SolveSpace maintainers for each stable release.
_Official_ release packages for macOS (>=10.6 64-bit) and Windows
(>=Vista 32-bit) are available via [GitHub releases][rel]. These packages are
automatically built by the SolveSpace maintainers for each stable release.
[rel]: https://github.com/solvespace/solvespace/releases
### Via Flathub
Official releases can be installed as a Flatpak from Flathub.
[Get SolveSpace from Flathub](https://flathub.org/apps/details/com.solvespace.SolveSpace)
These should work on any Linux distribution that supports Flatpak.
### Via Snap Store
Builds from master are automatically released to the `edge` channel in the Snap Store. Those packages contain the latest improvements, but receive less testing than release builds.
Official releases can be installed from the `stable` channel.
Future official releases will appear in the `stable` channel.
Builds from master are automatically released to the `edge` channel in the Snap
Store. Those packages contain the latest improvements, but receive less testing
than release builds.
[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/solvespace)
Or install from a terminal:
```
snap install --edge solvespace
```sh
# for the latest stable release:
snap install solvespace
# for the bleeding edge builds from master:
snap install solvespace --edge
```
### Via third-party binary packages
### Via automated edge builds
_Third-party_ nightly binary packages for Debian and Ubuntu are available
via [notesalexp.org][notesalexp]. These packages are automatically built from non-released
source code. The SolveSpace maintainers do not control the contents of these packages
and cannot guarantee their functionality.
> :warning: **Edge builds might be unstable or contain severe bugs!**
> They are intended for experienced users to test new features or verify bugfixes.
[notesalexp]: https://notesalexp.org/packages/en/source/solvespace/
Cutting edge builds from the latest master commit are available as zip archives
from the following links:
- [macOS](https://nightly.link/solvespace/solvespace/workflows/cd/master/macos.zip)
- [Windows](https://nightly.link/solvespace/solvespace/workflows/cd/master/windows.zip)
- [Windows with OpenMP enabled](https://nightly.link/solvespace/solvespace/workflows/cd/master/windows-openmp.zip)
Extract the downloaded archive and install or execute the contained file as is
appropriate for your platform.
### Via source code
See below.
Irrespective of the OS used, before building, check out the project and the
necessary submodules:
Building on Linux
-----------------
```sh
git clone https://github.com/solvespace/solvespace
cd solvespace
git submodule update --init
```
You will need `git`. See the platform specific instructions below to install it.
## Building on Linux
### Building for Linux
You will need the usual build tools, CMake, zlib, libpng, cairo, freetype.
To build the GUI, you will need fontconfig, gtkmm 3.0 (version 3.16 or later), pangomm 1.4,
OpenGL and OpenGL GLU, and optionally, the Space Navigator client library.
On a Debian derivative (e.g. Ubuntu) these can be installed with:
You will need the usual build tools, CMake, zlib, libpng, cairo, freetype. To
build the GUI, you will need fontconfig, gtkmm 3.0 (version 3.16 or later),
pangomm 1.4, OpenGL and OpenGL GLU, and optionally, the Space Navigator client
library. On a Debian derivative (e.g. Ubuntu) these can be installed with:
sudo apt install git build-essential cmake zlib1g-dev libpng-dev \
libcairo2-dev libfreetype6-dev libjson-c-dev \
libfontconfig1-dev libgtkmm-3.0-dev libpangomm-1.4-dev \
libgl-dev libglu-dev libspnav-dev
```sh
sudo apt install git build-essential cmake zlib1g-dev libpng-dev \
libcairo2-dev libfreetype6-dev libjson-c-dev \
libfontconfig1-dev libgtkmm-3.0-dev libpangomm-1.4-dev \
libgl-dev libglu-dev libspnav-dev
```
On a Redhat derivative (e.g. Fedora) the dependencies can be installed with:
On a RedHat derivative (e.g. Fedora) the dependencies can be installed with:
sudo dnf install git gcc-c++ cmake zlib-devel libpng-devel \
cairo-devel freetype-devel json-c-devel \
fontconfig-devel gtkmm30-devel pangomm-devel \
mesa-libGL-devel mesa-libGLU-devel libspnav-devel
```sh
sudo dnf install git gcc-c++ cmake zlib-devel libpng-devel \
cairo-devel freetype-devel json-c-devel \
fontconfig-devel gtkmm30-devel pangomm-devel \
mesa-libGL-devel mesa-libGLU-devel libspnav-devel
```
Before building, check out the project and the necessary submodules:
git clone https://github.com/solvespace/solvespace
cd solvespace
git submodule update --init extlib/libdxfrw extlib/mimalloc
Before building, [check out the project and the necessary submodules](#via-source-code).
After that, build SolveSpace as following:
mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DENABLE_OPENMP=ON
make
sudo make install
```sh
mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DENABLE_OPENMP=ON
make
Link Time Optimization is supported by adding -DENABLE_LTO=ON to cmake at the
# Optionally
sudo make install
```
Link Time Optimization is supported by adding `-DENABLE_LTO=ON` to cmake at the
expense of longer build time.
The graphical interface is built as `build/bin/solvespace`, and the command-line interface
is built as `build/bin/solvespace-cli`. It is possible to build only the command-line interface by passing the `-DENABLE_GUI=OFF` flag to the cmake invocation.
The graphical interface is built as `build/bin/solvespace`, and the command-line
interface is built as `build/bin/solvespace-cli`. It is possible to build only
the command-line interface by passing the `-DENABLE_GUI=OFF` flag to the cmake
invocation.
### Building for Windows
Ubuntu will require 20.04 or above. Cross-compiling with WSL is also confirmed to work.
Ubuntu will require 20.04 or above. Cross-compiling with WSL is also confirmed
to work.
You will need the usual build tools, CMake, a Windows cross-compiler, and flatc. On a Debian derivative (e.g. Ubuntu) these can be installed with:
You will need the usual build tools, CMake, and a Windows cross-compiler. On a
Debian derivative (e.g. Ubuntu) these can be installed with:
apt-get install git build-essential cmake mingw-w64
```sh
apt-get install git build-essential cmake mingw-w64
```
Before building, check out the project and the necessary submodules:
git clone https://github.com/solvespace/solvespace
cd solvespace
git submodule update --init
Before building, [check out the project and the necessary submodules](#via-source-code).
Build 64-bit SolveSpace with the following:
mkdir build
cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchain-mingw64.cmake \
-DCMAKE_BUILD_TYPE=Release \
-DFLATC=$(which flatc)
make
```sh
mkdir build
cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchain-mingw64.cmake \
-DCMAKE_BUILD_TYPE=Release
make
```
The graphical interface is built as `build/bin/solvespace.exe`, and the command-line interface
is built as `build/bin/solvespace-cli.exe`.
The graphical interface is built as `build/bin/solvespace.exe`, and the
command-line interface is built as `build/bin/solvespace-cli.exe`.
Space Navigator support will not be available.
If using Ubuntu to cross-compile, Ubuntu 17.10 or newer (or, alternatively, MinGW from the Ubuntu
17.10 repositories) is required.
### Building for web (very experimental)
Building on macOS
-----------------
**Please note that this port contains many critical bugs and unimplemented core functions.**
You will need git, XCode tools and CMake. Git and CMake can be installed
You will need the usual build tools, cmake and [Emscripten][]. On a Debian derivative (e.g. Ubuntu) dependencies other than Emscripten can be installed with:
```sh
apt-get install git build-essential cmake
```
First, install and prepare `emsdk`:
```sh
git clone https://github.com/emscripten-core/emsdk
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
cd ..
```
Before building, [check out the project and the necessary submodules](#via-source-code).
After that, build SolveSpace as following:
```sh
mkdir build
cd build
emcmake cmake .. -DCMAKE_BUILD_TYPE=Release -DENABLE_LTO="ON" -DENABLE_TESTS="OFF" -DENABLE_CLI="OFF" -DENABLE_COVERAGE="OFF"
make
```
The graphical interface is built as multiple files in the `build/bin` directory with names
starting with `solvespace`. It can be run locally with `emrun build/bin/solvespace.html`.
The command-line interface is not available.
[emscripten]: https://emscripten.org/
## Building on macOS
You will need git, XCode tools, CMake and libomp. Git, CMake and libomp can be installed
via [Homebrew][]:
brew install git cmake
```sh
brew install git cmake libomp
```
XCode has to be installed via AppStore or [the Apple website][appledeveloper];
it requires a free Apple ID.
Before building, check out the project and the necessary submodules:
git clone https://github.com/solvespace/solvespace
cd solvespace
git submodule update --init
Before building, [check out the project and the necessary submodules](#via-source-code).
After that, build SolveSpace as following:
mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DENABLE_OPENMP=ON
make
```sh
mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DENABLE_OPENMP=ON
make
```
Link Time Optimization is supported by adding -DENABLE_LTO=ON to cmake at the
Link Time Optimization is supported by adding `-DENABLE_LTO=ON` to cmake at the
expense of longer build time.
Alternatively, generate an XCode project, open it, and build the "Release" scheme:
mkdir build
cd build
cmake .. -G Xcode
```sh
mkdir build
cd build
cmake .. -G Xcode
```
The application is built in `build/bin/SolveSpace.app`, the graphical interface executable
is `build/bin/SolveSpace.app/Contents/MacOS/SolveSpace`, and the command-line interface executable
@ -175,26 +245,26 @@ is `build/bin/SolveSpace.app/Contents/MacOS/solvespace-cli`.
[homebrew]: https://brew.sh/
[appledeveloper]: https://developer.apple.com/download/
Building on OpenBSD
-------------------
## Building on OpenBSD
You will need git, cmake, libexecinfo, libpng, gtk3mm and pangomm.
These can be installed from the ports tree:
pkg_add -U git cmake libexecinfo png json-c gtk3mm pangomm
```sh
pkg_add -U git cmake libexecinfo png json-c gtk3mm pangomm
```
Before building, check out the project and the necessary submodules:
git clone https://github.com/solvespace/solvespace
cd solvespace
git submodule update --init extlib/libdxfrw extlib/mimalloc
Before building, [check out the project and the necessary submodules](#via-source-code).
After that, build SolveSpace as following:
mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make
```sh
mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make
sudo make install
```
Unfortunately, on OpenBSD, the produced executables are not filesystem location independent
and must be installed before use. By default, the graphical interface is installed to
@ -202,33 +272,35 @@ and must be installed before use. By default, the graphical interface is install
`/usr/local/bin/solvespace-cli`. It is possible to build only the command-line interface
by passing the `-DENABLE_GUI=OFF` flag to the cmake invocation.
Building on Windows
-------------------
## Building on Windows
You will need [git][gitwin], [cmake][cmakewin] and a C++ compiler
(either Visual C++ or MinGW). If using Visual C++, Visual Studio 2015
or later is required.
If gawk is in your path be sure it is a proper Windows port that can handle CL LF line endings.
If not CMake may fail in libpng due to some awk scripts - issue #1228.
Before building, [check out the project and the necessary submodules](#via-source-code).
### Building with Visual Studio IDE
Check out the git submodules. Create a directory `build` in
Create a directory `build` in
the source tree and point cmake-gui to the source tree and that directory.
Press "Configure" and "Generate", then open `build\solvespace.sln` with
Visual C++ and build it.
### Building with Visual Studio in a command prompt
First, ensure that git and cl (the Visual C++ compiler driver) are in your
First, ensure that `git` and `cl` (the Visual C++ compiler driver) are in your
`%PATH%`; the latter is usually done by invoking `vcvarsall.bat` from your
Visual Studio install. Then, run the following in cmd or PowerShell:
git clone https://github.com/solvespace/solvespace
cd solvespace
git submodule update --init
mkdir build
cd build
cmake .. -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release
nmake
```bat
mkdir build
cd build
cmake .. -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release
nmake
```
### Building with MinGW
@ -238,25 +310,22 @@ Space Navigator support will be disabled.
First, ensure that git and gcc are in your `$PATH`. Then, run the following
in bash:
git clone https://github.com/solvespace/solvespace
cd solvespace
git submodule update --init
mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make
```sh
mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make
```
[gitwin]: https://git-scm.com/download/win
[cmakewin]: http://www.cmake.org/download/#latest
[mingw]: http://www.mingw.org/
Contributing
------------
## Contributing
See the [guide for contributors](CONTRIBUTING.md) for the best way to file issues, contribute code,
and debug SolveSpace.
License
-------
## License
SolveSpace is distributed under the terms of the [GPL v3](COPYING.txt) or later.

View File

@ -4,12 +4,12 @@ function(disable_warnings)
if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID STREQUAL "Clang")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -w" PARENT_SCOPE)
elseif(CMAKE_C_COMPILER_ID STREQUAL "MSVC")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /W0" PARENT_SCOPE)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /W0 /MP" PARENT_SCOPE)
endif()
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -w" PARENT_SCOPE)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W0" PARENT_SCOPE)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W0 /MP" PARENT_SCOPE)
endif()
endfunction()

75
cmake/FindCairo.cmake Normal file
View File

@ -0,0 +1,75 @@
# - Try to find Cairo
# Once done, this will define
#
# CAIRO_FOUND - system has Cairo
# CAIRO_INCLUDE_DIRS - the Cairo include directories
# CAIRO_LIBRARIES - link these to use Cairo
#
# Copyright (C) 2012 Raphael Kubo da Costa <rakuco@webkit.org>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND ITS CONTRIBUTORS ``AS
# IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR ITS
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
find_package(PkgConfig)
pkg_check_modules(PC_CAIRO QUIET cairo)
find_path(CAIRO_INCLUDE_DIRS
NAMES cairo.h
HINTS ${PC_CAIRO_INCLUDEDIR}
${PC_CAIRO_INCLUDE_DIRS}
PATH_SUFFIXES cairo
)
find_library(CAIRO_LIBRARIES
NAMES cairo
HINTS ${PC_CAIRO_LIBDIR}
${PC_CAIRO_LIBRARY_DIRS}
)
if (CAIRO_INCLUDE_DIRS)
if (EXISTS "${CAIRO_INCLUDE_DIRS}/cairo-version.h")
file(READ "${CAIRO_INCLUDE_DIRS}/cairo-version.h" CAIRO_VERSION_CONTENT)
string(REGEX MATCH "#define +CAIRO_VERSION_MAJOR +([0-9]+)" _dummy "${CAIRO_VERSION_CONTENT}")
set(CAIRO_VERSION_MAJOR "${CMAKE_MATCH_1}")
string(REGEX MATCH "#define +CAIRO_VERSION_MINOR +([0-9]+)" _dummy "${CAIRO_VERSION_CONTENT}")
set(CAIRO_VERSION_MINOR "${CMAKE_MATCH_1}")
string(REGEX MATCH "#define +CAIRO_VERSION_MICRO +([0-9]+)" _dummy "${CAIRO_VERSION_CONTENT}")
set(CAIRO_VERSION_MICRO "${CMAKE_MATCH_1}")
set(CAIRO_VERSION "${CAIRO_VERSION_MAJOR}.${CAIRO_VERSION_MINOR}.${CAIRO_VERSION_MICRO}")
endif ()
endif ()
if ("${Cairo_FIND_VERSION}" VERSION_GREATER "${CAIRO_VERSION}")
message(FATAL_ERROR "Required version (" ${Cairo_FIND_VERSION} ") is higher than found version (" ${CAIRO_VERSION} ")")
endif ()
include(FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS(Cairo REQUIRED_VARS CAIRO_INCLUDE_DIRS CAIRO_LIBRARIES
VERSION_VAR CAIRO_VERSION)
mark_as_advanced(
CAIRO_INCLUDE_DIRS
CAIRO_LIBRARIES
)

View File

@ -16,7 +16,7 @@ if(UNIX)
# Support the REQUIRED and QUIET arguments, and set SPACEWARE_FOUND if found.
include(FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS(SPACEWARE DEFAULT_MSG
find_package_handle_standard_args(SpaceWare DEFAULT_MSG
SPACEWARE_LIBRARY SPACEWARE_INCLUDE_DIR)
if(SPACEWARE_FOUND)

View File

@ -15,11 +15,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleVersion</key>
<string>${solvespace_VERSION_MAJOR}.${solvespace_VERSION_MINOR}~${solvespace_GIT_HASH}</string>
<string>${PROJECT_VERSION}~${solvespace_GIT_HASH}</string>
<key>CFBundleShortVersionString</key>
<string>${solvespace_VERSION_MAJOR}.${solvespace_VERSION_MINOR}</string>
<string>${PROJECT_VERSION}</string>
<key>NSHumanReadableCopyright</key>
<string>© 2008-2016 Jonathan Westhues and other authors</string>
<string>© 2008-2024 Jonathan Westhues and other authors</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSMainNibFile</key>

View File

@ -0,0 +1,20 @@
set(EMSCRIPTEN 1)
set(CMAKE_C_OUTPUT_EXTENSION ".o")
set(CMAKE_CXX_OUTPUT_EXTENSION ".o")
set(CMAKE_EXECUTABLE_SUFFIX ".html")
set(CMAKE_SIZEOF_VOID_P 4)
set_property(GLOBAL PROPERTY TARGET_SUPPORTS_SHARED_LIBS FALSE)
# FIXME(emscripten): Suppress non-c-typedef-for-linkage warnings in solvespace.h
add_compile_options(-Wno-non-c-typedef-for-linkage)
add_link_options(-s EXPORTED_RUNTIME_METHODS=[allocate])
# Enable optimization. Workaround for "too many locals" error when runs on browser.
if(CMAKE_BUILD_TYPE STREQUAL Release)
add_compile_options(-O2)
else()
add_compile_options(-O1)
endif()

View File

@ -0,0 +1,8 @@
set(CMAKE_SYSTEM_NAME Emscripten)
set(TRIPLE asmjs-unknown-emscripten)
set(CMAKE_C_COMPILER emcc)
set(CMAKE_CXX_COMPILER em++)
set(M_LIBRARY m)

View File

@ -4,3 +4,7 @@ if(MSVC)
set(CMAKE_C_FLAGS_RELEASE_INIT "/MT /O2 /Ob2 /D NDEBUG")
set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "/MT /Zi /O2 /Ob1 /D NDEBUG")
endif()
if(EMSCRIPTEN)
set(CMAKE_C_FLAGS_DEBUG_INIT "-g4")
endif()

View File

@ -4,3 +4,7 @@ if(MSVC)
set(CMAKE_CXX_FLAGS_RELEASE_INIT "/MT /O2 /Ob2 /D NDEBUG")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "/MT /Zi /O2 /Ob1 /D NDEBUG")
endif()
if(EMSCRIPTEN)
set(CMAKE_CXX_FLAGS_DEBUG_INIT "-g4")
endif()

View File

@ -9,25 +9,25 @@ by pointers from the entity (font, extra points, etc...)
Entities in a sketch are kept in a global array (IdList) referenced by a unique
Id (handle) and can be looked up by Id in log(n) time via binary search. In
order to use binary seach the array must be kept in order sorted by Id. One
order to use binary search the array must be kept in order sorted by Id. One
problem is that insertion takes O(n) time because half the list (on average)
must be shifted to make room for a new item.
The IdList class is a template and is used for more than entites.
The IdList class is a template and is used for more than entities.
EntityMap:
==========
Another important structure is the EntityMap and EntityKey defined in sketch.h
This is what allows SovleSpace to update groups when earlier groups in the
sketch are changed. If a rectangle is extruded to a box and items are
constrained to entites on that box, the user can go back to the sketch and
modify it. Entites can be added, modified an even deleted. So long as the
entites that are later used to build upon are kept the later extrude group will
constrained to entities on that box, the user can go back to the sketch and
modify it. Entities can be added, modified an even deleted. So long as the
entities that are later used to build upon are kept the later extrude group will
pick up the changes from the 2D sketch and anything build on it will remain.
The way this works is that each group has a member called remap, which is one of
these maps. This is where my understanding is fuzzy. At the end of Group.cpp is
a function called Group::CopyEntity() which is used to make new sketch entites
a function called Group::CopyEntity() which is used to make new sketch entities
when a group is created. These are generally copies of entities in the previous
group, but there are exceptions. A point will be used to generate a line when
extruding a 2D sketch. A point will also be "copied" to a circle for a Lathe
@ -35,7 +35,7 @@ group. For this reason, the entity key is derived by combining its previous key
with something often called the CopyNumber or just remap (unfortunate).
When a group is regenerated (the first time, or after a previous one is
modified) entites are copied from the old group to the new one. For Step
modified) entities are copied from the old group to the new one. For Step
Translating and Rotating there may be many copies, and the copy number is
literally N for the Nth copy except for the last one which gets an enum - it is
common to constrain the last item, so it gets a large unique number so that
@ -45,5 +45,5 @@ Remap that was created the same way. This is how constructions are preserved
across underlying changes.
There are some hard limits used in the hash table for the remap mechanism which
limit the number of entites in a group (but not the global sketch).
limit the number of entities in a group (but not the global sketch).

View File

@ -46,7 +46,7 @@ POINT_N_ROT_TRANS: Rotates a point via quaternion param[3],param[4],param[5],par
POINT_N_COPY: A non-transformed copy of a point - numeric copy?
POINT_N_ROT_AA: A point rotated arount point param[0],param[1],param[2] Where the
POINT_N_ROT_AA: A point rotated around point param[0],param[1],param[2] Where the
angle is given by param[3]*timesApplied (times 2?) and the axis
of rotation defined by param[4],param[5],param[6]
@ -130,7 +130,7 @@ the entity itself.
The ForceTo() functions are shortcuts for using the solver. They are passed the
desired location of a point (or orientation of a normal...) and have the opportunity
to back-calculate what the group parameters should be to place it there. This is
used for mouse dragging of copied entites. It is notable that the constraints will
used for mouse dragging of copied entities. It is notable that the constraints will
still be applied afterward, but this is a good shortcut.
When creating a new entity transformation, the first thing to do is define the

View File

@ -6,3 +6,8 @@ add_executable(CDemo
target_link_libraries(CDemo
slvs)
if(EMSCRIPTEN)
set_target_properties(CDemo PROPERTIES
LINK_FLAGS "-s TOTAL_MEMORY=134217728")
endif()

1
extlib/eigen Submodule

@ -0,0 +1 @@
Subproject commit 3147391d946bb4b6c68edd901f2add6ac1f31f8c

@ -1 +1 @@
Subproject commit 4e643b6d3178e0ea2a093b7e14fe621631a91e4b
Subproject commit f819dbb4e4813fab464aee16770f39f11476bfea

View File

@ -113,6 +113,10 @@ typedef struct {
#define SLVS_C_WHERE_DRAGGED 100031
#define SLVS_C_CURVE_CURVE_TANGENT 100032
#define SLVS_C_LENGTH_DIFFERENCE 100033
#define SLVS_C_ARC_ARC_LEN_RATIO 100034
#define SLVS_C_ARC_LINE_LEN_RATIO 100035
#define SLVS_C_ARC_ARC_DIFFERENCE 100036
#define SLVS_C_ARC_LINE_DIFFERENCE 100037
typedef struct {
Slvs_hConstraint h;

View File

@ -1,69 +1,97 @@
{
"$schema": "https://raw.githubusercontent.com/TingPing/flatpak-manifest-schema/master/flatpak-manifest.schema",
"app-id": "com.solvespace.SolveSpace",
"runtime": "org.freedesktop.Platform",
"runtime-version": "20.08",
"runtime-version": "21.08",
"sdk": "org.freedesktop.Sdk",
"finish-args": [
/* Access to display server and OpenGL */
"--device=dri",
"--share=ipc",
"--socket=fallback-x11",
"--socket=wayland",
"--device=dri",
/* Access to save files */
"--filesystem=home"
"--socket=wayland"
],
"cleanup": [
"/include",
"/lib/*/include",
"*.a",
"*.la",
"*.m4",
"/lib/libslvs*.so*",
"/lib/libglibmm_generate_extra_defs*.so*",
"/share/pkgconfig",
"*.pc",
"/share/man",
"/share/doc",
"/lib/cmake",
"/lib/pkgconfig",
"/share/aclocal",
/* mm-common junk */
"/bin/mm-common-prepare",
"/share/mm-common"
"/share/pkgconfig",
"*.la"
],
"command": "solvespace",
"modules": [
{
"name": "mm-common",
"sources": [
{
"type": "archive",
"url": "https://download.gnome.org/sources/mm-common/1.0/mm-common-1.0.2.tar.xz",
"sha256": "a2a99f3fa943cf662f189163ed39a2cfc19a428d906dd4f92b387d3659d1641d"
}
]
},
{
"name": "sigc++",
"config-opts": [
"--disable-documentation"
],
"sources": [
{
"type": "archive",
"url": "https://download.gnome.org/sources/libsigc++/2.10/libsigc%2B%2B-2.10.6.tar.xz",
"sha256": "dda176dc4681bda9d5a2ac1bc55273bdd381662b7a6d49e918267d13e8774e1b"
}
]
},
{
"name": "glibmm",
"config-opts": [],
"buildsystem": "meson",
"sources": [
{
"type": "archive",
"url": "https://download.gnome.org/sources/glibmm/2.64/glibmm-2.64.5.tar.xz",
"sha256": "508fc86e2c9141198aa16c225b16fd6b911917c0d3817602652844d0973ea386"
"url": "https://download.gnome.org/sources/mm-common/1.0/mm-common-1.0.4.tar.xz",
"sha256": "e954c09b4309a7ef93e13b69260acdc5738c907477eb381b78bb1e414ee6dbd8",
"x-checker-data": {
"type": "gnome",
"name": "mm-common",
"stable-only": true
}
}
],
"cleanup": [
"/bin",
"/share/doc",
"/share/man",
"/share/mm-common"
]
},
{
"name": "sigc++",
"buildsystem": "meson",
"config-opts": [
"-Dbuild-examples=false"
],
"sources": [
{
"type": "archive",
"url": "https://download.gnome.org/sources/libsigc++/2.10/libsigc++-2.10.8.tar.xz",
"sha256": "235a40bec7346c7b82b6a8caae0456353dc06e71f14bc414bcc858af1838719a",
"x-checker-data": {
"type": "gnome",
"name": "libsigc++",
"stable-only": true,
"versions": {
"<": "3.0.0"
}
}
}
],
"cleanup": [
"/lib/sigc++-*"
]
},
{
"name": "glibmm",
"buildsystem": "meson",
"config-opts": [
"-Dbuild-examples=false"
],
"sources": [
{
"type": "archive",
"url": "https://download.gnome.org/sources/glibmm/2.66/glibmm-2.66.4.tar.xz",
"sha256": "199ace5682d81b15a1d565480b4a950682f2db6402c8aa5dd7217d71edff81d5",
"x-checker-data": {
"type": "gnome",
"name": "glibmm",
"stable-only": true,
"versions": {
"<": "2.68.0"
}
}
}
],
"cleanup": [
"/lib/giomm-*",
"/lib/glibmm-*",
"/lib/libglibmm_generate_extra_defs-*.so*"
]
},
{
@ -74,76 +102,152 @@
"sources": [
{
"type": "archive",
"url": "http://ftp.gnome.org/pub/GNOME/sources/cairomm/1.12/cairomm-1.12.0.tar.xz",
"sha256": "a54ada8394a86182525c0762e6f50db6b9212a2109280d13ec6a0b29bfd1afe6"
"url": "https://download.gnome.org/sources/cairomm/1.12/cairomm-1.12.0.tar.xz",
"sha256": "a54ada8394a86182525c0762e6f50db6b9212a2109280d13ec6a0b29bfd1afe6",
"x-checker-data": {
"type": "gnome",
"name": "cairomm",
"stable-only": true,
"versions": {
"<": "1.16.0"
}
}
}
],
"cleanup": [
"/lib/cairomm-*"
]
},
{
"name": "pangomm",
"config-opts": [
"--disable-documentation"
],
"sources": [
{
"type": "archive",
"url": "http://ftp.gnome.org/pub/GNOME/sources/pangomm/2.40/pangomm-2.40.2.tar.xz",
"sha256": "0a97aa72513db9088ca3034af923484108746dba146e98ed76842cf858322d05"
}
]
},
{
"name": "atkmm",
"config-opts": [
"--disable-documentation"
],
"sources": [
{
"type": "archive",
"url": "http://ftp.gnome.org/pub/GNOME/sources/atkmm/2.28/atkmm-2.28.0.tar.xz",
"sha256": "4c4cfc917fd42d3879ce997b463428d6982affa0fb660cafcc0bc2d9afcedd3a"
}
]
},
{
"name": "gtkmm",
"config-opts": [],
"buildsystem": "meson",
"sources": [
{
"type": "archive",
"url": "https://download.gnome.org/sources/gtkmm/3.24/gtkmm-3.24.4.tar.xz",
"sha256": "9beb71c3e90cfcfb790396b51e3f5e7169966751efd4f3ef9697114be3be6743"
"url": "https://download.gnome.org/sources/pangomm/2.46/pangomm-2.46.2.tar.xz",
"sha256": "57442ab4dc043877bfe3839915731ab2d693fc6634a71614422fb530c9eaa6f4",
"x-checker-data": {
"type": "gnome",
"name": "pangomm",
"stable-only": true,
"versions": {
"<": "2.48.0"
}
}
}
],
"cleanup": [
"/lib/pangomm-*"
]
},
{
"name": "atkmm",
"buildsystem": "meson",
"sources": [
{
"type": "archive",
"url": "https://download.gnome.org/sources/atkmm/2.28/atkmm-2.28.2.tar.xz",
"sha256": "a0bb49765ceccc293ab2c6735ba100431807d384ffa14c2ebd30e07993fd2fa4",
"x-checker-data": {
"type": "gnome",
"name": "atkmm",
"stable-only": true,
"versions": {
"<": "2.30.0"
}
}
}
],
"cleanup": [
"/lib/atkmm-*"
]
},
{
"name": "gtkmm",
"buildsystem": "meson",
"config-opts": [
"-Dbuild-demos=false",
"-Dbuild-tests=false"
],
"sources": [
{
"type": "archive",
"url": "https://download.gnome.org/sources/gtkmm/3.24/gtkmm-3.24.6.tar.xz",
"sha256": "4b3e142e944e1633bba008900605c341a93cfd755a7fa2a00b05d041341f11d6",
"x-checker-data": {
"type": "gnome",
"name": "gtkmm",
"stable-only": true,
"versions": {
"<": "4.0.0"
}
}
}
],
"cleanup": [
"/lib/gdkmm-*",
"/lib/gtkmm-*"
]
},
{
"name": "eigen",
"buildsystem": "cmake-ninja",
"builddir": true,
"sources": [
{
"type": "archive",
"url": "https://gitlab.com/libeigen/eigen/-/archive/3.4.0/eigen-3.4.0.tar.gz",
"sha256": "8586084f71f9bde545ee7fa6d00288b264a2b7ac3607b974e54d13e7162c1c72",
"x-checker-data": {
"type": "anitya",
"project-id": 13751,
"stable-only": true,
"url-template": "https://gitlab.com/libeigen/eigen/-/archive/$version/eigen-$version.tar.gz"
}
}
],
"cleanup": [
"*"
]
},
{
"name": "libjson-c",
"buildsystem": "cmake-ninja",
"builddir": true,
"config-opts": [
"-DBUILD_STATIC_LIBS=OFF",
"-DENABLE_THREADING=ON"
],
"sources": [
{
/* 0.15-nodoc doesn't build */
"type": "archive",
"url": "https://s3.amazonaws.com/json-c_releases/releases/json-c-0.13.1-nodoc.tar.gz",
"sha256": "94a26340c0785fcff4f46ff38609cf84ebcd670df0c8efd75d039cc951d80132"
"url": "https://s3.amazonaws.com/json-c_releases/releases/json-c-0.16.tar.gz",
"sha256": "8e45ac8f96ec7791eaf3bb7ee50e9c2100bbbc87b8d0f1d030c5ba8a0288d96b",
"x-checker-data": {
"type": "anitya",
"project-id": 1477,
"stable-only": true,
"url-template": "https://s3.amazonaws.com/json-c_releases/releases/json-c-$version.tar.gz"
}
}
],
"buildsystem": "cmake",
"builddir": true
]
},
{
"name": "SolveSpace",
"name": "solvespace",
"buildsystem": "cmake-ninja",
"builddir": true,
"config-opts": [
"-DFLATPAK=ON",
"-DENABLE_TESTS=OFF"
],
"sources": [
{
"type": "dir",
"path": "../.."
}
],
"buildsystem": "cmake",
"builddir": true,
"config-opts": [
"-DFLATPAK=ON",
"-DENABLE_CLI=OFF",
"-DENABLE_TESTS=OFF"
"cleanup": [
"/lib/libslvs*.so*"
]
}
]

View File

@ -1,5 +1,5 @@
name: solvespace
base: core18
base: core22
summary: Parametric 2d/3d CAD
adopt-info: solvespace
description: |
@ -14,6 +14,8 @@ description: |
confinement: strict
license: GPL-3.0
compression: lzo
grade: stable
layout:
/usr/share/solvespace:
@ -23,13 +25,13 @@ apps:
solvespace:
command: usr/bin/solvespace
desktop: solvespace.desktop
extensions: [gnome-3-34]
extensions: [gnome]
plugs: [opengl, unity7, home, removable-media, gsettings, network]
environment:
__EGL_VENDOR_LIBRARY_DIRS: $SNAP/gnome-platform/usr/share/glvnd/egl_vendor.d:$SNAP/usr/share/glvnd/egl_vendor.d
GTK_USE_PORTAL: "0"
cli:
command: usr/bin/solvespace-cli
extensions: [gnome-3-34]
extensions: [gnome]
plugs: [home, removable-media, network]
parts:
@ -38,15 +40,15 @@ parts:
source: ./solvespace-snap-src
source-type: local
override-pull: |
snapcraftctl pull
version_major=$(grep "solvespace_VERSION_MAJOR" CMakeLists.txt | tr -d "()" | cut -d" " -f2)
version_minor=$(grep "solvespace_VERSION_MINOR" CMakeLists.txt | tr -d "()" | cut -d" " -f2)
version="$version_major.$version_minor~$(git rev-parse --short=8 HEAD)"
snapcraftctl set-version "$version"
git describe --exact-match HEAD && grade="stable" || grade="devel"
snapcraftctl set-grade "$grade"
git submodule update --init extlib/libdxfrw extlib/mimalloc
configflags:
craftctl default
git submodule update --init extlib/libdxfrw extlib/mimalloc extlib/eigen
override-build: |
craftctl default
project_version=$(grep CMAKE_PROJECT_VERSION:STATIC CMakeCache.txt | cut -d "=" -f2)
cd $CRAFT_PART_SRC
version="$project_version~$(git rev-parse --short=8 HEAD)"
craftctl set version="$version"
cmake-parameters:
- -DCMAKE_INSTALL_PREFIX=/usr
- -DCMAKE_BUILD_TYPE=Release
- -DENABLE_TESTS=OFF
@ -56,6 +58,7 @@ parts:
build-packages:
- zlib1g-dev
- libpng-dev
- libcairo2-dev
- libfreetype6-dev
- libjson-c-dev
- libgl-dev
@ -63,6 +66,7 @@ parts:
- libspnav-dev
- git
- g++
- libc6-dev
stage-packages:
- libspnav0
- libsigc++-2.0-0v5
@ -70,11 +74,14 @@ parts:
cleanup:
after: [solvespace]
plugin: nil
build-snaps: [core18, gnome-3-34-1804]
build-snaps: [gnome-42-2204]
override-prime: |
# Remove all files from snap that are already included in the base snap or in
# any connected content snaps
set -eux
for snap in "core18" "gnome-3-34-1804"; do # List all content-snaps and base snaps you're using here
cd "/snap/$snap/current" && find . -type f,l -exec rm -f "$SNAPCRAFT_PRIME/{}" \;
for snap in "gnome-42-2204"; do # List all content-snaps you're using here
cd "/snap/$snap/current" && find . -type f,l -exec rm -f "$CRAFT_PRIME/{}" "$CRAFT_PRIME/usr/{}" \;
done
for cruft in bug lintian man; do
rm -rf $CRAFT_PRIME/usr/share/$cruft
done
find $CRAFT_PRIME/usr/share/doc/ -type f -not -name 'copyright' -delete
find $CRAFT_PRIME/usr/share -type d -empty -delete

View File

@ -1,6 +1,7 @@
# First, set up registration functions for the kinds of resources we handle.
set(resource_root ${CMAKE_CURRENT_SOURCE_DIR}/)
set(resource_list)
set(resource_names)
if(WIN32)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/win32/versioninfo.rc.in
${CMAKE_CURRENT_BINARY_DIR}/win32/versioninfo.rc)
@ -83,6 +84,23 @@ elseif(APPLE)
DEPENDS ${source}
VERBATIM)
endfunction()
elseif(EMSCRIPTEN)
set(resource_dir ${CMAKE_BINARY_DIR}/src/res)
function(add_resource name)
set(source ${CMAKE_CURRENT_SOURCE_DIR}/${name})
set(target ${resource_dir}/${name})
set(resource_list "${resource_list};${target}" PARENT_SCOPE)
set(resource_names "${resource_names};res/${name}" PARENT_SCOPE)
add_custom_command(
OUTPUT ${target}
COMMAND ${CMAKE_COMMAND} -E make_directory ${resource_dir}
COMMAND ${CMAKE_COMMAND} -E copy ${source} ${target}
COMMENT "Copying resource ${name}"
DEPENDS ${source}
VERBATIM)
endfunction()
else() # Unix
include(GNUInstallDirs)
@ -111,7 +129,8 @@ endif()
function(add_resources)
foreach(name ${ARGN})
add_resource(${name})
set(resource_list "${resource_list}" PARENT_SCOPE)
set(resource_list "${resource_list}" PARENT_SCOPE)
set(resource_names "${resource_names}" PARENT_SCOPE)
endforeach()
endfunction()
@ -249,6 +268,8 @@ add_resources(
icons/graphics-window/trim.png
icons/graphics-window/vert.png
icons/text-window/constraint.png
icons/text-window/constraint-dimo.png
icons/text-window/constraint-wo.png
icons/text-window/construction.png
icons/text-window/edges.png
icons/text-window/faces.png
@ -262,13 +283,16 @@ add_resources(
icons/text-window/shaded.png
icons/text-window/workplane.png
locales.txt
locales/cs_CZ.po
locales/de_DE.po
locales/en_US.po
locales/fr_FR.po
locales/uk_UA.po
locales/es_AR.po
locales/tr_TR.po
locales/ru_RU.po
locales/zh_CN.po
locales/ja_JP.po
fonts/unifont.hex.gz
fonts/private/0-check-false.png
fonts/private/1-check-true.png
@ -303,4 +327,6 @@ add_custom_target(resources
DEPENDS ${resource_list})
if(WIN32)
set_property(TARGET resources PROPERTY EXTRA_SOURCES ${rc_file})
elseif(EMSCRIPTEN)
set_property(TARGET resources PROPERTY NAMES ${resource_names})
endif()

View File

@ -19,7 +19,7 @@
SolveSpace is a free (GPLv3) parametric 3d CAD tool. Applications include:
</p>
<ul>
<li>Modeling 3d parts — draw with extrudes, revolves, and Boolean operations</li>
<li>Modeling 3d parts — draw with extrudes, revolves/helix, and Boolean operations</li>
<li>Modeling 2d parts — draw the part as a single section, and export; use 3d assembly to verify fit</li>
<li>Modeling 3d-printed parts — export the STL or other triangle mesh expected by most slicers</li>
<li>Preparing 2D CAM data — export 2d vector art for a waterjet machine or laser cutter</li>
@ -31,6 +31,34 @@
<url type="bugtracker">https://github.com/solvespace/solvespace/issues</url>
<launchable type="desktop-id">@DESKTOP_FILE_NAME@</launchable>
<screenshots>
<screenshot type="default">
<caption>Main window with an empty document</caption>
<image>https://solvespace.com/pics/window-linux-main.png</image>
</screenshot>
<screenshot>
<caption>Property Browser with an empty document</caption>
<image>https://solvespace.com/pics/window-linux-property-browser.png</image>
</screenshot>
<screenshot>
<caption>Viewing and editing constraints on a model</caption>
<image>https://solvespace.com/pics/front-page-pic.png</image>
</screenshot>
<screenshot>
<caption>3D view of a stand made from notched angle iron, from the "ex-stand" project</caption>
<image>https://solvespace.com/pics/ex-stand-detail.jpg</image>
</screenshot>
<screenshot>
<caption>Dimensioning a 2D sketch for a case for a printed circuit board, from the "ex-case" project</caption>
<image>https://solvespace.com/pics/ex-case-outline.png</image>
</screenshot>
<screenshot>
<caption>Showing tracing of Chebyshev's linkage, from the "ex-chebyshev" project</caption>
<image>https://solvespace.com/pics/ex-chebyshev.png</image>
</screenshot>
</screenshots>
<provides>
<mediatype>application/x-solvespace</mediatype>
</provides>
@ -38,6 +66,19 @@
<content_rating type="oars-1.0" />
<releases>
<release version="3.1" date="2022-06-01" type="stable">
<description>
<p>Major new stable release. Includes new arc and line length ratio and difference
constraints, comments associated with point entities. Adds "exploded view" to sketches,
and support for displaying measurements in "feet-inches". Adds a pitch parameter to
helix extrusions. Allows use of Point and Normal to define a new workplane to sketch in.
Adds live updating of Property Browser while dragging the sketch, and active links for
all points, normals, and vectors in the Property Browser. Adds the ability to link STL
files into a model. Includes a variety of UI improvements. Speeds up complex sketches
by up to 8x and doubles the maximum unknowns.</p>
</description>
<url>https://github.com/solvespace/solvespace/releases/tag/v3.0</url>
</release>
<release version="3.0" date="2021-04-18" type="stable">
<description>
<p>Major new stable release. Includes new intersection boolean operation,

View File

@ -2,7 +2,7 @@
Version=1.0
Name=SolveSpace
Comment=A parametric 2d/3d CAD
Exec=${CMAKE_INSTALL_FULL_BINDIR}/solvespace
Exec=${CMAKE_INSTALL_FULL_BINDIR}/solvespace %f
MimeType=application/x-solvespace
Icon=com.solvespace.SolveSpace
Type=Application

View File

@ -2,7 +2,7 @@
Version=1.0
Name=SolveSpace
Comment=A parametric 2d/3d CAD
Exec=solvespace
Exec=solvespace %f
MimeType=application/x-solvespace
Icon=${SNAP}/meta/icons/hicolor/scalable/apps/snap.solvespace.svg
Type=Application

View File

@ -2,7 +2,7 @@
Version=1.0
Name=SolveSpace
Comment=A parametric 2d/3d CAD
Exec=${CMAKE_INSTALL_FULL_BINDIR}/solvespace
Exec=${CMAKE_INSTALL_FULL_BINDIR}/solvespace %f
MimeType=application/x-solvespace
Icon=solvespace
Type=Application

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

View File

@ -1,9 +1,12 @@
# This file lists the ISO locale codes (ISO 639-1/ISO 3166-1), Windows LCIDs,
# and human-readable names for every culture supported by SolveSpace.
cs-CZ,1029,Česky
de-DE,0407,Deutsch
en-US,0409,English (US)
fr-FR,040C,Français
es-AR,2C0A,español (AR)
ru-RU,0419,Русский
tr-TR,041F,Türkçe
uk-UA,0422,Українська
zh-CN,0804,简体中文
ja-JP,0411,日本語

2236
res/locales/cs_CZ.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2274
res/locales/es_AR.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2328
res/locales/ja_JP.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
1 VERSIONINFO
FILEVERSION ${solvespace_VERSION_MAJOR},${solvespace_VERSION_MINOR},0,0
PRODUCTVERSION ${solvespace_VERSION_MAJOR},${solvespace_VERSION_MINOR},0,0
FILEVERSION ${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},0,0
PRODUCTVERSION ${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},0,0
FILEFLAGSMASK 0
FILEFLAGS 0
FILEOS VOS_NT_WINDOWS32
@ -13,12 +13,12 @@ BEGIN
BEGIN
VALUE "CompanyName", "The SolveSpace authors"
VALUE "ProductName", "SolveSpace"
VALUE "ProductVersion", "${solvespace_VERSION_MAJOR}.${solvespace_VERSION_MINOR}~${solvespace_GIT_HASH}"
VALUE "ProductVersion", "${PROJECT_VERSION}~${solvespace_GIT_HASH}"
VALUE "FileDescription", "SolveSpace, a parametric 2d/3d CAD"
VALUE "FileVersion", "${solvespace_VERSION_MAJOR}.${solvespace_VERSION_MINOR}~${solvespace_GIT_HASH}"
VALUE "FileVersion", "${PROJECT_VERSION}~${solvespace_GIT_HASH}"
VALUE "OriginalFilename", "solvespace.exe"
VALUE "InternalName", "solvespace"
VALUE "LegalCopyright", "(c) 2008-2021 Jonathan Westhues and other authors"
VALUE "LegalCopyright", "(c) 2008-2024 Jonathan Westhues and other authors"
END
END

View File

@ -19,50 +19,78 @@ endif()
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.in
${CMAKE_CURRENT_BINARY_DIR}/config.h)
# solvespace dependencies
add_library(slvs_deps INTERFACE)
target_include_directories(slvs_deps INTERFACE SYSTEM
${OPENGL_INCLUDE_DIR}
${ZLIB_INCLUDE_DIR}
${PNG_PNG_INCLUDE_DIR}
${FREETYPE_INCLUDE_DIRS}
${CAIRO_INCLUDE_DIRS}
${MIMALLOC_INCLUDE_DIR}
${EIGEN3_INCLUDE_DIRS})
target_link_libraries(slvs_deps INTERFACE
dxfrw
${ZLIB_LIBRARY}
${PNG_LIBRARY}
${FREETYPE_LIBRARY}
${CAIRO_LIBRARIES}
mimalloc-static)
if(Backtrace_FOUND)
target_include_directories(slvs_deps INTERFACE SYSTEM
${Backtrace_INCLUDE_DIRS})
target_link_libraries(slvs_deps INTERFACE
${Backtrace_LIBRARY})
endif()
if(SPACEWARE_FOUND)
target_include_directories(slvs_deps INTERFACE SYSTEM
${SPACEWARE_INCLUDE_DIR})
target_link_libraries(slvs_deps INTERFACE
${SPACEWARE_LIBRARIES})
endif()
if(ENABLE_OPENMP)
target_link_libraries(slvs_deps INTERFACE slvs_openmp)
endif()
target_compile_options(slvs_deps
INTERFACE ${COVERAGE_FLAGS})
# platform utilities
if(APPLE)
set(util_LIBRARIES
target_link_libraries(slvs_deps INTERFACE
${APPKIT_LIBRARY})
endif()
# libslvs
set(libslvs_SOURCES
add_library(slvs SHARED
solvespace.h
platform/platform.h
util.cpp
entity.cpp
expr.cpp
constraint.cpp
constrainteq.cpp
system.cpp
platform/platform.cpp)
set(libslvs_HEADERS
solvespace.h
platform/platform.h)
add_library(slvs SHARED
${libslvs_SOURCES}
${libslvs_HEADERS}
${util_SOURCES}
platform/platform.cpp
lib.cpp)
target_compile_definitions(slvs
PRIVATE -DLIBRARY)
target_include_directories(slvs
PUBLIC ${CMAKE_SOURCE_DIR}/include)
PUBLIC
${CMAKE_SOURCE_DIR}/include
${EIGEN3_INCLUDE_DIRS})
target_link_libraries(slvs
${util_LIBRARIES}
mimalloc-static)
add_dependencies(slvs
mimalloc-static)
target_link_libraries(slvs PRIVATE slvs_deps)
set_target_properties(slvs PROPERTIES
PUBLIC_HEADER ${CMAKE_SOURCE_DIR}/include/slvs.h
VERSION ${solvespace_VERSION_MAJOR}.${solvespace_VERSION_MINOR}
VERSION ${PROJECT_VERSION}
SOVERSION 1)
if(NOT WIN32)
@ -72,77 +100,18 @@ if(NOT WIN32)
PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
endif()
# solvespace dependencies
include_directories(
${OPENGL_INCLUDE_DIR}
${ZLIB_INCLUDE_DIR}
${PNG_PNG_INCLUDE_DIR}
${FREETYPE_INCLUDE_DIRS}
${CAIRO_INCLUDE_DIRS}
${MIMALLOC_INCLUDE_DIR}
${OpenMP_CXX_INCLUDE_DIRS})
if(Backtrace_FOUND)
include_directories(
${Backtrace_INCLUDE_DIRS})
endif()
if(SPACEWARE_FOUND)
include_directories(
${SPACEWARE_INCLUDE_DIR})
endif()
if(OPENGL STREQUAL 3)
set(gl_SOURCES
render/gl3shader.cpp
render/rendergl3.cpp)
elseif(OPENGL STREQUAL 1)
set(gl_SOURCES
render/rendergl1.cpp)
else()
message(FATAL_ERROR "Unsupported OpenGL version ${OPENGL}")
endif()
set(platform_SOURCES
${gl_SOURCES}
platform/entrygui.cpp)
if(WIN32)
list(APPEND platform_SOURCES
platform/guiwin.cpp)
set(platform_LIBRARIES
comctl32
${SPACEWARE_LIBRARIES})
elseif(APPLE)
add_compile_options(
-fobjc-arc)
list(APPEND platform_SOURCES
platform/guimac.mm)
else()
list(APPEND platform_SOURCES
platform/guigtk.cpp)
set(platform_LIBRARIES
${SPACEWARE_LIBRARIES})
foreach(pkg_config_lib GTKMM JSONC FONTCONFIG)
include_directories(${${pkg_config_lib}_INCLUDE_DIRS})
link_directories(${${pkg_config_lib}_LIBRARY_DIRS})
list(APPEND platform_LIBRARIES ${${pkg_config_lib}_LIBRARIES})
endforeach()
endif()
set(every_platform_SOURCES
platform/guiwin.cpp
platform/guigtk.cpp
platform/guimac.mm)
platform/guimac.mm
platform/guihtml.cpp)
# solvespace library
set(solvespace_core_HEADERS
set(solvespace_core_gl_SOURCES
solvespace.cpp)
add_library(solvespace-core STATIC
dsc.h
expr.h
polygon.h
@ -152,9 +121,7 @@ set(solvespace_core_HEADERS
platform/platform.h
render/render.h
render/gl3shader.h
srf/surface.h)
set(solvespace_core_SOURCES
srf/surface.h
bsp.cpp
clipboard.cpp
confscreen.cpp
@ -176,6 +143,7 @@ set(solvespace_core_SOURCES
groupmesh.cpp
importdxf.cpp
importidf.cpp
importmesh.cpp
mesh.cpp
modify.cpp
mouse.cpp
@ -201,44 +169,19 @@ set(solvespace_core_SOURCES
srf/merge.cpp
srf/ratpoly.cpp
srf/raycast.cpp
srf/shell.cpp
srf/surface.cpp
srf/surfinter.cpp
srf/triangulate.cpp)
set(solvespace_core_gl_SOURCES
solvespace.cpp)
add_library(solvespace-core STATIC
${util_SOURCES}
${solvespace_core_HEADERS}
${solvespace_core_SOURCES})
add_dependencies(solvespace-core
mimalloc-static)
target_link_libraries(solvespace-core
${OpenMP_CXX_LIBRARIES}
dxfrw
${util_LIBRARIES}
${ZLIB_LIBRARY}
${PNG_LIBRARY}
${FREETYPE_LIBRARY}
mimalloc-static)
if(Backtrace_FOUND)
target_link_libraries(solvespace-core
${Backtrace_LIBRARY})
endif()
target_compile_options(solvespace-core
PRIVATE ${COVERAGE_FLAGS})
target_link_libraries(solvespace-core PUBLIC slvs_deps)
# solvespace translations
if(HAVE_GETTEXT)
get_target_property(solvespace_core_SOURCES solvespace-core SOURCES)
set(inputs
${solvespace_core_SOURCES}
${solvespace_core_HEADERS}
${every_platform_SOURCES}
${solvespace_core_gl_SOURCES})
@ -265,9 +208,9 @@ if(HAVE_GETTEXT)
--keyword --keyword=_ --keyword=N_ --keyword=C_:2,1c --keyword=CN_:2,1c
--force-po --width=100 --sort-by-file
--package-name=SolveSpace
--package-version=${solvespace_VERSION_MAJOR}.${solvespace_VERSION_MINOR}
--package-version=${PROJECT_VERSION}
"--copyright-holder=the PACKAGE authors"
--msgid-bugs-address=whitequark@whitequark.org
--msgid-bugs-address=phkahler@gmail.com
--from-code=utf-8 --output=${gen_output_pot} ${inputs}
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${gen_output_pot} ${output_pot}
DEPENDS ${inputs}
@ -321,52 +264,137 @@ endif()
if(ENABLE_GUI)
add_executable(solvespace WIN32 MACOSX_BUNDLE
${solvespace_core_gl_SOURCES}
${platform_SOURCES}
platform/entrygui.cpp
$<TARGET_PROPERTY:resources,EXTRA_SOURCES>)
add_dependencies(solvespace
resources)
target_link_libraries(solvespace
PRIVATE
solvespace-core
${OPENGL_LIBRARIES}
${platform_LIBRARIES}
${COVERAGE_LIBRARY})
${OPENGL_LIBRARIES})
if(MSVC)
set_target_properties(solvespace PROPERTIES
LINK_FLAGS "/MANIFEST:NO /SAFESEH:NO /INCREMENTAL:NO /OPT:REF")
# OpenGL version
if(OPENGL STREQUAL 3)
target_sources(solvespace PRIVATE
render/gl3shader.cpp
render/rendergl3.cpp)
elseif(OPENGL STREQUAL 1)
target_sources(solvespace PRIVATE
render/rendergl1.cpp)
else()
message(FATAL_ERROR "Unsupported OpenGL version ${OPENGL}")
endif()
# Platform-specific
if(WIN32)
target_sources(solvespace PRIVATE
platform/guiwin.cpp)
target_link_libraries(solvespace PRIVATE comctl32)
elseif(APPLE)
target_compile_options(solvespace PRIVATE -fobjc-arc)
target_compile_definitions(solvespace PRIVATE GL_SILENCE_DEPRECATION)
target_sources(solvespace PRIVATE
platform/guimac.mm)
set_target_properties(solvespace PROPERTIES
OUTPUT_NAME SolveSpace
XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME "YES"
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.solvespace"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
elseif(EMSCRIPTEN)
set(SHELL ${CMAKE_CURRENT_SOURCE_DIR}/platform/html/emshell.html)
set(LINK_FLAGS
--bind --shell-file ${SHELL}
--no-heap-copy -s ALLOW_MEMORY_GROWTH=1 -s WASM=1 -s ASYNCIFY=1
-s DYNCALLS=1 -s ASSERTIONS=1
-s TOTAL_STACK=33554432 -s TOTAL_MEMORY=134217728)
get_target_property(resource_names resources NAMES)
foreach(resource ${resource_names})
list(APPEND LINK_FLAGS --preload-file ${resource})
endforeach()
if(CMAKE_BUILD_TYPE STREQUAL Debug)
list(APPEND LINK_FLAGS
--emrun --emit-symbol-map
-s DEMANGLE_SUPPORT=1
-s SAFE_HEAP=1)
endif()
target_sources(solvespace PRIVATE
platform/guihtml.cpp)
string(REPLACE ";" " " LINK_FLAGS "${LINK_FLAGS}")
set_target_properties(solvespace PROPERTIES
LINK_FLAGS "${LINK_FLAGS}")
set_source_files_properties(platform/guihtml.cpp PROPERTIES
OBJECT_DEPENDS ${SHELL})
add_custom_command(
TARGET solvespace POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/platform/html/solvespaceui.css
${EXECUTABLE_OUTPUT_PATH}/solvespaceui.css
COMMENT "Copying UI stylesheet"
VERBATIM)
add_custom_command(
TARGET solvespace POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/platform/html/solvespaceui.js
${EXECUTABLE_OUTPUT_PATH}/solvespaceui.js
COMMENT "Copying UI script solvespaceui.js"
VERBATIM)
add_custom_command(
TARGET solvespace POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/platform/html/filemanagerui.js
${EXECUTABLE_OUTPUT_PATH}/filemanagerui.js
COMMENT "Copying UI script filemanagerui.sj"
VERBATIM)
else()
target_sources(solvespace PRIVATE
platform/guigtk.cpp)
target_include_directories(solvespace PRIVATE SYSTEM
${GTKMM_INCLUDE_DIRS}
${JSONC_INCLUDE_DIRS}
${FONTCONFIG_INCLUDE_DIRS})
target_link_directories(solvespace PRIVATE
${GTKMM_LIBRARY_DIRS}
${JSONC_LIBRARY_DIRS}
${FONTCONFIG_LIBRARY_DIRS})
target_link_libraries(solvespace PRIVATE
${GTKMM_LIBRARIES}
${JSONC_LIBRARIES}
${FONTCONFIG_LIBRARIES})
endif()
if(MSVC)
set_target_properties(solvespace PROPERTIES
LINK_FLAGS "/MANIFEST:NO /SAFESEH:NO /INCREMENTAL:NO /OPT:REF")
endif()
endif()
# solvespace headless library
set(headless_SOURCES
add_library(solvespace-headless STATIC EXCLUDE_FROM_ALL
${solvespace_core_gl_SOURCES}
platform/guinone.cpp
render/rendercairo.cpp)
add_library(solvespace-headless STATIC EXCLUDE_FROM_ALL
${solvespace_core_gl_SOURCES}
${headless_SOURCES})
target_compile_definitions(solvespace-headless
PRIVATE -DHEADLESS)
PRIVATE HEADLESS)
target_include_directories(solvespace-headless
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
PUBLIC ${EIGEN3_INCLUDE_DIRS})
target_link_libraries(solvespace-headless
solvespace-core
${CAIRO_LIBRARIES})
target_compile_options(solvespace-headless
PRIVATE ${COVERAGE_FLAGS})
PRIVATE
solvespace-core)
# solvespace command-line executable
@ -390,7 +418,7 @@ endif()
# solvespace unix package
if(NOT (WIN32 OR APPLE))
if(NOT (WIN32 OR APPLE OR EMSCRIPTEN))
if(ENABLE_GUI)
install(TARGETS solvespace
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})

View File

@ -249,8 +249,10 @@ void GraphicsWindow::PasteClipboard(Vector trans, double theta, double scale) {
case Constraint::Type::COMMENT:
c.disp.offset = c.disp.offset.Plus(trans);
break;
case Constraint::Type::PT_PT_DISTANCE:
case Constraint::Type::PT_LINE_DISTANCE:
c.valA *= scale;
break;
case Constraint::Type::PT_PT_DISTANCE:
case Constraint::Type::PROJ_PT_DISTANCE:
case Constraint::Type::DIAMETER:
c.valA *= fabs(scale);
@ -281,7 +283,7 @@ void GraphicsWindow::PasteClipboard(Vector trans, double theta, double scale) {
}
case Constraint::Type::HORIZONTAL:
case Constraint::Type::VERTICAL:
// When rotating 90 or 270 degrees, swap the vertical / horizontal constaints
// When rotating 90 or 270 degrees, swap the vertical / horizontal constraints
if (EXACT(fmod(theta + (PI/2), PI) == 0)) {
if(c.type == Constraint::Type::HORIZONTAL) {
c.type = Constraint::Type::VERTICAL;

View File

@ -1,7 +1,8 @@
#ifndef SOLVESPACE_CONFIG_H
#define SOLVESPACE_CONFIG_H
#define PACKAGE_VERSION "@solvespace_VERSION_MAJOR@.@solvespace_VERSION_MINOR@~@solvespace_GIT_HASH@"
#define PACKAGE_VERSION "@PROJECT_VERSION@~@solvespace_GIT_HASH@"
#define GIT_HASH_URL "https://github.com/solvespace/solvespace/commit/@solvespace_GIT_HASH@"
/* Non-OS X *nix only */
#define UNIX_DATADIR "@CMAKE_INSTALL_PREFIX@/@CMAKE_INSTALL_DATAROOTDIR@/solvespace"

View File

@ -9,24 +9,6 @@
#include <omp.h>
#endif
void TextWindow::ScreenChangeLightDirection(int link, uint32_t v) {
SS.TW.ShowEditControl(8, ssprintf("%.2f, %.2f, %.2f", CO(SS.lightDir[v])));
SS.TW.edit.meaning = Edit::LIGHT_DIRECTION;
SS.TW.edit.i = v;
}
void TextWindow::ScreenChangeLightIntensity(int link, uint32_t v) {
SS.TW.ShowEditControl(31, ssprintf("%.2f", SS.lightIntensity[v]));
SS.TW.edit.meaning = Edit::LIGHT_INTENSITY;
SS.TW.edit.i = v;
}
void TextWindow::ScreenChangeLightAmbient(int link, uint32_t v) {
SS.TW.ShowEditControl(31, ssprintf("%.2f", SS.ambientIntensity));
SS.TW.edit.meaning = Edit::LIGHT_AMBIENT;
SS.TW.edit.i = 0;
}
void TextWindow::ScreenChangeColor(int link, uint32_t v) {
SS.TW.ShowEditControlWithColorPicker(13, SS.modelColor[v]);
@ -58,13 +40,8 @@ void TextWindow::ScreenChangeExportMaxSegments(int link, uint32_t v) {
SS.TW.edit.i = 1;
}
void TextWindow::ScreenChangeCameraTangent(int link, uint32_t v) {
SS.TW.ShowEditControl(3, ssprintf("%.3f", 1000*SS.cameraTangent));
SS.TW.edit.meaning = Edit::CAMERA_TANGENT;
}
void TextWindow::ScreenChangeGridSpacing(int link, uint32_t v) {
SS.TW.ShowEditControl(3, SS.MmToString(SS.gridSpacing));
SS.TW.ShowEditControl(3, SS.MmToString(SS.gridSpacing, true));
SS.TW.edit.meaning = Edit::GRID_SPACING;
}
@ -89,10 +66,19 @@ void TextWindow::ScreenChangeExportScale(int link, uint32_t v) {
}
void TextWindow::ScreenChangeExportOffset(int link, uint32_t v) {
SS.TW.ShowEditControl(3, SS.MmToString(SS.exportOffset));
SS.TW.ShowEditControl(3, SS.MmToString(SS.exportOffset, true));
SS.TW.edit.meaning = Edit::EXPORT_OFFSET;
}
void TextWindow::ScreenChangeArcDimDefault(int link, uint32_t v) {
SS.arcDimDefaultDiameter = !SS.arcDimDefaultDiameter;
}
void TextWindow::ScreenChangeShowFullFilePath(int link, uint32_t v) {
SS.showFullFilePath = !SS.showFullFilePath;
SS.UpdateWindowTitles();
}
void TextWindow::ScreenChangeFixExportColors(int link, uint32_t v) {
SS.fixExportColors = !SS.fixExportColors;
}
@ -115,6 +101,10 @@ void TextWindow::ScreenChangeTurntableNav(int link, uint32_t v) {
}
}
void TextWindow::ScreenChangeCameraNav(int link, uint32_t v) {
SS.cameraNav = !SS.cameraNav;
}
void TextWindow::ScreenChangeImmediatelyEditDimension(int link, uint32_t v) {
SS.immediatelyEditDimension = !SS.immediatelyEditDimension;
SS.GW.Invalidate(/*clearPersistent=*/true);
@ -171,7 +161,7 @@ void TextWindow::ScreenChangeCanvasSize(int link, uint32_t v) {
}
int col = 13;
if(v < 10) col = 11;
SS.TW.ShowEditControl(col, SS.MmToString(d));
SS.TW.ShowEditControl(col, SS.MmToString(d, true));
SS.TW.edit.meaning = Edit::CANVAS_SIZE;
SS.TW.edit.i = v;
}
@ -181,7 +171,12 @@ void TextWindow::ScreenChangeGCodeParameter(int link, uint32_t v) {
switch(link) {
case 'd':
SS.TW.edit.meaning = Edit::G_CODE_DEPTH;
buf += SS.MmToString(SS.gCode.depth);
buf += SS.MmToString(SS.gCode.depth, true);
break;
case 'h':
SS.TW.edit.meaning = Edit::G_CODE_SAFE_HEIGHT;
buf += SS.MmToString(SS.gCode.safeHeight, true);
break;
case 's':
@ -191,12 +186,12 @@ void TextWindow::ScreenChangeGCodeParameter(int link, uint32_t v) {
case 'F':
SS.TW.edit.meaning = Edit::G_CODE_FEED;
buf += SS.MmToString(SS.gCode.feed);
buf += SS.MmToString(SS.gCode.feed, true);
break;
case 'P':
SS.TW.edit.meaning = Edit::G_CODE_PLUNGE_FEED;
buf += SS.MmToString(SS.gCode.plungeFeed);
buf += SS.MmToString(SS.gCode.plungeFeed, true);
break;
}
SS.TW.ShowEditControl(14, buf);
@ -227,18 +222,6 @@ void TextWindow::ShowConfiguration() {
&ScreenChangeColor, i);
}
Printf(false, "");
Printf(false, "%Ft light direction intensity");
for(i = 0; i < 2; i++) {
Printf(false, "%Bp #%d (%2,%2,%2)%Fl%D%f%Ll[c]%E "
"%2 %Fl%D%f%Ll[c]%E",
(i & 1) ? 'd' : 'a', i,
CO(SS.lightDir[i]), i, &ScreenChangeLightDirection,
SS.lightIntensity[i], i, &ScreenChangeLightIntensity);
}
Printf(false, "%Ba ambient lighting %2 %Fl%f%Ll[c]%E",
SS.ambientIntensity, &ScreenChangeLightAmbient);
Printf(false, "");
Printf(false, "%Ft chord tolerance (in percents)%E");
Printf(false, "%Ba %@ %% %Fl%Ll%f%D[change]%E; %@ mm, %d triangles",
@ -260,11 +243,6 @@ void TextWindow::ShowConfiguration() {
SS.exportMaxSegments,
&ScreenChangeExportMaxSegments);
Printf(false, "");
Printf(false, "%Ft perspective factor (0 for parallel)%E");
Printf(false, "%Ba %# %Fl%Ll%f%D[change]%E",
SS.cameraTangent*1000,
&ScreenChangeCameraTangent, 0);
Printf(false, "%Ft snap grid spacing%E");
Printf(false, "%Ba %s %Fl%Ll%f%D[change]%E",
SS.MmToString(SS.gridSpacing).c_str(),
@ -368,11 +346,18 @@ void TextWindow::ShowConfiguration() {
Printf(false, " %Fd%f%Ll%s enable automatic line constraints%E",
&ScreenChangeAutomaticLineConstraints,
SS.automaticLineConstraints ? CHECK_TRUE : CHECK_FALSE);
Printf(false, " %Fd%f%Ll%s use camera mouse navigation%E", &ScreenChangeCameraNav,
SS.cameraNav ? CHECK_TRUE : CHECK_FALSE);
Printf(false, " %Fd%f%Ll%s use turntable mouse navigation%E", &ScreenChangeTurntableNav,
SS.turntableNav ? CHECK_TRUE : CHECK_FALSE);
Printf(false, " %Fd%f%Ll%s edit newly added dimensions%E",
&ScreenChangeImmediatelyEditDimension,
SS.immediatelyEditDimension ? CHECK_TRUE : CHECK_FALSE);
Printf(false, " %Fd%f%Ll%s arc default is diameter%E",
&ScreenChangeArcDimDefault,
SS.arcDimDefaultDiameter ? CHECK_TRUE : CHECK_FALSE);
Printf(false, " %Fd%f%Ll%s display the full path in the title bar%E",
&ScreenChangeShowFullFilePath, SS.showFullFilePath ? CHECK_TRUE : CHECK_FALSE);
Printf(false, "");
Printf(false, "%Ft autosave interval (in minutes)%E");
Printf(false, "%Ba %d %Fl%Ll%f[change]%E",
@ -459,6 +444,11 @@ bool TextWindow::EditControlDoneForConfiguration(const std::string &s) {
SS.GW.Invalidate();
break;
}
case Edit::EXPLODE_DISTANCE: {
SS.explodeDistance = min(1e4, max(-1e4, SS.StringToMm(s)));
SS.MarkGroupDirty(SS.GW.activeGroup, true);
break;
}
case Edit::DIGITS_AFTER_DECIMAL: {
int v = atoi(s.c_str());
if(v < 0 || v > 8) {
@ -527,6 +517,11 @@ bool TextWindow::EditControlDoneForConfiguration(const std::string &s) {
if(e) SS.gCode.depth = (float)SS.ExprToMm(e);
break;
}
case Edit::G_CODE_SAFE_HEIGHT: {
Expr *e = Expr::From(s, /*popUpError=*/true);
if(e) SS.gCode.safeHeight = (float)SS.ExprToMm(e);
break;
}
case Edit::G_CODE_PASSES: {
Expr *e = Expr::From(s, /*popUpError=*/true);
if(e) SS.gCode.passes = (int)(e->Eval());

View File

@ -22,7 +22,11 @@ std::string Constraint::DescriptionString() const {
case Type::EQ_LEN_PT_LINE_D: s = C_("constr-name", "eq-length-and-pt-ln-dist"); break;
case Type::EQ_PT_LN_DISTANCES: s = C_("constr-name", "eq-pt-line-distances"); break;
case Type::LENGTH_RATIO: s = C_("constr-name", "length-ratio"); break;
case Type::ARC_ARC_LEN_RATIO: s = C_("constr-name", "arc-arc-length-ratio"); break;
case Type::ARC_LINE_LEN_RATIO: s = C_("constr-name", "arc-line-length-ratio"); break;
case Type::LENGTH_DIFFERENCE: s = C_("constr-name", "length-difference"); break;
case Type::ARC_ARC_DIFFERENCE: s = C_("constr-name", "arc-arc-len-difference"); break;
case Type::ARC_LINE_DIFFERENCE: s = C_("constr-name", "arc-line-len-difference"); break;
case Type::SYMMETRIC: s = C_("constr-name", "symmetric"); break;
case Type::SYMMETRIC_HORIZ: s = C_("constr-name", "symmetric-h"); break;
case Type::SYMMETRIC_VERT: s = C_("constr-name", "symmetric-v"); break;
@ -191,6 +195,8 @@ bool Constraint::ConstrainCurveCurveTangent(Constraint *c, Entity *eA, Entity *e
}
void Constraint::MenuConstrain(Command id) {
std::vector<Constraint> newcons;
Constraint c = {};
c.group = SS.GW.activeGroup;
c.workplane = SS.GW.ActiveWorkplane();
@ -230,6 +236,12 @@ void Constraint::MenuConstrain(Command id) {
} else if(gs.circlesOrArcs == 1 && gs.n == 1) {
c.type = Type::DIAMETER;
c.entityA = gs.entity[0];
Entity* arc = SK.GetEntity(gs.entity[0]);
if ((arc->type == EntityBase::Type::ARC_OF_CIRCLE)
&& (!SS.arcDimDefaultDiameter))
{
c.other = true;
}
} else {
Error(_("Bad selection for distance / diameter constraint. This "
"constraint can apply to:\n\n"
@ -259,54 +271,69 @@ void Constraint::MenuConstrain(Command id) {
c.valA = 0;
c.ModifyToSatisfy();
AddConstraint(&c);
newcons.push_back(c);
break;
}
case Command::ON_ENTITY:
if(gs.points == 2 && gs.n == 2) {
if(gs.points >= 2 && gs.points == gs.n) {
c.type = Type::POINTS_COINCIDENT;
c.ptA = gs.point[0];
c.ptB = gs.point[1];
for(int k = 1; k < gs.points; k++) {
c.ptB = gs.point[k];
newcons.push_back(c);
}
} else if(gs.points == 1 && gs.workplanes == 1 && gs.n == 2) {
c.type = Type::PT_IN_PLANE;
c.ptA = gs.point[0];
c.entityA = gs.entity[0];
newcons.push_back(c);
} else if(gs.points == 1 && gs.lineSegments == 1 && gs.n == 2) {
c.type = Type::PT_ON_LINE;
c.ptA = gs.point[0];
c.entityA = gs.entity[0];
newcons.push_back(c);
} else if(gs.points == 1 && gs.circlesOrArcs == 1 && gs.n == 2) {
c.type = Type::PT_ON_CIRCLE;
c.ptA = gs.point[0];
c.entityA = gs.entity[0];
} else if(gs.points == 1 && gs.faces == 1 && gs.n == 2) {
newcons.push_back(c);
} else if(gs.points == 1 && gs.faces >= 1 && gs.n == gs.points+gs.faces) {
c.type = Type::PT_ON_FACE;
c.ptA = gs.point[0];
c.entityA = gs.face[0];
for (int k=0; k<gs.faces; k++) {
c.entityA = gs.face[k];
newcons.push_back(c);
}
} else {
Error(_("Bad selection for on point / curve / plane constraint. "
"This constraint can apply to:\n\n"
" * two points (points coincident)\n"
" * two or more points (points coincident)\n"
" * a point and a workplane (point in plane)\n"
" * a point and a line segment (point on line)\n"
" * a point and a circle or arc (point on curve)\n"
" * a point and a plane face (point on face)\n"));
" * a point and one to three plane faces (point on face(s))\n"));
return;
}
AddConstraint(&c);
for (auto&& nc : newcons)
AddConstraint(&nc);
break;
case Command::EQUAL:
if(gs.lineSegments == 2 && gs.n == 2) {
if(gs.lineSegments >= 2 && gs.lineSegments == gs.n) {
c.type = Type::EQUAL_LENGTH_LINES;
c.entityA = gs.entity[0];
c.entityB = gs.entity[1];
for (std::vector<hEntity>::size_type k = 1;k < gs.entity.size(); ++k){
c.entityB = gs.entity[k];
newcons.push_back(c);
}
} else if(gs.lineSegments == 2 && gs.points == 2 && gs.n == 4) {
c.type = Type::EQ_PT_LN_DISTANCES;
c.entityA = gs.entity[0];
c.ptA = gs.point[0];
c.entityB = gs.entity[1];
c.ptB = gs.point[1];
newcons.push_back(c);
} else if(gs.lineSegments == 1 && gs.points == 2 && gs.n == 3) {
// The same line segment for the distances, but different
// points.
@ -315,27 +342,20 @@ void Constraint::MenuConstrain(Command id) {
c.ptA = gs.point[0];
c.entityB = gs.entity[0];
c.ptB = gs.point[1];
newcons.push_back(c);
} else if(gs.lineSegments == 2 && gs.points == 1 && gs.n == 3) {
c.type = Type::EQ_LEN_PT_LINE_D;
c.entityA = gs.entity[0];
c.entityB = gs.entity[1];
c.ptA = gs.point[0];
} else if(gs.vectors == 4 && gs.n == 4) {
c.type = Type::EQUAL_ANGLE;
c.entityA = gs.vector[0];
c.entityB = gs.vector[1];
c.entityC = gs.vector[2];
c.entityD = gs.vector[3];
} else if(gs.vectors == 3 && gs.n == 3) {
c.type = Type::EQUAL_ANGLE;
c.entityA = gs.vector[0];
c.entityB = gs.vector[1];
c.entityC = gs.vector[1];
c.entityD = gs.vector[2];
} else if(gs.circlesOrArcs == 2 && gs.n == 2) {
newcons.push_back(c);
} else if(gs.circlesOrArcs >= 2 && gs.circlesOrArcs == gs.n) {
c.type = Type::EQUAL_RADIUS;
c.entityA = gs.entity[0];
c.entityB = gs.entity[1];
for (std::vector<hEntity>::size_type k = 1;k < gs.entity.size(); ++k){
c.entityB = gs.entity[k];
newcons.push_back(c);
}
} else if(gs.arcs == 1 && gs.lineSegments == 1 && gs.n == 2) {
c.type = Type::EQUAL_LINE_ARC_LEN;
if(SK.GetEntity(gs.entity[0])->type == Entity::Type::ARC_OF_CIRCLE) {
@ -345,38 +365,38 @@ void Constraint::MenuConstrain(Command id) {
c.entityA = gs.entity[0];
c.entityB = gs.entity[1];
}
newcons.push_back(c);
} else {
Error(_("Bad selection for equal length / radius constraint. "
"This constraint can apply to:\n\n"
" * two line segments (equal length)\n"
" * two or more line segments (equal length)\n"
" * two line segments and two points "
"(equal point-line distances)\n"
" * a line segment and two points "
"(equal point-line distances)\n"
" * a line segment, and a point and line segment "
"(point-line distance equals length)\n"
" * four line segments or normals "
"(equal angle between A,B and C,D)\n"
" * three line segments or normals "
"(equal angle between A,B and B,C)\n"
" * two circles or arcs (equal radius)\n"
" * two or more circles or arcs (equal radius)\n"
" * a line segment and an arc "
"(line segment length equals arc length)\n"));
return;
}
if(c.type == Type::EQUAL_ANGLE) {
// Infer the nearest supplementary angle from the sketch.
Vector a1 = SK.GetEntity(c.entityA)->VectorGetNum(),
b1 = SK.GetEntity(c.entityB)->VectorGetNum(),
a2 = SK.GetEntity(c.entityC)->VectorGetNum(),
b2 = SK.GetEntity(c.entityD)->VectorGetNum();
double d1 = a1.Dot(b1), d2 = a2.Dot(b2);
SS.UndoRemember();
for (auto&& nc : newcons){
if(nc.type == Type::EQUAL_ANGLE) {
// Infer the nearest supplementary angle from the sketch.
Vector a1 = SK.GetEntity(c.entityA)->VectorGetNum(),
b1 = SK.GetEntity(c.entityB)->VectorGetNum(),
a2 = SK.GetEntity(c.entityC)->VectorGetNum(),
b2 = SK.GetEntity(c.entityD)->VectorGetNum();
double d1 = a1.Dot(b1), d2 = a2.Dot(b2);
if(d1*d2 < 0) {
c.other = true;
if(d1*d2 < 0) {
nc.other = true;
}
}
AddConstraint(&nc, /*rememberForUndo=*/false);
}
AddConstraint(&c);
break;
case Command::RATIO:
@ -384,16 +404,34 @@ void Constraint::MenuConstrain(Command id) {
c.type = Type::LENGTH_RATIO;
c.entityA = gs.entity[0];
c.entityB = gs.entity[1];
}
else if(gs.arcs == 2 && gs.n == 2) {
c.type = Type::ARC_ARC_LEN_RATIO;
c.entityA = gs.entity[0];
c.entityB = gs.entity[1];
}
else if(gs.lineSegments == 1 && gs.arcs == 1 && gs.n == 2) {
c.type = Type::ARC_LINE_LEN_RATIO;
if(SK.GetEntity(gs.entity[0])->type == Entity::Type::ARC_OF_CIRCLE) {
c.entityA = gs.entity[1];
c.entityB = gs.entity[0];
} else {
c.entityA = gs.entity[0];
c.entityB = gs.entity[1];
}
} else {
Error(_("Bad selection for length ratio constraint. This "
"constraint can apply to:\n\n"
" * two line segments\n"));
" * two line segments\n"
" * two arcs\n"
" * one arc and one line segment\n"));
return;
}
c.valA = 0;
c.ModifyToSatisfy();
AddConstraint(&c);
newcons.push_back(c);
break;
case Command::DIFFERENCE:
@ -401,16 +439,34 @@ void Constraint::MenuConstrain(Command id) {
c.type = Type::LENGTH_DIFFERENCE;
c.entityA = gs.entity[0];
c.entityB = gs.entity[1];
}
else if(gs.arcs == 2 && gs.n == 2) {
c.type = Type::ARC_ARC_DIFFERENCE;
c.entityA = gs.entity[0];
c.entityB = gs.entity[1];
}
else if(gs.lineSegments == 1 && gs.arcs == 1 && gs.n == 2) {
c.type = Type::ARC_LINE_DIFFERENCE;
if(SK.GetEntity(gs.entity[0])->type == Entity::Type::ARC_OF_CIRCLE) {
c.entityA = gs.entity[1];
c.entityB = gs.entity[0];
} else {
c.entityA = gs.entity[0];
c.entityB = gs.entity[1];
}
} else {
Error(_("Bad selection for length difference constraint. This "
"constraint can apply to:\n\n"
" * two line segments\n"));
" * two line segments\n"
" * two arcs\n"
" * one arc and one line segment\n"));
return;
}
c.valA = 0;
c.ModifyToSatisfy();
AddConstraint(&c);
newcons.push_back(c);
break;
case Command::AT_MIDPOINT:
@ -424,12 +480,15 @@ void Constraint::MenuConstrain(Command id) {
SS.UndoRemember();
DeleteAllConstraintsFor(Type::PT_ON_LINE, c.entityA, c.ptA);
AddConstraint(&c, /*rememberForUndo=*/false);
newcons.push_back(c);
break;
} else if(gs.lineSegments == 1 && gs.workplanes == 1 && gs.n == 2) {
c.type = Type::AT_MIDPOINT;
int i = SK.GetEntity(gs.entity[0])->IsWorkplane() ? 1 : 0;
c.entityA = gs.entity[i];
c.entityB = gs.entity[1-i];
AddConstraint(&c);
newcons.push_back(c);
} else {
Error(_("Bad selection for at midpoint constraint. This "
"constraint can apply to:\n\n"
@ -439,7 +498,7 @@ void Constraint::MenuConstrain(Command id) {
"(line's midpoint on plane)\n"));
return;
}
AddConstraint(&c);
break;
case Command::SYMMETRIC:
@ -528,41 +587,47 @@ void Constraint::MenuConstrain(Command id) {
DeleteAllConstraintsFor(Type::VERTICAL, (gs.entity[0]),
Entity::NO_ENTITY);
AddConstraint(&c, /*rememberForUndo=*/false);
newcons.push_back(c);
break;
}
}
AddConstraint(&c);
newcons.push_back(c);
break;
case Command::VERTICAL:
case Command::HORIZONTAL: {
hEntity ha, hb;
if(c.workplane == Entity::FREE_IN_3D) {
Error(_("Activate a workplane (with Sketch -> In Workplane) before "
"applying a horizontal or vertical constraint."));
return;
}
if(gs.lineSegments == 1 && gs.n == 1) {
c.entityA = gs.entity[0];
Entity *e = SK.GetEntity(c.entityA);
ha = e->point[0];
hb = e->point[1];
} else if(gs.points == 2 && gs.n == 2) {
ha = c.ptA = gs.point[0];
hb = c.ptB = gs.point[1];
} else {
Error(_("Bad selection for horizontal / vertical constraint. "
"This constraint can apply to:\n\n"
" * two points\n"
" * a line segment\n"));
return;
}
if(id == Command::HORIZONTAL) {
c.type = Type::HORIZONTAL;
} else {
c.type = Type::VERTICAL;
}
AddConstraint(&c);
if(c.workplane == Entity::FREE_IN_3D) {
Error(_("Activate a workplane (with Sketch -> In Workplane) before "
"applying a horizontal or vertical constraint."));
return;
}
if(gs.lineSegments > 0 && gs.lineSegments == gs.n) {
for (auto enti : gs.entity){
c.entityA = enti;
newcons.push_back(c);
}
} else if(gs.points >= 2 && gs.n == gs.points) {
c.ptA = gs.point[0];
for (int k = 1; k<gs.points; k++) {
c.ptB = gs.point[k];
newcons.push_back(c);
}
} else {
Error(_("Bad selection for horizontal / vertical constraint. "
"This constraint can apply to:\n\n"
" * two or more points\n"
" * one or more line segments\n"));
return;
}
SS.UndoRemember();
for (auto && nc: newcons)
AddConstraint(&nc, /*rememberForUndo=*/false);
break;
}
@ -603,6 +668,7 @@ void Constraint::MenuConstrain(Command id) {
nfree->NormalForceTo(Quaternion::From(fu, fv));
}
AddConstraint(&c, /*rememberForUndo=*/false);
newcons.push_back(c);
break;
}
@ -640,7 +706,19 @@ void Constraint::MenuConstrain(Command id) {
case Command::ANGLE:
case Command::REF_ANGLE: {
if(gs.vectors == 2 && gs.n == 2) {
if(gs.vectors == 3 && gs.n == 3) {
c.type = Type::EQUAL_ANGLE;
c.entityA = gs.vector[0];
c.entityB = gs.vector[1];
c.entityC = gs.vector[1];
c.entityD = gs.vector[2];
} else if(gs.vectors == 4 && gs.n == 4) {
c.type = Type::EQUAL_ANGLE;
c.entityA = gs.vector[0];
c.entityB = gs.vector[1];
c.entityC = gs.vector[2];
c.entityD = gs.vector[3];
} else if(gs.vectors == 2 && gs.n == 2) {
c.type = Type::ANGLE;
c.entityA = gs.vector[0];
c.entityB = gs.vector[1];
@ -648,9 +726,15 @@ void Constraint::MenuConstrain(Command id) {
} else {
Error(_("Bad selection for angle constraint. This constraint "
"can apply to:\n\n"
"Angle between:\n"
" * two line segments\n"
" * a line segment and a normal\n"
" * two normals\n"));
" * two normals\n"
"\nEqual angles:\n"
" * four line segments or normals "
"(equal angle between A,B and C,D)\n"
" * three line segments or normals "
"(equal angle between A,B and B,C)\n"));
return;
}
@ -679,14 +763,23 @@ void Constraint::MenuConstrain(Command id) {
c.ModifyToSatisfy();
AddConstraint(&c);
newcons.push_back(c);
break;
}
case Command::PARALLEL:
if(gs.vectors == 2 && gs.n == 2) {
if(gs.faces == 2 && gs.n == 2) {
c.type = Type::PARALLEL;
c.entityA = gs.face[0];
c.entityB = gs.face[1];
newcons.push_back(c);
} else if(gs.vectors > 1 && gs.vectors == gs.n) {
c.type = Type::PARALLEL;
c.entityA = gs.vector[0];
c.entityB = gs.vector[1];
for (std::vector<hEntity>::size_type k = 1; k < gs.vector.size();++k ){
c.entityB = gs.vector[k];
newcons.push_back(c);
}
} else if(gs.lineSegments == 1 && gs.arcs == 1 && gs.n == 2) {
Entity *line = SK.GetEntity(gs.entity[0]),
*arc = SK.GetEntity(gs.entity[1]);
@ -699,6 +792,7 @@ void Constraint::MenuConstrain(Command id) {
c.type = Type::ARC_LINE_TANGENT;
c.entityA = arc->h;
c.entityB = line->h;
newcons.push_back(c);
} else if(gs.lineSegments == 1 && gs.cubics == 1 && gs.n == 2) {
Entity *line = SK.GetEntity(gs.entity[0]),
*cubic = SK.GetEntity(gs.entity[1]);
@ -711,6 +805,7 @@ void Constraint::MenuConstrain(Command id) {
c.type = Type::CUBIC_LINE_TANGENT;
c.entityA = cubic->h;
c.entityB = line->h;
newcons.push_back(c);
} else if(gs.cubics + gs.arcs == 2 && gs.n == 2) {
if(!SS.GW.LockedInWorkplane()) {
Error(_("Curve-curve tangency must apply in workplane."));
@ -724,33 +819,43 @@ void Constraint::MenuConstrain(Command id) {
c.type = Type::CURVE_CURVE_TANGENT;
c.entityA = eA->h;
c.entityB = eB->h;
newcons.push_back(c);
} else {
Error(_("Bad selection for parallel / tangent constraint. This "
"constraint can apply to:\n\n"
" * two line segments (parallel)\n"
" * a line segment and a normal (parallel)\n"
" * two normals (parallel)\n"
" * two faces\n"
" * two or more line segments (parallel)\n"
" * one or more line segments and one or more normals (parallel)\n"
" * two or more normals (parallel)\n"
" * two line segments, arcs, or beziers, that share "
"an endpoint (tangent)\n"));
return;
}
AddConstraint(&c);
SS.UndoRemember();
for (auto&& nc:newcons)
AddConstraint(&nc, /*rememberForUndo=*/false);
break;
case Command::PERPENDICULAR:
if(gs.vectors == 2 && gs.n == 2) {
if(gs.faces == 2 && gs.n == 2) {
c.type = Type::PERPENDICULAR;
c.entityA = gs.face[0];
c.entityB = gs.face[1];
} else if(gs.vectors == 2 && gs.n == 2) {
c.type = Type::PERPENDICULAR;
c.entityA = gs.vector[0];
c.entityB = gs.vector[1];
} else {
Error(_("Bad selection for perpendicular constraint. This "
"constraint can apply to:\n\n"
" * two faces\n"
" * two line segments\n"
" * a line segment and a normal\n"
" * two normals\n"));
return;
}
AddConstraint(&c);
newcons.push_back(c);
break;
case Command::WHERE_DRAGGED:
@ -764,6 +869,7 @@ void Constraint::MenuConstrain(Command id) {
return;
}
AddConstraint(&c);
newcons.push_back(c);
break;
case Command::COMMENT:
@ -774,6 +880,7 @@ void Constraint::MenuConstrain(Command id) {
c.workplane = SS.GW.ActiveWorkplane();
c.comment = _("NEW COMMENT -- DOUBLE-CLICK TO EDIT");
AddConstraint(&c);
newcons.push_back(c);
} else {
SS.GW.pending.operation = GraphicsWindow::Pending::COMMAND;
SS.GW.pending.command = Command::COMMENT;
@ -784,31 +891,32 @@ void Constraint::MenuConstrain(Command id) {
default: ssassert(false, "Unexpected menu ID");
}
for(const Constraint &cc : SK.constraint) {
if(c.h != cc.h && c.Equals(cc)) {
// Oops, we already have this exact constraint. Remove the one we just added.
SK.constraint.RemoveById(c.h);
SS.GW.ClearSelection();
// And now select the old one, to give feedback.
SS.GW.MakeSelected(cc.h);
return;
for (auto nc:newcons){
for(const Constraint &cc : SK.constraint) {
if(nc.h != cc.h && nc.Equals(cc)) {
// Oops, we already have this exact constraint. Remove the one we just added.
SK.constraint.RemoveById(nc.h);
SS.GW.ClearSelection();
// And now select the old one, to give feedback.
SS.GW.MakeSelected(cc.h);
return;
}
}
}
if(SK.constraint.FindByIdNoOops(c.h)) {
Constraint *constraint = SK.GetConstraint(c.h);
if(SS.TestRankForGroup(c.group) == SolveResult::REDUNDANT_OKAY &&
!SK.GetGroup(SS.GW.activeGroup)->allowRedundant &&
constraint->HasLabel()) {
constraint->reference = true;
if(SK.constraint.FindByIdNoOops(nc.h)) {
Constraint *constraint = SK.GetConstraint(nc.h);
if(SS.TestRankForGroup(nc.group) == SolveResult::REDUNDANT_OKAY &&
!SK.GetGroup(SS.GW.activeGroup)->allowRedundant &&
constraint->HasLabel()) {
constraint->reference = true;
}
}
}
if((id == Command::DISTANCE_DIA || id == Command::ANGLE ||
id == Command::RATIO || id == Command::DIFFERENCE) &&
SS.immediatelyEditDimension) {
SS.GW.EditConstraint(c.h);
if((id == Command::DISTANCE_DIA || id == Command::ANGLE ||
id == Command::RATIO || id == Command::DIFFERENCE) &&
SS.immediatelyEditDimension) {
SS.GW.EditConstraint(nc.h);
}
}
SS.GW.ClearSelection();

View File

@ -18,7 +18,11 @@ bool ConstraintBase::HasLabel() const {
case Type::PROJ_PT_DISTANCE:
case Type::DIAMETER:
case Type::LENGTH_RATIO:
case Type::ARC_ARC_LEN_RATIO:
case Type::ARC_LINE_LEN_RATIO:
case Type::LENGTH_DIFFERENCE:
case Type::ARC_ARC_DIFFERENCE:
case Type::ARC_LINE_DIFFERENCE:
case Type::ANGLE:
case Type::COMMENT:
return true;
@ -39,7 +43,11 @@ bool ConstraintBase::IsProjectible() const {
case Type::EQ_PT_LN_DISTANCES:
case Type::EQUAL_ANGLE:
case Type::LENGTH_RATIO:
case Type::ARC_ARC_LEN_RATIO:
case Type::ARC_LINE_LEN_RATIO:
case Type::LENGTH_DIFFERENCE:
case Type::ARC_ARC_DIFFERENCE:
case Type::ARC_LINE_DIFFERENCE:
case Type::SYMMETRIC:
case Type::SYMMETRIC_HORIZ:
case Type::SYMMETRIC_VERT:
@ -335,6 +343,110 @@ void ConstraintBase::GenerateEquations(IdList<Equation,hEquation> *l,
return;
}
case Type::ARC_ARC_LEN_RATIO: {
EntityBase *arc1 = SK.GetEntity(entityA),
*arc2 = SK.GetEntity(entityB);
// And get the arc1 radius, and the cosine of its angle
EntityBase *ao1 = SK.GetEntity(arc1->point[0]),
*as1 = SK.GetEntity(arc1->point[1]),
*af1 = SK.GetEntity(arc1->point[2]);
ExprVector aos1 = (as1->PointGetExprs()).Minus(ao1->PointGetExprs()),
aof1 = (af1->PointGetExprs()).Minus(ao1->PointGetExprs());
Expr *r1 = aof1.Magnitude();
ExprVector n1 = arc1->Normal()->NormalExprsN();
ExprVector u1 = aos1.WithMagnitude(Expr::From(1.0));
ExprVector v1 = n1.Cross(u1);
// so in our new csys, we start at (1, 0, 0)
Expr *costheta1 = aof1.Dot(u1)->Div(r1);
Expr *sintheta1 = aof1.Dot(v1)->Div(r1);
double thetas1, thetaf1, dtheta1;
arc1->ArcGetAngles(&thetas1, &thetaf1, &dtheta1);
Expr *theta1;
if(dtheta1 < 3*PI/4) {
theta1 = costheta1->ACos();
} else if(dtheta1 < 5*PI/4) {
// As the angle crosses pi, cos theta1 is not invertible;
// so use the sine to stop blowing up
theta1 = Expr::From(PI)->Minus(sintheta1->ASin());
} else {
theta1 = (Expr::From(2*PI))->Minus(costheta1->ACos());
}
// And get the arc2 radius, and the cosine of its angle
EntityBase *ao2 = SK.GetEntity(arc2->point[0]),
*as2 = SK.GetEntity(arc2->point[1]),
*af2 = SK.GetEntity(arc2->point[2]);
ExprVector aos2 = (as2->PointGetExprs()).Minus(ao2->PointGetExprs()),
aof2 = (af2->PointGetExprs()).Minus(ao2->PointGetExprs());
Expr *r2 = aof2.Magnitude();
ExprVector n2 = arc2->Normal()->NormalExprsN();
ExprVector u2 = aos2.WithMagnitude(Expr::From(1.0));
ExprVector v2 = n2.Cross(u2);
// so in our new csys, we start at (1, 0, 0)
Expr *costheta2 = aof2.Dot(u2)->Div(r2);
Expr *sintheta2 = aof2.Dot(v2)->Div(r2);
double thetas2, thetaf2, dtheta2;
arc2->ArcGetAngles(&thetas2, &thetaf2, &dtheta2);
Expr *theta2;
if(dtheta2 < 3*PI/4) {
theta2 = costheta2->ACos();
} else if(dtheta2 < 5*PI/4) {
// As the angle crosses pi, cos theta2 is not invertible;
// so use the sine to stop blowing up
theta2 = Expr::From(PI)->Minus(sintheta2->ASin());
} else {
theta2 = (Expr::From(2*PI))->Minus(costheta2->ACos());
}
// And write the equation; (r1*theta1) / ( r2*theta2) = some ratio
AddEq(l, (r1->Times(theta1))->Div(r2->Times(theta2))->Minus(exA), 0);
return;
}
case Type::ARC_LINE_LEN_RATIO: {
EntityBase *line = SK.GetEntity(entityA),
*arc1 = SK.GetEntity(entityB);
Expr *ll = Distance(workplane, line->point[0], line->point[1]);
// And get the arc1 radius, and the cosine of its angle
EntityBase *ao1 = SK.GetEntity(arc1->point[0]),
*as1 = SK.GetEntity(arc1->point[1]),
*af1 = SK.GetEntity(arc1->point[2]);
ExprVector aos1 = (as1->PointGetExprs()).Minus(ao1->PointGetExprs()),
aof1 = (af1->PointGetExprs()).Minus(ao1->PointGetExprs());
Expr *r1 = aof1.Magnitude();
ExprVector n1 = arc1->Normal()->NormalExprsN();
ExprVector u1 = aos1.WithMagnitude(Expr::From(1.0));
ExprVector v1 = n1.Cross(u1);
// so in our new csys, we start at (1, 0, 0)
Expr *costheta1 = aof1.Dot(u1)->Div(r1);
Expr *sintheta1 = aof1.Dot(v1)->Div(r1);
double thetas1, thetaf1, dtheta1;
arc1->ArcGetAngles(&thetas1, &thetaf1, &dtheta1);
Expr *theta1;
if(dtheta1 < 3*PI/4) {
theta1 = costheta1->ACos();
} else if(dtheta1 < 5*PI/4) {
// As the angle crosses pi, cos theta1 is not invertible;
// so use the sine to stop blowing up
theta1 = Expr::From(PI)->Minus(sintheta1->ASin());
} else {
theta1 = (Expr::From(2*PI))->Minus(costheta1->ACos());
}
// And write the equation; (r1*theta1) / ( length) = some ratio
AddEq(l, (r1->Times(theta1))->Div(ll)->Minus(exA), 0);
return;
}
case Type::LENGTH_DIFFERENCE: {
EntityBase *a = SK.GetEntity(entityA);
EntityBase *b = SK.GetEntity(entityB);
@ -344,6 +456,110 @@ void ConstraintBase::GenerateEquations(IdList<Equation,hEquation> *l,
return;
}
case Type::ARC_ARC_DIFFERENCE: {
EntityBase *arc1 = SK.GetEntity(entityA),
*arc2 = SK.GetEntity(entityB);
// And get the arc1 radius, and the cosine of its angle
EntityBase *ao1 = SK.GetEntity(arc1->point[0]),
*as1 = SK.GetEntity(arc1->point[1]),
*af1 = SK.GetEntity(arc1->point[2]);
ExprVector aos1 = (as1->PointGetExprs()).Minus(ao1->PointGetExprs()),
aof1 = (af1->PointGetExprs()).Minus(ao1->PointGetExprs());
Expr *r1 = aof1.Magnitude();
ExprVector n1 = arc1->Normal()->NormalExprsN();
ExprVector u1 = aos1.WithMagnitude(Expr::From(1.0));
ExprVector v1 = n1.Cross(u1);
// so in our new csys, we start at (1, 0, 0)
Expr *costheta1 = aof1.Dot(u1)->Div(r1);
Expr *sintheta1 = aof1.Dot(v1)->Div(r1);
double thetas1, thetaf1, dtheta1;
arc1->ArcGetAngles(&thetas1, &thetaf1, &dtheta1);
Expr *theta1;
if(dtheta1 < 3*PI/4) {
theta1 = costheta1->ACos();
} else if(dtheta1 < 5*PI/4) {
// As the angle crosses pi, cos theta1 is not invertible;
// so use the sine to stop blowing up
theta1 = Expr::From(PI)->Minus(sintheta1->ASin());
} else {
theta1 = (Expr::From(2*PI))->Minus(costheta1->ACos());
}
// And get the arc2 radius, and the cosine of its angle
EntityBase *ao2 = SK.GetEntity(arc2->point[0]),
*as2 = SK.GetEntity(arc2->point[1]),
*af2 = SK.GetEntity(arc2->point[2]);
ExprVector aos2 = (as2->PointGetExprs()).Minus(ao2->PointGetExprs()),
aof2 = (af2->PointGetExprs()).Minus(ao2->PointGetExprs());
Expr *r2 = aof2.Magnitude();
ExprVector n2 = arc2->Normal()->NormalExprsN();
ExprVector u2 = aos2.WithMagnitude(Expr::From(1.0));
ExprVector v2 = n2.Cross(u2);
// so in our new csys, we start at (1, 0, 0)
Expr *costheta2 = aof2.Dot(u2)->Div(r2);
Expr *sintheta2 = aof2.Dot(v2)->Div(r2);
double thetas2, thetaf2, dtheta2;
arc2->ArcGetAngles(&thetas2, &thetaf2, &dtheta2);
Expr *theta2;
if(dtheta2 < 3*PI/4) {
theta2 = costheta2->ACos();
} else if(dtheta2 < 5*PI/4) {
// As the angle crosses pi, cos theta2 is not invertible;
// so use the sine to stop blowing up
theta2 = Expr::From(PI)->Minus(sintheta2->ASin());
} else {
theta2 = (Expr::From(2*PI))->Minus(costheta2->ACos());
}
// And write the equation; (r1*theta1) - ( r2*theta2) = some difference
AddEq(l, (r1->Times(theta1))->Minus(r2->Times(theta2))->Minus(exA), 0);
return;
}
case Type::ARC_LINE_DIFFERENCE: {
EntityBase *line = SK.GetEntity(entityA),
*arc1 = SK.GetEntity(entityB);
Expr *ll = Distance(workplane, line->point[0], line->point[1]);
// And get the arc1 radius, and the cosine of its angle
EntityBase *ao1 = SK.GetEntity(arc1->point[0]),
*as1 = SK.GetEntity(arc1->point[1]),
*af1 = SK.GetEntity(arc1->point[2]);
ExprVector aos1 = (as1->PointGetExprs()).Minus(ao1->PointGetExprs()),
aof1 = (af1->PointGetExprs()).Minus(ao1->PointGetExprs());
Expr *r1 = aof1.Magnitude();
ExprVector n1 = arc1->Normal()->NormalExprsN();
ExprVector u1 = aos1.WithMagnitude(Expr::From(1.0));
ExprVector v1 = n1.Cross(u1);
// so in our new csys, we start at (1, 0, 0)
Expr *costheta1 = aof1.Dot(u1)->Div(r1);
Expr *sintheta1 = aof1.Dot(v1)->Div(r1);
double thetas1, thetaf1, dtheta1;
arc1->ArcGetAngles(&thetas1, &thetaf1, &dtheta1);
Expr *theta1;
if(dtheta1 < 3*PI/4) {
theta1 = costheta1->ACos();
} else if(dtheta1 < 5*PI/4) {
// As the angle crosses pi, cos theta1 is not invertible;
// so use the sine to stop blowing up
theta1 = Expr::From(PI)->Minus(sintheta1->ASin());
} else {
theta1 = (Expr::From(2*PI))->Minus(costheta1->ACos());
}
// And write the equation; (r1*theta1) - ( length) = some difference
AddEq(l, (r1->Times(theta1))->Minus(ll)->Minus(exA), 0);
return;
}
case Type::DIAMETER: {
EntityBase *circle = SK.GetEntity(entityA);
Expr *r = circle->CircleGetRadiusExpr();

View File

@ -19,6 +19,17 @@ void TextWindow::ScreenEditTtfText(int link, uint32_t v) {
SS.TW.edit.request = hr;
}
void TextWindow::ScreenToggleTtfKerning(int link, uint32_t v) {
hRequest hr = { v };
Request *r = SK.GetRequest(hr);
SS.UndoRemember();
r->extraPoints = !r->extraPoints;
SS.MarkGroupDirty(r->group);
SS.ScheduleShowTW();
}
void TextWindow::ScreenSetTtfFont(int link, uint32_t v) {
int i = (int)v;
if(i < 0) return;
@ -64,17 +75,36 @@ void TextWindow::ScreenConstraintShowAsRadius(int link, uint32_t v) {
void TextWindow::DescribeSelection() {
Printf(false, "");
#define COSTR_NO_LINK(p) \
SS.MmToString((p).x).c_str(), \
SS.MmToString((p).y).c_str(), \
SS.MmToString((p).z).c_str()
#define PT_AS_STR_NO_LINK "(%Fi%s%Fd, %Fi%s%Fd, %Fi%s%Fd)"
#define PT_AS_NUM "(%Fi%3%Fd, %Fi%3%Fd, %Fi%3%Fd)"
#define COSTR(e, p) \
e->h, (&TextWindow::ScreenSelectEntity), (&TextWindow::ScreenHoverEntity), \
COSTR_NO_LINK(p)
#define PT_AS_STR "%Ll%D%f%h" PT_AS_STR_NO_LINK "%E"
#define CO_LINK(e, p) e->h, (&TextWindow::ScreenSelectEntity), (&TextWindow::ScreenHoverEntity), CO(p)
#define PT_AS_NUM_LINK "%Ll%D%f%h" PT_AS_NUM "%E"
auto const &gs = SS.GW.gs;
auto ListFaces = [&]() {
char abc = 'A';
for(auto &fc : gs.face) {
Vector n = SK.GetEntity(fc)->FaceGetNormalNum();
Printf(true, " plane%c normal = " PT_AS_NUM, abc, CO(n));
Vector p = SK.GetEntity(fc)->FaceGetPointNum();
Printf(false, " plane%c thru = " PT_AS_STR, abc, COSTR(SK.GetEntity(fc), p));
++abc;
}
};
if(gs.n == 1 && (gs.points == 1 || gs.entities == 1)) {
Entity *e = SK.GetEntity(gs.points == 1 ? gs.point[0] : gs.entity[0]);
Vector p;
#define COSTR(p) \
SS.MmToString((p).x).c_str(), \
SS.MmToString((p).y).c_str(), \
SS.MmToString((p).z).c_str()
#define PT_AS_STR "(%Fi%s%E, %Fi%s%E, %Fi%s%E)"
#define PT_AS_NUM "(%Fi%3%E, %Fi%3%E, %Fi%3%E)"
switch(e->type) {
case Entity::Type::POINT_IN_3D:
case Entity::Type::POINT_IN_2D:
@ -82,8 +112,9 @@ void TextWindow::DescribeSelection() {
case Entity::Type::POINT_N_ROT_TRANS:
case Entity::Type::POINT_N_COPY:
case Entity::Type::POINT_N_ROT_AA:
case Entity::Type::POINT_N_ROT_AXIS_TRANS:
p = e->PointGetNum();
Printf(false, "%FtPOINT%E at " PT_AS_STR, COSTR(p));
Printf(false, "%FtPOINT%E at " PT_AS_STR, COSTR(e, p));
break;
case Entity::Type::NORMAL_IN_3D:
@ -104,20 +135,20 @@ void TextWindow::DescribeSelection() {
case Entity::Type::WORKPLANE: {
p = SK.GetEntity(e->point[0])->PointGetNum();
Printf(false, "%FtWORKPLANE%E");
Printf(true, " origin = " PT_AS_STR, COSTR(p));
Printf(true, " origin = " PT_AS_STR, COSTR(SK.GetEntity(e->point[0]), p));
Quaternion q = e->Normal()->NormalGetNum();
p = q.RotationN();
Printf(true, " normal = " PT_AS_NUM, CO(p));
Printf(true, " normal = " PT_AS_NUM_LINK, CO_LINK(e->Normal(), p));
break;
}
case Entity::Type::LINE_SEGMENT: {
Vector p0 = SK.GetEntity(e->point[0])->PointGetNum();
p = p0;
Printf(false, "%FtLINE SEGMENT%E");
Printf(true, " thru " PT_AS_STR, COSTR(p));
Printf(true, " thru " PT_AS_STR, COSTR(SK.GetEntity(e->point[0]), p));
Vector p1 = SK.GetEntity(e->point[1])->PointGetNum();
p = p1;
Printf(false, " " PT_AS_STR, COSTR(p));
Printf(false, " " PT_AS_STR, COSTR(SK.GetEntity(e->point[1]), p));
Printf(true, " len = %Fi%s%E",
SS.MmToString((p1.Minus(p0).Magnitude())).c_str());
break;
@ -137,18 +168,18 @@ void TextWindow::DescribeSelection() {
}
for(int i = 0; i < pts; i++) {
p = SK.GetEntity(e->point[i])->PointGetNum();
Printf((i==0), " p%d = " PT_AS_STR, i, COSTR(p));
Printf((i==0), " p%d = " PT_AS_STR, i, COSTR(SK.GetEntity(e->point[i]), p));
}
break;
case Entity::Type::ARC_OF_CIRCLE: {
Printf(false, "%FtARC OF A CIRCLE%E");
p = SK.GetEntity(e->point[0])->PointGetNum();
Printf(true, " center = " PT_AS_STR, COSTR(p));
Printf(true, " center = " PT_AS_STR, COSTR(SK.GetEntity(e->point[0]), p));
p = SK.GetEntity(e->point[1])->PointGetNum();
Printf(true, " endpoints = " PT_AS_STR, COSTR(p));
Printf(true, " endpoints = " PT_AS_STR, COSTR(SK.GetEntity(e->point[1]), p));
p = SK.GetEntity(e->point[2])->PointGetNum();
Printf(false, " " PT_AS_STR, COSTR(p));
Printf(false, " " PT_AS_STR, COSTR(SK.GetEntity(e->point[2]), p));
double r = e->CircleGetRadiusNum();
Printf(true, " diameter = %Fi%s", SS.MmToString(r*2).c_str());
Printf(false, " radius = %Fi%s", SS.MmToString(r).c_str());
@ -160,10 +191,11 @@ void TextWindow::DescribeSelection() {
case Entity::Type::CIRCLE: {
Printf(false, "%FtCIRCLE%E");
p = SK.GetEntity(e->point[0])->PointGetNum();
Printf(true, " center = " PT_AS_STR, COSTR(p));
Printf(true, " center = " PT_AS_STR, COSTR(SK.GetEntity(e->point[0]), p));
double r = e->CircleGetRadiusNum();
Printf(true, " diameter = %Fi%s", SS.MmToString(r*2).c_str());
Printf(false, " radius = %Fi%s", SS.MmToString(r).c_str());
Printf(true, " diameter = %Fi%s", SS.MmToString(r*2).c_str());
Printf(false, " radius = %Fi%s", SS.MmToString(r).c_str());
Printf(false, " circumference = %Fi%s", SS.MmToString(2*M_PI*r).c_str());
break;
}
case Entity::Type::FACE_NORMAL_PT:
@ -171,19 +203,24 @@ void TextWindow::DescribeSelection() {
case Entity::Type::FACE_N_ROT_TRANS:
case Entity::Type::FACE_N_ROT_AA:
case Entity::Type::FACE_N_TRANS:
Printf(false, "%FtPLANE FACE%E");
case Entity::Type::FACE_ROT_NORMAL_PT:
case Entity::Type::FACE_N_ROT_AXIS_TRANS:
Printf(false, "%FtPLANE FACE%E");
p = e->FaceGetNormalNum();
Printf(true, " normal = " PT_AS_NUM, CO(p));
p = e->FaceGetPointNum();
Printf(false, " thru = " PT_AS_STR, COSTR(p));
Printf(false, " thru = " PT_AS_STR, COSTR(e, p));
break;
case Entity::Type::TTF_TEXT: {
Printf(false, "%FtTRUETYPE FONT TEXT%E");
Printf(true, " font = '%Fi%s%E'", e->font.c_str());
if(e->h.isFromRequest()) {
Printf(false, " text = '%Fi%s%E' %Fl%Ll%f%D[change]%E",
Printf(true, " text = '%Fi%s%E' %Fl%Ll%f%D[change]%E",
e->str.c_str(), &ScreenEditTtfText, e->h.request().v);
Printf(true, " %Fd%f%D%Ll%s apply kerning",
&ScreenToggleTtfKerning, e->h.request().v,
e->extraPoints ? CHECK_TRUE : CHECK_FALSE);
Printf(true, " select new font");
SS.fonts.LoadAll();
// Not using range-for here because we use i inside the output.
@ -315,12 +352,12 @@ void TextWindow::DescribeSelection() {
} else if(gs.n == 2 && gs.points == 2) {
Printf(false, "%FtTWO POINTS");
Vector p0 = SK.GetEntity(gs.point[0])->PointGetNum();
Printf(true, " at " PT_AS_STR, COSTR(p0));
Printf(true, " at " PT_AS_STR, COSTR(SK.GetEntity(gs.point[0]), p0));
Vector p1 = SK.GetEntity(gs.point[1])->PointGetNum();
Printf(false, " " PT_AS_STR, COSTR(p1));
Printf(false, " " PT_AS_STR, COSTR(SK.GetEntity(gs.point[1]), p1));
Vector dv = p1.Minus(p0);
Printf(true, " d = %Fi%s", SS.MmToString(dv.Magnitude()).c_str());
Printf(false, " d(x, y, z) = " PT_AS_STR, COSTR(dv));
Printf(false, " d(x, y, z) = " PT_AS_STR_NO_LINK, COSTR_NO_LINK(dv));
} else if(gs.n == 2 && gs.points == 1 && gs.circlesOrArcs == 1) {
Entity *ec = SK.GetEntity(gs.entity[0]);
if(ec->type == Entity::Type::CIRCLE) {
@ -329,9 +366,9 @@ void TextWindow::DescribeSelection() {
Printf(false, "%FtPOINT AND AN ARC");
} else ssassert(false, "Unexpected entity type");
Vector p = SK.GetEntity(gs.point[0])->PointGetNum();
Printf(true, " pt at " PT_AS_STR, COSTR(p));
Printf(true, " pt at " PT_AS_STR, COSTR(SK.GetEntity(gs.point[0]), p));
Vector c = SK.GetEntity(ec->point[0])->PointGetNum();
Printf(true, " center = " PT_AS_STR, COSTR(c));
Printf(true, " center = " PT_AS_STR, COSTR(SK.GetEntity(ec->point[0]), c));
double r = ec->CircleGetRadiusNum();
Printf(false, " diameter = %Fi%s", SS.MmToString(r*2).c_str());
Printf(false, " radius = %Fi%s", SS.MmToString(r).c_str());
@ -340,22 +377,22 @@ void TextWindow::DescribeSelection() {
} else if(gs.n == 2 && gs.faces == 1 && gs.points == 1) {
Printf(false, "%FtA POINT AND A PLANE FACE");
Vector pt = SK.GetEntity(gs.point[0])->PointGetNum();
Printf(true, " point = " PT_AS_STR, COSTR(pt));
Printf(true, " point = " PT_AS_STR, COSTR(SK.GetEntity(gs.point[0]), pt));
Vector n = SK.GetEntity(gs.face[0])->FaceGetNormalNum();
Printf(true, " plane normal = " PT_AS_NUM, CO(n));
Vector pl = SK.GetEntity(gs.face[0])->FaceGetPointNum();
Printf(false, " plane thru = " PT_AS_STR, COSTR(pl));
Printf(false, " plane thru = " PT_AS_STR, COSTR(SK.GetEntity(gs.face[0]), pl));
double dd = n.Dot(pl) - n.Dot(pt);
Printf(true, " distance = %Fi%s", SS.MmToString(dd).c_str());
} else if(gs.n == 3 && gs.points == 2 && gs.vectors == 1) {
Printf(false, "%FtTWO POINTS AND A VECTOR");
Vector p0 = SK.GetEntity(gs.point[0])->PointGetNum();
Printf(true, " pointA = " PT_AS_STR, COSTR(p0));
Printf(true, " pointA = " PT_AS_STR, COSTR(SK.GetEntity(gs.point[0]), p0));
Vector p1 = SK.GetEntity(gs.point[1])->PointGetNum();
Printf(false, " pointB = " PT_AS_STR, COSTR(p1));
Printf(false, " pointB = " PT_AS_STR, COSTR(SK.GetEntity(gs.point[1]), p1));
Vector v = SK.GetEntity(gs.vector[0])->VectorGetNum();
v = v.WithMagnitude(1);
Printf(true, " vector = " PT_AS_NUM, CO(v));
Printf(true, " vector = " PT_AS_NUM_LINK, CO_LINK(SK.GetEntity(gs.vector[0]), v));
double d = (p1.Minus(p0)).Dot(v);
Printf(true, " proj_d = %Fi%s", SS.MmToString(d).c_str());
} else if(gs.n == 2 && gs.lineSegments == 1 && gs.points == 1) {
@ -363,11 +400,11 @@ void TextWindow::DescribeSelection() {
Vector lp0 = SK.GetEntity(ln->point[0])->PointGetNum(),
lp1 = SK.GetEntity(ln->point[1])->PointGetNum();
Printf(false, "%FtLINE SEGMENT AND POINT%E");
Printf(true, " ln thru " PT_AS_STR, COSTR(lp0));
Printf(false, " " PT_AS_STR, COSTR(lp1));
Printf(true, " ln thru " PT_AS_STR, COSTR(SK.GetEntity(ln->point[0]), lp0));
Printf(false, " " PT_AS_STR, COSTR(SK.GetEntity(ln->point[1]), lp1));
Entity *p = SK.GetEntity(gs.point[0]);
Vector pp = p->PointGetNum();
Printf(true, " point " PT_AS_STR, COSTR(pp));
Printf(true, " point " PT_AS_STR, COSTR(p, pp));
Printf(true, " pt-ln distance = %Fi%s%E",
SS.MmToString(pp.DistanceToLine(lp0, lp1.Minus(lp0))).c_str());
hEntity wrkpl = SS.GW.ActiveWorkplane();
@ -386,8 +423,8 @@ void TextWindow::DescribeSelection() {
v0 = v0.WithMagnitude(1);
v1 = v1.WithMagnitude(1);
Printf(true, " vectorA = " PT_AS_NUM, CO(v0));
Printf(false, " vectorB = " PT_AS_NUM, CO(v1));
Printf(true, " vectorA = " PT_AS_NUM_LINK, CO_LINK(SK.GetEntity(gs.entity[0]), v0));
Printf(false, " vectorB = " PT_AS_NUM_LINK, CO_LINK(SK.GetEntity(gs.entity[1]), v1));
double theta = acos(v0.Dot(v1));
Printf(true, " angle = %Fi%2%E degrees", theta*180/PI);
@ -397,15 +434,10 @@ void TextWindow::DescribeSelection() {
} else if(gs.n == 2 && gs.faces == 2) {
Printf(false, "%FtTWO PLANE FACES");
Vector n0 = SK.GetEntity(gs.face[0])->FaceGetNormalNum();
Printf(true, " planeA normal = " PT_AS_NUM, CO(n0));
Vector p0 = SK.GetEntity(gs.face[0])->FaceGetPointNum();
Printf(false, " planeA thru = " PT_AS_STR, COSTR(p0));
ListFaces();
Vector n0 = SK.GetEntity(gs.face[0])->FaceGetNormalNum();
Vector n1 = SK.GetEntity(gs.face[1])->FaceGetNormalNum();
Printf(true, " planeB normal = " PT_AS_NUM, CO(n1));
Vector p1 = SK.GetEntity(gs.face[1])->FaceGetPointNum();
Printf(false, " planeB thru = " PT_AS_STR, COSTR(p1));
double theta = acos(n0.Dot(n1));
Printf(true, " angle = %Fi%2%E degrees", theta*180/PI);
@ -414,17 +446,32 @@ void TextWindow::DescribeSelection() {
Printf(false, " or angle = %Fi%2%E (mod 180)", theta*180/PI);
if(fabs(theta) < 0.01) {
double d = (p1.Minus(p0)).Dot(n0);
Vector p0 = SK.GetEntity(gs.face[0])->FaceGetPointNum();
Vector p1 = SK.GetEntity(gs.face[1])->FaceGetPointNum();
double d = (p1.Minus(p0)).Dot(n0);
Printf(true, " distance = %Fi%s", SS.MmToString(d).c_str());
}
} else if(gs.n == 0 && gs.stylables > 0) {
Printf(false, "%FtSELECTED:%E comment text");
} else if(gs.n == 3 && gs.faces == 3) {
Printf(false, "%FtTHREE PLANE FACES");
ListFaces();
// We should probably compute and show the intersection point if there is one.
} else if(gs.n == 0 && gs.constraints == 1) {
Constraint *c = SK.GetConstraint(gs.constraint[0]);
const std::string &desc = c->DescriptionString().c_str();
if(c->type == Constraint::Type::COMMENT) {
Printf(false, "%FtCOMMENT%E %s", desc.c_str());
if(c->ptA != Entity::NO_ENTITY) {
Vector p = SK.GetEntity(c->ptA)->PointGetNum();
Printf(true, " attached to point at: " PT_AS_STR, COSTR(SK.GetEntity(c->ptA), p));
Vector dv = c->disp.offset;
Printf(false, " distance = %Fi%s", SS.MmToString(dv.Magnitude()).c_str());
Printf(false, " d(x, y, z) = " PT_AS_STR_NO_LINK, COSTR_NO_LINK(dv));
}
} else if(c->HasLabel()) {
if(c->reference) {
Printf(false, "%FtREFERENCE%E %s", desc.c_str());

View File

@ -185,15 +185,17 @@ void GraphicsWindow::MakeSelected(Selection *stog) {
if(stog->entity.v != 0 && SK.GetEntity(stog->entity)->IsFace()) {
// In the interest of speed for the triangle drawing code,
// only two faces may be selected at a time.
int c = 0;
// only MAX_SELECTABLE_FACES faces may be selected at a time.
unsigned int c = 0;
Selection *s;
selection.ClearTags();
for(s = selection.First(); s; s = selection.NextAfter(s)) {
hEntity he = s->entity;
if(he.v != 0 && SK.GetEntity(he)->IsFace()) {
c++;
if(c >= 2) s->tag = 1;
// See also GraphicsWindow::GroupSelection "if(e->IsFace())"
// and Group::DrawMesh "case DrawMeshAs::SELECTED:"
if(c >= MAX_SELECTABLE_FACES) s->tag = 1;
}
}
selection.RemoveTagged();
@ -218,13 +220,28 @@ void GraphicsWindow::SelectByMarquee() {
bool entityHasBBox;
BBox entityBBox = e.GetOrGenerateScreenBBox(&entityHasBBox);
if(entityHasBBox && entityBBox.Overlaps(marqueeBBox)) {
if(e.type == Entity::Type::LINE_SEGMENT) {
Vector p0 = SS.GW.ProjectPoint3(e.EndpointStart());
Vector p1 = SS.GW.ProjectPoint3(e.EndpointFinish());
if((!marqueeBBox.Contains({p0.x, p0.y}, 0)) &&
(!marqueeBBox.Contains({p1.x, p1.y}, 0))) {
// The selection marquee does not contain either of the line segment end points.
// This means that either the segment is entirely outside the marquee or that
// it intersects it. Check if it does...
if(!Vector::BoundingBoxIntersectsLine(marqueeBBox.maxp, marqueeBBox.minp, p0,
p1, true)) {
// ... it does not so it is outside.
continue;
}
}
}
MakeSelected(e.h);
}
}
}
//-----------------------------------------------------------------------------
// Sort the selection according to various critieria: the entities and
// Sort the selection according to various criteria: the entities and
// constraints separately, counts of certain types of entities (circles,
// lines, etc.), and so on.
//-----------------------------------------------------------------------------

View File

@ -12,7 +12,7 @@ std::string Constraint::Label() const {
std::string result;
if(type == Type::ANGLE) {
result = SS.DegreeToString(valA) + "°";
} else if(type == Type::LENGTH_RATIO) {
} else if(type == Type::LENGTH_RATIO || type == Type::ARC_ARC_LEN_RATIO || type == Type::ARC_LINE_LEN_RATIO) {
result = ssprintf("%.3f:1", valA);
} else if(type == Type::COMMENT) {
result = comment;
@ -267,7 +267,7 @@ void Constraint::DoEqualRadiusTicks(Canvas *canvas, Canvas::hStroke hcs,
const Camera &camera = canvas->GetCamera();
Entity *circ = SK.GetEntity(he);
Vector center = SK.GetEntity(circ->point[0])->PointGetNum();
Vector center = SK.GetEntity(circ->point[0])->PointGetDrawNum();
double r = circ->CircleGetRadiusNum();
Quaternion q = circ->Normal()->NormalGetNum();
Vector u = q.RotationU(), v = q.RotationV();
@ -291,7 +291,8 @@ void Constraint::DoEqualRadiusTicks(Canvas *canvas, Canvas::hStroke hcs,
void Constraint::DoArcForAngle(Canvas *canvas, Canvas::hStroke hcs,
Vector a0, Vector da, Vector b0, Vector db,
Vector offset, Vector *ref, bool trim)
Vector offset, Vector *ref, bool trim,
Vector explodeOffset)
{
const Camera &camera = canvas->GetCamera();
double pixels = 1.0 / camera.scale;
@ -305,6 +306,9 @@ void Constraint::DoArcForAngle(Canvas *canvas, Canvas::hStroke hcs,
db = db.ProjectVectorInto(workplane);
}
a0 = a0.Plus(explodeOffset);
b0 = b0.Plus(explodeOffset);
Vector a1 = a0.Plus(da);
Vector b1 = b0.Plus(db);
@ -445,21 +449,38 @@ void Constraint::DoArcForAngle(Canvas *canvas, Canvas::hStroke hcs,
}
bool Constraint::IsVisible() const {
if(!SS.GW.showConstraints) return false;
Group *g = SK.GetGroup(group);
// If the group is hidden, then the constraints are hidden and not
// able to be selected.
if(!(g->visible)) return false;
// And likewise if the group is not the active group; except for comments
// with an assigned style.
if(g->h != SS.GW.activeGroup && !(type == Type::COMMENT && disp.style.v)) {
if(SS.GW.showConstraints == GraphicsWindow::ShowConstraintMode::SCM_NOSHOW)
return false;
bool isDim = false;
if(SS.GW.showConstraints == GraphicsWindow::ShowConstraintMode::SCM_SHOW_DIM)
switch(type) {
case ConstraintBase::Type::ANGLE:
case ConstraintBase::Type::DIAMETER:
case ConstraintBase::Type::PT_PT_DISTANCE:
case ConstraintBase::Type::PT_FACE_DISTANCE:
case ConstraintBase::Type::PT_LINE_DISTANCE:
case ConstraintBase::Type::PT_PLANE_DISTANCE: isDim = true; break;
default:;
}
if(SS.GW.showConstraints == GraphicsWindow::ShowConstraintMode::SCM_SHOW_ALL || isDim ) {
Group *g = SK.GetGroup(group);
// If the group is hidden, then the constraints are hidden and not
// able to be selected.
if(!(g->visible)) return false;
// And likewise if the group is not the active group; except for comments
// with an assigned style.
if(g->h != SS.GW.activeGroup && !(type == Type::COMMENT && disp.style.v)) {
return false;
}
if(disp.style.v) {
Style *s = Style::Get(disp.style);
if(!s->visible) return false;
}
return true;
}
if(disp.style.v) {
Style *s = Style::Get(disp.style);
if(!s->visible) return false;
}
return true;
return false;
}
bool Constraint::DoLineExtend(Canvas *canvas, Canvas::hStroke hcs,
@ -534,6 +555,15 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
DoProjectedPoint(canvas, hcs, &bp);
}
if(ShouldDrawExploded()) {
// Offset A and B by the same offset so the constraint is drawn
// in the plane of one of the exploded points (rather than at an
// angle)
Vector offset = SK.GetEntity(ptA)->ExplodeOffset();
ap = ap.Plus(offset);
bp = bp.Plus(offset);
}
Vector ref = ((ap.Plus(bp)).ScaledBy(0.5)).Plus(disp.offset);
if(refs) refs->push_back(ref);
@ -548,6 +578,19 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
dp = (bp.Minus(ap)),
pp = SK.GetEntity(entityA)->VectorGetNum();
if(ShouldDrawExploded()) {
// explode for whichever point is in the workplane (or the first if both are)
Entity *pt = SK.GetEntity(ptA);
if(pt->group != group) {
pt = SK.GetEntity(ptB);
}
if(pt->group == group) {
Vector offset = pt->ExplodeOffset();
ap = ap.Plus(offset);
bp = bp.Plus(offset);
}
}
Vector ref = ((ap.Plus(bp)).ScaledBy(0.5)).Plus(disp.offset);
if(refs) refs->push_back(ref);
@ -564,7 +607,7 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
case Type::PT_FACE_DISTANCE:
case Type::PT_PLANE_DISTANCE: {
Vector pt = SK.GetEntity(ptA)->PointGetNum();
Vector pt = SK.GetEntity(ptA)->PointGetDrawNum();
Entity *enta = SK.GetEntity(entityA);
Vector n, p;
if(type == Type::PT_PLANE_DISTANCE) {
@ -590,7 +633,8 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
}
case Type::PT_LINE_DISTANCE: {
Vector pt = SK.GetEntity(ptA)->PointGetNum();
Entity *ptEntity = SK.GetEntity(ptA);
Vector pt = ptEntity->PointGetNum();
Entity *line = SK.GetEntity(entityA);
Vector lA = SK.GetEntity(line->point[0])->PointGetNum();
Vector lB = SK.GetEntity(line->point[1])->PointGetNum();
@ -602,6 +646,19 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
DoProjectedPoint(canvas, hcs, &pt);
}
// Only explode if the point and line are in the same group (and that group is a sketch
// with explode enabled) otherwise it's too visually confusing to figure out what the
// correct projections should be.
bool shouldExplode = ShouldDrawExploded()
&& ptEntity->group == group
&& line->group == group;
if(shouldExplode) {
Vector explodeOffset = ptEntity->ExplodeOffset();
pt = pt.Plus(explodeOffset);
lA = lA.Plus(explodeOffset);
lB = lB.Plus(explodeOffset);
}
// Find the closest point on the line
Vector closest = pt.ClosestPointOnLine(lA, dl);
@ -655,7 +712,7 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
case Type::DIAMETER: {
Entity *circle = SK.GetEntity(entityA);
Vector center = SK.GetEntity(circle->point[0])->PointGetNum();
Vector center = SK.GetEntity(circle->point[0])->PointGetDrawNum();
Quaternion q = SK.GetEntity(circle->normal)->NormalGetNum();
Vector n = q.RotationN().WithMagnitude(1);
double r = circle->CircleGetRadiusNum();
@ -697,7 +754,7 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
Vector r = camera.projRight.ScaledBy((a+1)/camera.scale);
Vector d = camera.projUp.ScaledBy((2-a)/camera.scale);
for(int i = 0; i < 2; i++) {
Vector p = SK.GetEntity(i == 0 ? ptA : ptB)-> PointGetNum();
Vector p = SK.GetEntity(i == 0 ? ptA : ptB)->PointGetDrawNum();
if(refs) refs->push_back(p);
canvas->DrawQuad(p.Plus (r).Plus (d),
p.Plus (r).Minus(d),
@ -715,7 +772,7 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
case Type::PT_ON_FACE:
case Type::PT_IN_PLANE: {
double s = 8/camera.scale;
Vector p = SK.GetEntity(ptA)->PointGetNum();
Vector p = SK.GetEntity(ptA)->PointGetDrawNum();
if(refs) refs->push_back(p);
Vector r, d;
if(type == Type::PT_ON_FACE) {
@ -740,7 +797,7 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
}
case Type::WHERE_DRAGGED: {
Vector p = SK.GetEntity(ptA)->PointGetNum();
Vector p = SK.GetEntity(ptA)->PointGetDrawNum();
if(refs) refs->push_back(p);
Vector u = p.Plus(gu.WithMagnitude(8/camera.scale)).Plus(
gr.WithMagnitude(8/camera.scale)),
@ -797,10 +854,10 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
}
DoArcForAngle(canvas, hcs, a0, da, b0, db,
da.WithMagnitude(40/camera.scale), &ref, /*trim=*/false);
da.WithMagnitude(40/camera.scale), &ref, /*trim=*/false, a->ExplodeOffset());
if(refs) refs->push_back(ref);
DoArcForAngle(canvas, hcs, c0, dc, d0, dd,
dc.WithMagnitude(40/camera.scale), &ref, /*trim=*/false);
dc.WithMagnitude(40/camera.scale), &ref, /*trim=*/false, c->ExplodeOffset());
if(refs) refs->push_back(ref);
return;
@ -820,7 +877,7 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
}
Vector ref;
DoArcForAngle(canvas, hcs, a0, da, b0, db, disp.offset, &ref, /*trim=*/true);
DoArcForAngle(canvas, hcs, a0, da, b0, db, disp.offset, &ref, /*trim=*/true, a->ExplodeOffset());
DoLabel(canvas, hcs, ref, labelPos, gr, gu);
if(refs) refs->push_back(ref);
return;
@ -855,7 +912,7 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
if(u.Dot(ru) < 0) u = u.ScaledBy(-1);
}
Vector p = e->VectorGetRefPoint();
Vector p = e->VectorGetRefPoint().Plus(e->ExplodeOffset());
Vector s = p.Plus(u).Plus(v);
DoLine(canvas, hcs, s, s.Plus(v));
Vector m = s.Plus(v.ScaledBy(0.5));
@ -873,9 +930,9 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
if(type == Type::ARC_LINE_TANGENT) {
Entity *arc = SK.GetEntity(entityA);
Entity *norm = SK.GetEntity(arc->normal);
Vector c = SK.GetEntity(arc->point[0])->PointGetNum();
Vector c = SK.GetEntity(arc->point[0])->PointGetDrawNum();
Vector p =
SK.GetEntity(arc->point[other ? 2 : 1])->PointGetNum();
SK.GetEntity(arc->point[other ? 2 : 1])->PointGetDrawNum();
Vector r = p.Minus(c);
textAt = p.Plus(r.WithMagnitude(14/camera.scale));
u = norm->NormalU();
@ -896,6 +953,7 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
Entity *cubic = SK.GetEntity(entityA);
Vector p = other ? cubic->CubicGetFinishNum() :
cubic->CubicGetStartNum();
p = p.Plus(cubic->ExplodeOffset());
Vector dir = SK.GetEntity(entityB)->VectorGetNum();
Vector out = n.Cross(dir);
textAt = p.Plus(out.WithMagnitude(14/camera.scale));
@ -905,12 +963,12 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
u = wn->NormalU();
v = wn->NormalV();
n = wn->NormalN();
EntityBase *eA = SK.GetEntity(entityA);
Entity *eA = SK.GetEntity(entityA);
// Big pain; we have to get a vector tangent to the curve
// at the shared point, which could be from either a cubic
// or an arc.
if(other) {
textAt = eA->EndpointFinish();
textAt = eA->EndpointFinish().Plus(eA->ExplodeOffset());
if(eA->type == Entity::Type::CUBIC) {
dir = eA->CubicGetFinishTangentNum();
} else {
@ -919,7 +977,7 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
dir = n.Cross(dir);
}
} else {
textAt = eA->EndpointStart();
textAt = eA->EndpointStart().Plus(eA->ExplodeOffset());
if(eA->type == Entity::Type::CUBIC) {
dir = eA->CubicGetStartTangentNum();
} else {
@ -947,6 +1005,10 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
Vector u = (gn.Cross(n)).WithMagnitude(4/camera.scale);
Vector p = e->VectorGetRefPoint();
if(ShouldDrawExploded()) {
p = p.Plus(e->ExplodeOffset());
}
DoLine(canvas, hcs, p.Plus(u), p.Plus(u).Plus(n));
DoLine(canvas, hcs, p.Minus(u), p.Minus(u).Plus(n));
if(refs) refs->push_back(p.Plus(n.ScaledBy(0.5)));
@ -967,8 +1029,8 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
Entity *line = SK.GetEntity(entityA);
Vector ref;
DoEqualLenTicks(canvas, hcs,
SK.GetEntity(line->point[0])->PointGetNum(),
SK.GetEntity(line->point[1])->PointGetNum(),
SK.GetEntity(line->point[0])->PointGetDrawNum(),
SK.GetEntity(line->point[1])->PointGetDrawNum(),
gn, &ref);
if(refs) refs->push_back(ref);
DoEqualRadiusTicks(canvas, hcs, entityB, &ref);
@ -990,6 +1052,12 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
DoProjectedPoint(canvas, hcs, &b);
}
if(ShouldDrawExploded()) {
Vector offset = e->ExplodeOffset();
a = a.Plus(offset);
b = b.Plus(offset);
}
Vector ref;
DoEqualLenTicks(canvas, hcs, a, b, gn, &ref);
if(refs) refs->push_back(ref);
@ -1000,6 +1068,41 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
}
return;
}
case Type::ARC_ARC_LEN_RATIO:
case Type::ARC_ARC_DIFFERENCE: {
Entity *circle = SK.GetEntity(entityA);
Vector center = SK.GetEntity(circle->point[0])->PointGetNum();
Quaternion q = SK.GetEntity(circle->normal)->NormalGetNum();
Vector n = q.RotationN().WithMagnitude(1);
Vector ref2;
DoEqualRadiusTicks(canvas, hcs, entityA, &ref2);
DoEqualRadiusTicks(canvas, hcs, entityB, &ref2);
Vector ref = center.Plus(disp.offset);
// Force the label into the same plane as the circle.
ref = ref.Minus(n.ScaledBy(n.Dot(ref) - n.Dot(center)));
if(refs) refs->push_back(ref);
Vector topLeft;
DoLabel(canvas, hcs, ref, &topLeft, gr, gu);
if(labelPos) *labelPos = topLeft;
return;
}
case Type::ARC_LINE_LEN_RATIO:
case Type::ARC_LINE_DIFFERENCE: {
Vector a, b = Vector::From(0, 0, 0);
Vector ref;
Entity *e = SK.GetEntity(entityA);
a = SK.GetEntity(e->point[0])->PointGetNum();
b = SK.GetEntity(e->point[1])->PointGetNum();
DoEqualLenTicks(canvas, hcs, a, b, gn, &ref);
if(refs) refs->push_back(ref);
DoEqualRadiusTicks(canvas, hcs, entityB, &ref);
if(refs) refs->push_back(ref);
ref = ((a.Plus(b)).ScaledBy(0.5)).Plus(disp.offset);
DoLabel(canvas, hcs, ref, labelPos, gr, gu);
return;
}
case Type::EQ_LEN_PT_LINE_D: {
Entity *forLen = SK.GetEntity(entityA);
@ -1009,6 +1112,11 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
DoProjectedPoint(canvas, hcs, &a);
DoProjectedPoint(canvas, hcs, &b);
}
if(ShouldDrawExploded()) {
Vector offset = forLen->ExplodeOffset();
a = a.Plus(offset);
b = b.Plus(offset);
}
Vector refa;
DoEqualLenTicks(canvas, hcs, a, b, gn, &refa);
if(refs) refs->push_back(refa);
@ -1024,6 +1132,11 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
}
Vector closest = pt.ClosestPointOnLine(la, lb.Minus(la));
if(ShouldDrawExploded()) {
Vector offset = SK.GetEntity(ptA)->ExplodeOffset();
pt = pt.Plus(offset);
closest = closest.Plus(offset);
}
DoLine(canvas, hcs, pt, closest);
Vector refb;
DoEqualLenTicks(canvas, hcs, pt, closest, gn, &refb);
@ -1046,6 +1159,11 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
}
Vector closest = pt.ClosestPointOnLine(la, lb.Minus(la));
if(ShouldDrawExploded()) {
Vector offset = pte->ExplodeOffset();
pt = pt.Plus(offset);
closest = closest.Plus(offset);
}
DoLine(canvas, hcs, pt, closest);
Vector ref;
@ -1075,8 +1193,8 @@ void Constraint::DoLayout(DrawAs how, Canvas *canvas,
goto s;
}
s:
Vector a = SK.GetEntity(ptA)->PointGetNum();
Vector b = SK.GetEntity(ptB)->PointGetNum();
Vector a = SK.GetEntity(ptA)->PointGetDrawNum();
Vector b = SK.GetEntity(ptB)->PointGetDrawNum();
for(int i = 0; i < 2; i++) {
Vector tail = (i == 0) ? a : b;
@ -1113,8 +1231,8 @@ s:
}
// For "at midpoint", this branch is always taken.
Entity *e = SK.GetEntity(entityA);
Vector a = SK.GetEntity(e->point[0])->PointGetNum();
Vector b = SK.GetEntity(e->point[1])->PointGetNum();
Vector a = SK.GetEntity(e->point[0])->PointGetDrawNum();
Vector b = SK.GetEntity(e->point[1])->PointGetDrawNum();
Vector m = (a.ScaledBy(0.5)).Plus(b.ScaledBy(0.5));
Vector offset = (a.Minus(b)).Cross(n);
offset = offset.WithMagnitude(textHeight);
@ -1138,8 +1256,8 @@ s:
r.WithMagnitude(1), u.WithMagnitude(1), hcs);
if(refs) refs->push_back(o);
} else {
Vector a = SK.GetEntity(ptA)->PointGetNum();
Vector b = SK.GetEntity(ptB)->PointGetNum();
Vector a = SK.GetEntity(ptA)->PointGetDrawNum();
Vector b = SK.GetEntity(ptB)->PointGetDrawNum();
Entity *w = SK.GetEntity(workplane);
Vector cu = w->Normal()->NormalU();
@ -1243,7 +1361,11 @@ bool Constraint::HasLabel() const {
case Type::PT_FACE_DISTANCE:
case Type::PROJ_PT_DISTANCE:
case Type::LENGTH_RATIO:
case Type::ARC_ARC_LEN_RATIO:
case Type::ARC_LINE_LEN_RATIO:
case Type::LENGTH_DIFFERENCE:
case Type::ARC_ARC_DIFFERENCE:
case Type::ARC_LINE_DIFFERENCE:
case Type::DIAMETER:
case Type::ANGLE:
return true;
@ -1252,3 +1374,7 @@ bool Constraint::HasLabel() const {
return false;
}
}
bool Constraint::ShouldDrawExploded() const {
return SK.GetGroup(group)->ShouldDrawExploded();
}

View File

@ -88,7 +88,7 @@ void Entity::GetReferencePoints(std::vector<Vector> *refs) {
case Type::POINT_N_ROT_AXIS_TRANS:
case Type::POINT_IN_3D:
case Type::POINT_IN_2D:
refs->push_back(PointGetNum());
refs->push_back(PointGetDrawNum());
break;
case Type::NORMAL_N_COPY:
@ -103,12 +103,12 @@ void Entity::GetReferencePoints(std::vector<Vector> *refs) {
case Type::CUBIC_PERIODIC:
case Type::TTF_TEXT:
case Type::IMAGE:
refs->push_back(SK.GetEntity(point[0])->PointGetNum());
refs->push_back(SK.GetEntity(point[0])->PointGetDrawNum());
break;
case Type::LINE_SEGMENT: {
Vector a = SK.GetEntity(point[0])->PointGetNum(),
b = SK.GetEntity(point[1])->PointGetNum();
Vector a = SK.GetEntity(point[0])->PointGetDrawNum(),
b = SK.GetEntity(point[1])->PointGetDrawNum();
refs->push_back(b.Plus(a.Minus(b).ScaledBy(0.5)));
break;
}
@ -180,19 +180,43 @@ bool Entity::IsVisible() const {
return true;
}
static bool PtCanDrag(hEntity pt) {
Entity* p = SK.GetEntity(pt);
// a numeric copy can not move
if(p->type == Entity::Type::POINT_N_COPY) return false;
// these transforms applied zero times can not be moved
if(((p->type == Entity::Type::POINT_N_TRANS) ||
(p->type == Entity::Type::POINT_N_ROT_AA) ||
(p->type == Entity::Type::POINT_N_ROT_AXIS_TRANS))
&& (p->timesApplied == 0)) return false;
return true;
}
// entities that were created via some copy types will not be
// draggable with the mouse. We identify the undraggables here
bool Entity::CanBeDragged() const {
// a numeric copy can not move
if(type == Entity::Type::POINT_N_COPY) return false;
// these transforms applied zero times can not be moved
if(((type == Entity::Type::POINT_N_TRANS) ||
(type == Entity::Type::POINT_N_ROT_AA) ||
(type == Entity::Type::POINT_N_ROT_AXIS_TRANS))
&& (timesApplied == 0)) return false;
if(IsPoint()) {
if(!PtCanDrag(h))
return false;
// are we constrained pt-on-point from a previous group?
for(const Constraint &cc : SK.constraint) {
if(cc.group == group && cc.type == ConstraintBase::Type::POINTS_COINCIDENT) {
if(cc.ptA == h) {
if((SK.GetEntity(cc.ptB)->group < group)
|| (!PtCanDrag(cc.ptB)))
return false;
}
if(cc.ptB == h) {
if((SK.GetEntity(cc.ptA)->group < group)
|| (!PtCanDrag(cc.ptA)))
return false;
}
}
}
}
// for these types of entities the first point will indicate draggability
if(HasEndpoints() || type == Entity::Type::CIRCLE) {
return SK.GetEntity(point[0])->CanBeDragged();
return PtCanDrag(point[0]);
}
// if we're not certain it can't be dragged then default to true
return true;
@ -451,7 +475,8 @@ void Entity::GenerateBezierCurves(SBezierList *sbl) const {
Vector v = topLeft.Minus(botLeft);
Vector u = (v.Cross(n)).WithMagnitude(v.Magnitude());
SS.fonts.PlotString(font, str, sbl, botLeft, u, v);
// `extraPoints` is storing kerning boolean
SS.fonts.PlotString(font, str, sbl, extraPoints, botLeft, u, v);
break;
}
@ -466,6 +491,26 @@ void Entity::GenerateBezierCurves(SBezierList *sbl) const {
}
}
bool Entity::ShouldDrawExploded() const {
return SK.GetGroup(group)->ShouldDrawExploded();
}
Vector Entity::ExplodeOffset() const {
if(ShouldDrawExploded() && workplane.v != 0) {
int requestIdx = SK.GetRequest(h.request())->groupRequestIndex;
double offset = SS.explodeDistance * (requestIdx + 1);
return SK.GetEntity(workplane)->Normal()->NormalN().ScaledBy(offset);
} else {
return Vector::From(0, 0, 0);
}
}
Vector Entity::PointGetDrawNum() const {
// As per EntityBase::PointGetNum but specifically for when drawing/rendering the point
// (and not when solving), so we can potentially draw it somewhere different
return PointGetNum().Plus(ExplodeOffset());
}
void Entity::Draw(DrawAs how, Canvas *canvas) {
if(!(how == DrawAs::HOVERED || how == DrawAs::SELECTED) &&
!IsVisible()) return;
@ -557,16 +602,17 @@ void Entity::Draw(DrawAs how, Canvas *canvas) {
pointStroke.unit = Canvas::Unit::PX;
Canvas::hStroke hcsPoint = canvas->GetStroke(pointStroke);
Vector p = PointGetDrawNum();
if(free) {
Canvas::Stroke analyzeStroke = Style::Stroke(Style::ANALYZE);
analyzeStroke.width = 14.0;
analyzeStroke.layer = Canvas::Layer::FRONT;
Canvas::hStroke hcsAnalyze = canvas->GetStroke(analyzeStroke);
canvas->DrawPoint(PointGetNum(), hcsAnalyze);
canvas->DrawPoint(p, hcsAnalyze);
}
canvas->DrawPoint(PointGetNum(), hcsPoint);
canvas->DrawPoint(p, hcsPoint);
return;
}
@ -621,7 +667,7 @@ void Entity::Draw(DrawAs how, Canvas *canvas) {
tail = camera.projRight.ScaledBy(w/s).Plus(
camera.projUp. ScaledBy(h/s)).Minus(camera.offset);
} else {
tail = SK.GetEntity(point[0])->PointGetNum();
tail = SK.GetEntity(point[0])->PointGetDrawNum();
}
tail = camera.AlignToPixelGrid(tail);
@ -709,8 +755,32 @@ void Entity::Draw(DrawAs how, Canvas *canvas) {
case Type::TTF_TEXT: {
// Generate the rational polynomial curves, then piecewise linearize
// them, and display those.
if(!canvas->DrawBeziers(*GetOrGenerateBezierCurves(), hcs)) {
canvas->DrawEdges(*GetOrGenerateEdges(), hcs);
// Calculating the draw offset, if necessary.
const bool shouldExplode = ShouldDrawExploded();
Vector explodeOffset;
SBezierList offsetBeziers = {};
SBezierList *beziers = GetOrGenerateBezierCurves();
if(shouldExplode) {
explodeOffset = ExplodeOffset();
for(const SBezier& b : beziers->l) {
SBezier offset = b.TransformedBy(explodeOffset, Quaternion::IDENTITY, 1.0);
offsetBeziers.l.Add(&offset);
}
beziers = &offsetBeziers;
}
SEdgeList *edges = nullptr;
SEdgeList offsetEdges = {};
if(!canvas->DrawBeziers(*beziers, hcs)) {
edges = GetOrGenerateEdges();
if(shouldExplode) {
for(const SEdge &e : edges->l) {
offsetEdges.AddEdge(e.a.Plus(explodeOffset), e.b.Plus(explodeOffset), e.auxA, e.auxB, e.tag);
}
edges = &offsetEdges;
}
canvas->DrawEdges(*edges, hcs);
}
if(type == Type::CIRCLE) {
Entity *dist = SK.GetEntity(distance);
@ -720,12 +790,14 @@ void Entity::Draw(DrawAs how, Canvas *canvas) {
Canvas::Stroke analyzeStroke = Style::Stroke(Style::ANALYZE);
analyzeStroke.layer = Canvas::Layer::FRONT;
Canvas::hStroke hcsAnalyze = canvas->GetStroke(analyzeStroke);
if(!canvas->DrawBeziers(*GetOrGenerateBezierCurves(), hcsAnalyze)) {
canvas->DrawEdges(*GetOrGenerateEdges(), hcsAnalyze);
if(!canvas->DrawBeziers(*beziers, hcsAnalyze)) {
canvas->DrawEdges(*edges, hcsAnalyze);
}
}
}
}
offsetBeziers.Clear();
offsetEdges.Clear();
return;
}
case Type::IMAGE: {
@ -757,7 +829,7 @@ void Entity::Draw(DrawAs how, Canvas *canvas) {
Canvas::hFill hf = canvas->GetFill(fill);
Vector v[4] = {};
for(int i = 0; i < 4; i++) {
v[i] = SK.GetEntity(point[i])->PointGetNum();
v[i] = SK.GetEntity(point[i])->PointGetDrawNum();
}
Vector iu = v[3].Minus(v[0]);
Vector iv = v[1].Minus(v[0]);

View File

@ -26,6 +26,9 @@ bool EntityBase::HasVector() const {
}
ExprVector EntityBase::VectorGetExprsInWorkplane(hEntity wrkpl) const {
if(IsFace()) {
return FaceGetNormalExprs();
}
switch(type) {
case Type::LINE_SEGMENT:
return (SK.GetEntity(point[0])->PointGetExprsInWorkplane(wrkpl)).Minus(
@ -62,6 +65,9 @@ ExprVector EntityBase::VectorGetExprs() const {
}
Vector EntityBase::VectorGetNum() const {
if(IsFace()) {
return FaceGetNormalNum();
}
switch(type) {
case Type::LINE_SEGMENT:
return (SK.GetEntity(point[0])->PointGetNum()).Minus(
@ -79,6 +85,9 @@ Vector EntityBase::VectorGetNum() const {
}
Vector EntityBase::VectorGetRefPoint() const {
if(IsFace()) {
return FaceGetPointNum();
}
switch(type) {
case Type::LINE_SEGMENT:
return ((SK.GetEntity(point[0])->PointGetNum()).Plus(

View File

@ -227,7 +227,7 @@ void SolveSpaceUI::ExportViewOrWireframeTo(const Platform::Path &filename, bool
}
}
if(SS.GW.showConstraints) {
if(SS.GW.showConstraints != GraphicsWindow::ShowConstraintMode::SCM_NOSHOW ) {
if(!out->OutputConstraints(&SK.constraint)) {
GetEdgesCanvas canvas = {};
canvas.camera = SS.GW.GetCamera();

View File

@ -1308,9 +1308,9 @@ void GCodeFileWriter::FinishAndCloseFile() {
SS.MmToString(pt->p.x).c_str(), SS.MmToString(pt->p.y).c_str(),
SS.MmToString(SS.gCode.feed).c_str());
}
// Move up to a clearance plane 5mm above the work.
// Move up to a clearance plane above the work.
fprintf(f, "G00 Z%s\r\n",
SS.MmToString(SS.gCode.depth < 0 ? +5 : -5).c_str());
SS.MmToString(SS.gCode.safeHeight).c_str());
}
}

View File

@ -400,15 +400,24 @@ Expr *Expr::PartialWrt(hParam p) const {
ssassert(false, "Unexpected operation");
}
uint64_t Expr::ParamsUsed() const {
uint64_t r = 0;
if(op == Op::PARAM) r |= ((uint64_t)1 << (parh.v % 61));
if(op == Op::PARAM_PTR) r |= ((uint64_t)1 << (parp->h.v % 61));
void Expr::ParamsUsedList(std::vector<hParam> *list) const {
if(op == Op::PARAM || op == Op::PARAM_PTR) {
// leaf: just add ourselves if we aren't already there
hParam param = (op == Op::PARAM) ? parh : parp->h;
if(list->end() != std::find_if(list->begin(), list->end(),
[=](const hParam &p) { return p.v == param.v; })) {
// We found ourselves in the list already, early out.
return;
}
list->push_back(param);
return;
}
int c = Children();
if(c >= 1) r |= a->ParamsUsed();
if(c >= 2) r |= b->ParamsUsed();
return r;
if(c >= 1) {
a->ParamsUsedList(list);
if(c >= 2) b->ParamsUsedList(list);
}
}
bool Expr::DependsOn(hParam p) const {
@ -424,6 +433,11 @@ bool Expr::DependsOn(hParam p) const {
bool Expr::Tol(double a, double b) {
return fabs(a - b) < 0.001;
}
bool Expr::IsZeroConst() const {
return op == Op::CONSTANT && EXACT(v == 0.0);
}
Expr *Expr::FoldConstants() {
Expr *n = AllocExpr();
*n = *this;

View File

@ -70,9 +70,10 @@ public:
Expr *PartialWrt(hParam p) const;
double Eval() const;
uint64_t ParamsUsed() const;
void ParamsUsedList(std::vector<hParam> *list) const;
bool DependsOn(hParam p) const;
static bool Tol(double a, double b);
bool IsZeroConst() const;
Expr *FoldConstants();
void Substitute(hParam oldh, hParam newh);

View File

@ -711,6 +711,8 @@ bool SolveSpaceUI::LoadEntitiesFromFile(const Platform::Path &filename, EntityLi
{
if(strcmp(filename.Extension().c_str(), "emn")==0) {
return LinkIDF(filename, le, m, sh);
} else if(strcmp(filename.Extension().c_str(), "stl")==0) {
return LinkStl(filename, le, m, sh);
} else {
return LoadEntitiesFromSlvs(filename, le, m, sh);
}
@ -907,6 +909,8 @@ try_again:
return false;
}
}
if(g.IsTriangleMeshAssembly())
g.forceToMesh = true;
} else if(linkMap.count(g.linkFile) == 0) {
dbp("Missing file for group: %s", g.name.c_str());
// The file was moved; prompt the user for its new location.
@ -914,7 +918,7 @@ try_again:
switch(LocateImportedFile(linkFileRelative, canCancel)) {
case Platform::MessageDialog::Response::YES: {
Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window);
dialog->AddFilters(Platform::SolveSpaceModelFileFilters);
dialog->AddFilters(Platform::SolveSpaceLinkFileFilters);
dialog->ThawChoices(settings, "LinkSketch");
dialog->SuggestFilename(linkFileRelative);
if(dialog->RunModal()) {

View File

@ -224,9 +224,11 @@ void SolveSpaceUI::GenerateAll(Generate type, bool andFindFree, bool genForBBox)
if(PruneGroups(hg))
goto pruned;
int groupRequestIndex = 0;
for(auto &req : SK.request) {
Request *r = &req;
if(r->group != hg) continue;
r->groupRequestIndex = groupRequestIndex++;
r->Generate(&(SK.entity), &(SK.param));
}
@ -548,8 +550,11 @@ void SolveSpaceUI::SolveGroup(hGroup hg, bool andFindFree) {
}
SolveResult SolveSpaceUI::TestRankForGroup(hGroup hg, int *rank) {
WriteEqSystemForGroup(hg);
Group *g = SK.GetGroup(hg);
// If we don't calculate dof or redundant is allowed, there is
// no point to solve rank because this result is not meaningful
if(g->suppressDofCalculation || g->allowRedundant) return SolveResult::OKAY;
WriteEqSystemForGroup(hg);
SolveResult result = sys.SolveRank(g, rank);
FreeAllTemporary();
return result;

View File

@ -43,7 +43,7 @@ const MenuEntry Menu[] = {
{ 1, N_("&Open..."), Command::OPEN, C|'o', KN, mFile },
{ 1, N_("Open &Recent"), Command::OPEN_RECENT, 0, KN, mFile },
{ 1, N_("&Save"), Command::SAVE, C|'s', KN, mFile },
{ 1, N_("Save &As..."), Command::SAVE_AS, 0, KN, mFile },
{ 1, N_("Save &As..."), Command::SAVE_AS, C|S|'s', KN, mFile },
{ 1, NULL, Command::NONE, 0, KN, NULL },
{ 1, N_("Export &Image..."), Command::EXPORT_IMAGE, 0, KN, mFile },
{ 1, N_("Export 2d &View..."), Command::EXPORT_VIEW, 0, KN, mFile },
@ -94,12 +94,14 @@ const MenuEntry Menu[] = {
{ 1, N_("Show Snap &Grid"), Command::SHOW_GRID, '>', KC, mView },
{ 1, N_("Darken Inactive Solids"), Command::DIM_SOLID_MODEL, 0, KC, mView },
{ 1, N_("Use &Perspective Projection"), Command::PERSPECTIVE_PROJ, '`', KC, mView },
{ 1, N_("Show E&xploded View"), Command::EXPLODE_SKETCH, '\\', KC, mView },
{ 1, N_("Dimension &Units"), Command::NONE, 0, KN, NULL },
{ 2, N_("Dimensions in &Millimeters"), Command::UNITS_MM, 0, KR, mView },
{ 2, N_("Dimensions in M&eters"), Command::UNITS_METERS, 0, KR, mView },
{ 2, N_("Dimensions in &Inches"), Command::UNITS_INCHES, 0, KR, mView },
{ 2, N_("Dimensions in &Feet and Inches"), Command::UNITS_FEET_INCHES, 0, KR, mView },
{ 1, NULL, Command::NONE, 0, KN, NULL },
{ 1, N_("Show &Toolbar"), Command::SHOW_TOOLBAR, 0, KC, mView },
{ 1, N_("Show &Toolbar"), Command::SHOW_TOOLBAR, C|'\t', KC, mView },
{ 1, N_("Show Property Bro&wser"), Command::SHOW_TEXT_WND, '\t', KC, mView },
{ 1, NULL, Command::NONE, 0, KN, NULL },
{ 1, N_("&Full Screen"), Command::FULL_SCREEN, C|F|11, KC, mView },
@ -124,7 +126,7 @@ const MenuEntry Menu[] = {
{ 1, N_("Anywhere In &3d"), Command::FREE_IN_3D, '3', KR, mReq },
{ 1, NULL, Command::NONE, 0, KN, NULL },
{ 1, N_("Datum &Point"), Command::DATUM_POINT, 'p', KN, mReq },
{ 1, N_("&Workplane"), Command::WORKPLANE, 0, KN, mReq },
{ 1, N_("Wor&kplane"), Command::WORKPLANE, 0, KN, mReq },
{ 1, NULL, Command::NONE, 0, KN, NULL },
{ 1, N_("Line &Segment"), Command::LINE_SEGMENT, 's', KN, mReq },
{ 1, N_("C&onstruction Line Segment"), Command::CONSTR_SEGMENT, S|'s', KN, mReq },
@ -134,16 +136,16 @@ const MenuEntry Menu[] = {
{ 1, N_("&Bezier Cubic Spline"), Command::CUBIC, 'b', KN, mReq },
{ 1, NULL, Command::NONE, 0, KN, NULL },
{ 1, N_("&Text in TrueType Font"), Command::TTF_TEXT, 't', KN, mReq },
{ 1, N_("&Image"), Command::IMAGE, 0, KN, mReq },
{ 1, N_("I&mage"), Command::IMAGE, 0, KN, mReq },
{ 1, NULL, Command::NONE, 0, KN, NULL },
{ 1, N_("To&ggle Construction"), Command::CONSTRUCTION, 'g', KN, mReq },
{ 1, N_("Tangent &Arc at Point"), Command::TANGENT_ARC, S|'a', KN, mReq },
{ 1, N_("Ta&ngent Arc at Point"), Command::TANGENT_ARC, S|'a', KN, mReq },
{ 1, N_("Split Curves at &Intersection"), Command::SPLIT_CURVES, 'i', KN, mReq },
{ 0, N_("&Constrain"), Command::NONE, 0, KN, mCon },
{ 1, N_("&Distance / Diameter"), Command::DISTANCE_DIA, 'd', KN, mCon },
{ 1, N_("Re&ference Dimension"), Command::REF_DISTANCE, S|'d', KN, mCon },
{ 1, N_("A&ngle"), Command::ANGLE, 'n', KN, mCon },
{ 1, N_("A&ngle / Equal Angle"), Command::ANGLE, 'n', KN, mCon },
{ 1, N_("Reference An&gle"), Command::REF_ANGLE, S|'n', KN, mCon },
{ 1, N_("Other S&upplementary Angle"), Command::OTHER_ANGLE, 'u', KN, mCon },
{ 1, N_("Toggle R&eference Dim"), Command::REFERENCE, 'e', KN, mCon },
@ -152,9 +154,9 @@ const MenuEntry Menu[] = {
{ 1, N_("&Vertical"), Command::VERTICAL, 'v', KN, mCon },
{ 1, NULL, Command::NONE, 0, KN, NULL },
{ 1, N_("&On Point / Curve / Plane"), Command::ON_ENTITY, 'o', KN, mCon },
{ 1, N_("E&qual Length / Radius / Angle"), Command::EQUAL, 'q', KN, mCon },
{ 1, N_("Length Ra&tio"), Command::RATIO, 'z', KN, mCon },
{ 1, N_("Length Diff&erence"), Command::DIFFERENCE, 'j', KN, mCon },
{ 1, N_("E&qual Length / Radius"), Command::EQUAL, 'q', KN, mCon },
{ 1, N_("Length / Arc Ra&tio"), Command::RATIO, 'z', KN, mCon },
{ 1, N_("Length / Arc Diff&erence"), Command::DIFFERENCE, 'j', KN, mCon },
{ 1, N_("At &Midpoint"), Command::AT_MIDPOINT, 'm', KN, mCon },
{ 1, N_("S&ymmetric"), Command::SYMMETRIC, 'y', KN, mCon },
{ 1, N_("Para&llel / Tangent"), Command::PARALLEL, 'l', KN, mCon },
@ -181,6 +183,7 @@ const MenuEntry Menu[] = {
{ 0, N_("&Help"), Command::NONE, 0, KN, mHelp },
{ 1, N_("&Language"), Command::LOCALE, 0, KN, mHelp },
{ 1, N_("&Website / Manual"), Command::WEBSITE, 0, KN, mHelp },
{ 1, N_("&Go to GitHub commit"), Command::GITHUB, 0, KN, mHelp },
#ifndef __APPLE__
{ 1, N_("&About"), Command::ABOUT, 0, KN, mHelp },
#endif
@ -297,7 +300,7 @@ void GraphicsWindow::PopulateMainMenu() {
SS.UpdateWindowTitles();
PopulateMainMenu();
EnsureValidActives();
SS.GW.EnsureValidActives();
});
}
} else if(Menu[i].fn == NULL) {
@ -317,6 +320,8 @@ void GraphicsWindow::PopulateMainMenu() {
dimSolidModelMenuItem = menuItem;
} else if(Menu[i].cmd == Command::PERSPECTIVE_PROJ) {
perspectiveProjMenuItem = menuItem;
} else if(Menu[i].cmd == Command::EXPLODE_SKETCH) {
explodeMenuItem = menuItem;
} else if(Menu[i].cmd == Command::SHOW_TOOLBAR) {
showToolbarMenuItem = menuItem;
} else if(Menu[i].cmd == Command::SHOW_TEXT_WND) {
@ -329,6 +334,8 @@ void GraphicsWindow::PopulateMainMenu() {
unitsMetersMenuItem = menuItem;
} else if(Menu[i].cmd == Command::UNITS_INCHES) {
unitsInchesMenuItem = menuItem;
} else if(Menu[i].cmd == Command::UNITS_FEET_INCHES) {
unitsFeetInchesMenuItem = menuItem;
} else if(Menu[i].cmd == Command::SEL_WORKPLANE) {
inWorkplaneMenuItem = menuItem;
} else if(Menu[i].cmd == Command::FREE_IN_3D) {
@ -370,7 +377,7 @@ static void PopulateMenuWithPathnames(Platform::MenuRef menu,
void GraphicsWindow::PopulateRecentFiles() {
PopulateMenuWithPathnames(openRecentMenu, SS.recentFiles, [](const Platform::Path &path) {
// OkayToStartNewFile could mutate recentFiles, which will invalidate path (which is a
// refererence into the recentFiles vector), so take a copy of it here.
// reference into the recentFiles vector), so take a copy of it here.
Platform::Path pathCopy(path);
if(!SS.OkayToStartNewFile()) return;
SS.Load(pathCopy);
@ -402,11 +409,13 @@ void GraphicsWindow::Init() {
showNormals = true;
showPoints = true;
showConstruction = true;
showConstraints = true;
showConstraints = ShowConstraintMode::SCM_SHOW_ALL;
showShaded = true;
showEdges = true;
showMesh = false;
showOutlines = false;
showFacesDrawing = false;
showFacesNonDrawing = true;
drawOccludedAs = DrawOccludedAs::INVISIBLE;
showTextWindow = true;
@ -422,8 +431,13 @@ void GraphicsWindow::Init() {
using namespace std::placeholders;
// Do this first, so that if it causes an onRender event we don't try to paint without
// a canvas.
window->SetMinContentSize(720, 670);
window->SetMinContentSize(720, /*ToolbarDrawOrHitTest 636*/ 32 * 18 + 3 * 16 + 8 + 4);
window->onClose = std::bind(&SolveSpaceUI::MenuFile, Command::EXIT);
window->onContextLost = [&] {
canvas = NULL;
persistentCanvas = NULL;
persistentDirty = true;
};
window->onRender = std::bind(&GraphicsWindow::Paint, this);
window->onKeyboardEvent = std::bind(&GraphicsWindow::KeyboardEvent, this, _1);
window->onMouseEvent = std::bind(&GraphicsWindow::MouseEvent, this, _1);
@ -490,11 +504,11 @@ void GraphicsWindow::AnimateOnto(Quaternion quatf, Vector offsetf) {
// Animate transition, unless it's a tiny move.
int64_t t0 = GetMilliseconds();
int32_t dt = (mp < 0.01 && mo < 10) ? (-20) :
(int32_t)(100 + 1000*mp + 0.4*mo);
// Don't ever animate for longer than 2000 ms; we can get absurdly
(int32_t)(100 + 600*mp + 0.4*mo);
// Don't ever animate for longer than 800 ms; we can get absurdly
// long translations (as measured in pixels) if the user zooms out, moves,
// and then zooms in again.
if(dt > 2000) dt = 2000;
if(dt > 800) dt = 800;
Quaternion dq = quatf.Times(quat0.Inverse());
if(!animateTimer) {
@ -703,16 +717,47 @@ double GraphicsWindow::ZoomToFit(const Camera &camera,
return scale;
}
void GraphicsWindow::ZoomToMouse(double zoomMultiplyer) {
double offsetRight = offset.Dot(projRight);
double offsetUp = offset.Dot(projUp);
double width, height;
window->GetContentSize(&width, &height);
double righti = currentMousePosition.x / scale - offsetRight;
double upi = currentMousePosition.y / scale - offsetUp;
// zoomMultiplyer of 1 gives a default zoom factor of 1.2x: zoomMultiplyer * 1.2
// zoom = adjusted zoom negative zoomMultiplyer will zoom out, positive will zoom in
//
scale *= exp(0.1823216 * zoomMultiplyer); // ln(1.2) = 0.1823216
double rightf = currentMousePosition.x / scale - offsetRight;
double upf = currentMousePosition.y / scale - offsetUp;
offset = offset.Plus(projRight.ScaledBy(rightf - righti));
offset = offset.Plus(projUp.ScaledBy(upf - upi));
if(SS.TW.shown.screen == TextWindow::Screen::EDIT_VIEW) {
if(havePainted) {
SS.ScheduleShowTW();
}
}
havePainted = false;
Invalidate();
}
void GraphicsWindow::MenuView(Command id) {
switch(id) {
case Command::ZOOM_IN:
SS.GW.scale *= 1.2;
SS.ScheduleShowTW();
SS.GW.ZoomToMouse(1);
break;
case Command::ZOOM_OUT:
SS.GW.scale /= 1.2;
SS.ScheduleShowTW();
SS.GW.ZoomToMouse(-1);
break;
case Command::ZOOM_TO_FIT:
@ -748,6 +793,12 @@ void GraphicsWindow::MenuView(Command id) {
}
break;
case Command::EXPLODE_SKETCH:
SS.explode = !SS.explode;
SS.GW.EnsureValidActives();
SS.MarkGroupDirty(SS.GW.activeGroup, true);
break;
case Command::ONTO_WORKPLANE:
if(SS.GW.LockedInWorkplane()) {
SS.GW.AnimateOntoWorkplane();
@ -766,13 +817,18 @@ void GraphicsWindow::MenuView(Command id) {
Quaternion quatf = quat0;
double dmin = 1e10;
// There are 24 possible views; 3*2*2*2
int i, j, negi, negj;
for(i = 0; i < 3; i++) {
for(j = 0; j < 3; j++) {
// There are 24 possible views (3*2*2*2), if all are
// allowed. If the user is in turn-table mode, the
// isometric view must have the z-axis facing up, leaving
// 8 possible views (2*1*2*2).
bool require_turntable = (id==Command::NEAREST_ISO && SS.turntableNav);
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 3; j++) {
if(i == j) continue;
for(negi = 0; negi < 2; negi++) {
for(negj = 0; negj < 2; negj++) {
if(require_turntable && (j!=2)) continue;
for(int negi = 0; negi < 2; negi++) {
for(int negj = 0; negj < 2; negj++) {
Vector ou = ortho[i], ov = ortho[j];
if(negi) ou = ou.ScaledBy(-1);
if(negj) ov = ov.ScaledBy(-1);
@ -841,6 +897,12 @@ void GraphicsWindow::MenuView(Command id) {
SS.GW.EnsureValidActives();
break;
case Command::UNITS_FEET_INCHES:
SS.viewUnits = Unit::FEET_INCHES;
SS.ScheduleShowTW();
SS.GW.EnsureValidActives();
break;
case Command::UNITS_MM:
SS.viewUnits = Unit::MM;
SS.ScheduleShowTW();
@ -923,6 +985,7 @@ void GraphicsWindow::EnsureValidActives() {
case Unit::MM:
case Unit::METERS:
case Unit::INCHES:
case Unit::FEET_INCHES:
break;
default:
SS.viewUnits = Unit::MM;
@ -931,6 +994,7 @@ void GraphicsWindow::EnsureValidActives() {
unitsMmMenuItem->SetActive(SS.viewUnits == Unit::MM);
unitsMetersMenuItem->SetActive(SS.viewUnits == Unit::METERS);
unitsInchesMenuItem->SetActive(SS.viewUnits == Unit::INCHES);
unitsFeetInchesMenuItem->SetActive(SS.viewUnits == Unit::FEET_INCHES);
if(SS.TW.window) SS.TW.window->SetVisible(SS.GW.showTextWindow);
showTextWndMenuItem->SetActive(SS.GW.showTextWindow);
@ -938,6 +1002,7 @@ void GraphicsWindow::EnsureValidActives() {
showGridMenuItem->SetActive(SS.GW.showSnapGrid);
dimSolidModelMenuItem->SetActive(SS.GW.dimSolidModel);
perspectiveProjMenuItem->SetActive(SS.usePerspectiveProj);
explodeMenuItem->SetActive(SS.explode);
showToolbarMenuItem->SetActive(SS.showToolbar);
fullScreenMenuItem->SetActive(SS.GW.window->IsFullScreen());
@ -1037,6 +1102,16 @@ void GraphicsWindow::MenuEdit(Command id) {
}
}
}
// some pending operations need an Undo to properly clean up on ESC
if ( (SS.GW.pending.operation == Pending::DRAGGING_NEW_POINT)
|| (SS.GW.pending.operation == Pending::DRAGGING_NEW_LINE_POINT)
|| (SS.GW.pending.operation == Pending::DRAGGING_NEW_ARC_POINT)
|| (SS.GW.pending.operation == Pending::DRAGGING_NEW_CUBIC_POINT)
|| (SS.GW.pending.operation == Pending::DRAGGING_NEW_RADIUS) )
{
SS.GW.ClearSuper();
SS.UndoUndo();
}
SS.GW.ClearSuper();
SS.TW.HideEditControl();
SS.nakedEdges.Clear();
@ -1366,6 +1441,14 @@ void GraphicsWindow::ToggleBool(bool *v) {
SS.GenerateAll(SolveSpaceUI::Generate::UNTIL_ACTIVE);
}
if(v == &showFaces) {
if(g->type == Group::Type::DRAWING_WORKPLANE || g->type == Group::Type::DRAWING_3D) {
showFacesDrawing = showFaces;
} else {
showFacesNonDrawing = showFaces;
}
}
Invalidate(/*clearPersistent=*/true);
SS.ScheduleShowTW();
}

View File

@ -136,14 +136,30 @@ void Group::MenuGroup(Command id, Platform::Path linkFile) {
g.predef.negateV = wrkplg->predef.negateV;
} else if(wrkplg->subtype == Subtype::WORKPLANE_BY_POINT_ORTHO) {
g.predef.q = wrkplg->predef.q;
} else if(wrkplg->subtype == Subtype::WORKPLANE_BY_POINT_NORMAL) {
g.predef.q = wrkplg->predef.q;
g.predef.entityB = wrkplg->predef.entityB;
} else ssassert(false, "Unexpected workplane subtype");
}
} else if(gs.anyNormals == 1 && gs.points == 1 && gs.n == 2) {
g.subtype = Subtype::WORKPLANE_BY_POINT_NORMAL;
g.predef.entityB = gs.anyNormal[0];
g.predef.q = SK.GetEntity(gs.anyNormal[0])->NormalGetNum();
g.predef.origin = gs.point[0];
//} else if(gs.faces == 1 && gs.points == 1 && gs.n == 2) {
// g.subtype = Subtype::WORKPLANE_BY_POINT_FACE;
// g.predef.q = SK.GetEntity(gs.face[0])->NormalGetNum();
// g.predef.origin = gs.point[0];
} else {
Error(_("Bad selection for new sketch in workplane. This "
"group can be created with:\n\n"
" * a point (through the point, orthogonal to coordinate axes)\n"
" * a point and two line segments (through the point, "
"parallel to the lines)\n"
"parallel to the lines)\n"
" * a point and a normal (through the point, "
"orthogonal to the normal)\n"
/*" * a point and a face (through the point, "
"parallel to the face)\n"*/
" * a workplane (copy of the workplane)\n"));
return;
}
@ -392,7 +408,13 @@ bool Group::IsForcedToMeshBySource() const {
}
bool Group::IsForcedToMesh() const {
return forceToMesh || IsForcedToMeshBySource();
return forceToMesh || IsTriangleMeshAssembly() || IsForcedToMeshBySource();
}
bool Group::IsTriangleMeshAssembly() const {
if (type != Type::LINKED) return false;
if (!impMesh.IsEmpty() && impShell.IsEmpty()) return true;
return false;
}
std::string Group::DescriptionString() {
@ -404,11 +426,10 @@ std::string Group::DescriptionString() {
}
void Group::Activate() {
if(type == Type::EXTRUDE || type == Type::LINKED || type == Type::LATHE ||
type == Type::REVOLVE || type == Type::HELIX || type == Type::TRANSLATE || type == Type::ROTATE) {
SS.GW.showFaces = true;
if(type == Type::DRAWING_WORKPLANE || type == Type::DRAWING_3D) {
SS.GW.showFaces = SS.GW.showFacesDrawing;
} else {
SS.GW.showFaces = false;
SS.GW.showFaces = SS.GW.showFacesNonDrawing;
}
SS.MarkGroupDirty(h); // for good measure; shouldn't be needed
SS.ScheduleShowTW();
@ -443,11 +464,14 @@ void Group::Generate(IdList<Entity,hEntity> *entity,
} else if(subtype == Subtype::WORKPLANE_BY_POINT_ORTHO) {
// Already given, numerically.
q = predef.q;
} else if(subtype == Subtype::WORKPLANE_BY_POINT_NORMAL) {
q = SK.GetEntity(predef.entityB)->NormalGetNum();
} else ssassert(false, "Unexpected workplane subtype");
Entity normal = {};
normal.type = Entity::Type::NORMAL_N_COPY;
normal.numNormal = q;
normal.point[0] = h.entity(2);
normal.group = h;
normal.h = h.entity(1);
@ -800,6 +824,12 @@ void Group::GenerateEquations(IdList<Equation,hEquation> *l) {
AddEq(l, (EC(axis.z))->Minus(EP(6)), 5);
#undef EC
#undef EP
if(type == Type::HELIX) {
if(valB != 0.0) {
AddEq(l, Expr::From(h.param(7))->Times(Expr::From(PI))->
Minus(Expr::From(h.param(3))->Times(Expr::From(valB))), 6);
}
}
} else if(type == Type::EXTRUDE) {
if(predef.entityB != Entity::FREE_IN_3D) {
// The extrusion path is locked along a line, normal to the
@ -1150,6 +1180,11 @@ void Group::CopyEntity(IdList<Entity,hEntity> *el,
break;
default: {
if((Entity::Type::IMAGE == ep->type) && (true == ep->construction)) {
// Do not copy image entities if they are construction.
return;
}
int i, points;
bool hasNormal, hasDistance;
EntReqTable::GetEntityInfo(ep->type, ep->extraPoints,
@ -1172,3 +1207,6 @@ void Group::CopyEntity(IdList<Entity,hEntity> *el,
el->Add(&en);
}
bool Group::ShouldDrawExploded() const {
return SS.explode && h == SS.GW.activeGroup && type == Type::DRAWING_WORKPLANE && !SS.exportMode;
}

View File

@ -635,8 +635,11 @@ void Group::DrawMesh(DrawMeshAs how, Canvas *canvas) {
std::vector<uint32_t> faces;
SS.GW.GroupSelection();
auto const &gs = SS.GW.gs;
if(gs.faces > 0) faces.push_back(gs.face[0].v);
if(gs.faces > 1) faces.push_back(gs.face[1].v);
// See also GraphicsWindow::MakeSelected "if(c >= MAX_SELECTABLE_FACES)"
// and GraphicsWindow::GroupSelection "if(e->IsFace())"
for(auto &fc : gs.face) {
faces.push_back(fc.v);
}
canvas->DrawFaces(displayMesh, faces, hcf);
break;
}

View File

@ -53,8 +53,22 @@ static std::vector <std::string> splitString(const std::string line) {
return v;
}
static bool isHoleDuplicate(EntityList *el, double x, double y, double r) {
bool duplicate = false;
for(int i = 0; i < el->n && !duplicate; i++) {
Entity &en = el->Get(i);
if(en.type != Entity::Type::CIRCLE)
continue;
Entity *distance = el->FindById(en.distance);
Entity *center = el->FindById(en.point[0]);
duplicate =
center->actPoint.x == x && center->actPoint.y == y && distance->actDistance == r;
}
return duplicate;
}
//////////////////////////////////////////////////////////////////////////////
// Functions for linking an IDF file - we need to create entites that
// Functions for linking an IDF file - we need to create entities that
// get remapped into a linked group similar to linking .slvs files
//////////////////////////////////////////////////////////////////////////////
@ -291,9 +305,9 @@ static void MakeBeziersForArcs(SBezierList *sbl, Vector center, Vector pa, Vecto
namespace SolveSpace {
// Here we read the important section of an IDF file. SolveSpace Entities are directly created by
// the funcions above, which is only OK because of the way linking works. For example points do
// the functions above, which is only OK because of the way linking works. For example points do
// not have handles for solver parameters (coordinates), they only have their actPoint values
// set (or actNormal or actDistance). These are incompete entites and would be a problem if
// set (or actNormal or actDistance). These are incomplete entities and would be a problem if
// they were part of the sketch, but they are not. After making a list of them here, a new group
// gets created from copies of these. Those copies are complete and part of the sketch group.
bool LinkIDF(const Platform::Path &filename, EntityList *el, SMesh *m, SShell *sh) {
@ -332,7 +346,8 @@ bool LinkIDF(const Platform::Path &filename, EntityList *el, SMesh *m, SShell *s
double board_thickness = 10.0;
double scale = 1.0; //mm
bool topEntities, bottomEntities;
bool topEntities = false;
bool bottomEntities = false;
Quaternion normal = Quaternion::From(Vector::From(1,0,0), Vector::From(0,1,0));
hEntity hnorm = newNormal(el, &entityCount, normal);
@ -461,9 +476,10 @@ bool LinkIDF(const Platform::Path &filename, EntityList *el, SMesh *m, SShell *s
double d = stof(values[0]);
double x = stof(values[1]);
double y = stof(values[2]);
bool duplicate = isHoleDuplicate(el, x, y, d / 2);
// Only show holes likely to be useful in MCAD to reduce complexity.
if((d > 1.7) || (values[5].compare(0,3,"PIN") == 0)
|| (values[5].compare(0,3,"MTG") == 0)) {
if(((d > 1.7) || (values[5].compare(0,3,"PIN") == 0)
|| (values[5].compare(0,3,"MTG") == 0)) && !duplicate) {
// create the entity
Vector cent = Vector::From(x,y,0.0);
hEntity hcent = newPoint(el, &entityCount, cent);

256
src/importmesh.cpp Normal file
View File

@ -0,0 +1,256 @@
//-----------------------------------------------------------------------------
// Triangle mesh file reader. Reads an STL file triangle mesh and creates
// a SovleSpace SMesh from it. Supports only Linking, not import.
//
// Copyright 2020 Paul Kahler.
//-----------------------------------------------------------------------------
#include "solvespace.h"
#include "sketch.h"
#include <vector>
#define MIN_POINT_DISTANCE 0.001
// we will check for duplicate vertices and keep all their normals
class vertex {
public:
Vector p;
std::vector<Vector> normal;
};
static bool isEdgeVertex(vertex &v) {
unsigned int i,j;
bool result = false;
for(i=0;i<v.normal.size();i++) {
for(j=i;j<v.normal.size();j++) {
if(v.normal[i].Dot(v.normal[j]) < 0.9) {
result = true;
}
}
}
return result;
}
// This function has poor performance, used inside a loop it is O(n**2)
static void addUnique(std::vector<vertex> &lv, Vector &p, Vector &n) {
unsigned int i;
for(i=0; i<lv.size(); i++) {
if(lv[i].p.Equals(p, MIN_POINT_DISTANCE)) {
break;
}
}
if(i==lv.size()) {
vertex v;
v.p = p;
lv.push_back(v);
}
// we could improve a little by only storing unique normals
lv[i].normal.push_back(n);
};
// Make a new point - type doesn't matter since we will make a copy later
static hEntity newPoint(EntityList *el, int *id, Vector p) {
Entity en = {};
en.type = Entity::Type::POINT_N_COPY;
en.extraPoints = 0;
en.timesApplied = 0;
en.group.v = 462;
en.actPoint = p;
en.construction = false;
en.style.v = Style::DATUM;
en.actVisible = true;
en.forceHidden = false;
en.h.v = *id + en.group.v*65536;
*id = *id+1;
el->Add(&en);
return en.h;
}
// check if a vertex is unique and add it via newPoint if it is.
static void addVertex(EntityList *el, Vector v) {
if(el->n < 15000) {
int id = el->n;
newPoint(el, &id, v);
}
}
static hEntity newNormal(EntityList *el, int *id, Quaternion normal, hEntity p) {
// normals have parameters, but we don't need them to make a NORMAL_N_COPY from this
Entity en = {};
en.type = Entity::Type::NORMAL_N_COPY;
en.extraPoints = 0;
en.timesApplied = 0;
en.group.v = 472;
en.actNormal = normal;
en.construction = false;
en.style.v = Style::NORMALS;
// to be visible we need to add a point.
// en.point[0] = newPoint(el, id, Vector::From(0,0,0));
en.point[0] = p;
en.actVisible = true;
en.forceHidden = false;
*id = *id+1;
en.h.v = *id + en.group.v*65536;
el->Add(&en);
return en.h;
}
static hEntity newLine(EntityList *el, int *id, hEntity p0, hEntity p1) {
Entity en = {};
en.type = Entity::Type::LINE_SEGMENT;
en.point[0] = p0;
en.point[1] = p1;
en.extraPoints = 0;
en.timesApplied = 0;
en.group.v = 493;
en.construction = true;
en.style.v = Style::CONSTRUCTION;
en.actVisible = true;
en.forceHidden = false;
en.h.v = *id + en.group.v*65536;
*id = *id + 1;
el->Add(&en);
return en.h;
}
namespace SolveSpace {
bool LinkStl(const Platform::Path &filename, EntityList *el, SMesh *m, SShell *sh) {
dbp("\nLink STL triangle mesh.");
el->Clear();
std::string data;
if(!ReadFile(filename, &data)) {
Error("Couldn't read from '%s'", filename.raw.c_str());
return false;
}
std::stringstream f(data);
char str[80] = {};
f.read(str, 80);
if(0==memcmp("solid", str, 5)) {
// just returning false will trigger the warning that linked file is not present
// best solution is to add an importer for text STL.
Message(_("Text-formated STL files are not currently supported"));
return false;
}
uint32_t n;
uint32_t color;
f.read((char*)&n, 4);
dbp("%d triangles", n);
float x,y,z;
float xn,yn,zn;
std::vector<vertex> verts = {};
for(uint32_t i = 0; i<n; i++) {
STriangle tr = {};
// read the triangle normal
f.read((char*)&xn, 4);
f.read((char*)&yn, 4);
f.read((char*)&zn, 4);
tr.an = Vector::From(xn,yn,zn);
tr.bn = tr.an;
tr.cn = tr.an;
f.read((char*)&x, 4);
f.read((char*)&y, 4);
f.read((char*)&z, 4);
tr.a.x = x;
tr.a.y = y;
tr.a.z = z;
f.read((char*)&x, 4);
f.read((char*)&y, 4);
f.read((char*)&z, 4);
tr.b.x = x;
tr.b.y = y;
tr.b.z = z;
f.read((char*)&x, 4);
f.read((char*)&y, 4);
f.read((char*)&z, 4);
tr.c.x = x;
tr.c.y = y;
tr.c.z = z;
f.read((char*)&color,2);
if(color & 0x8000) {
tr.meta.color.red = (color >> 7) & 0xf8;
tr.meta.color.green = (color >> 2) & 0xf8;
tr.meta.color.blue = (color << 3);
tr.meta.color.alpha = 255;
} else {
tr.meta.color.red = 90;
tr.meta.color.green = 120;
tr.meta.color.blue = 140;
tr.meta.color.alpha = 255;
}
m->AddTriangle(&tr);
Vector normal = tr.Normal().WithMagnitude(1.0);
addUnique(verts, tr.a, normal);
addUnique(verts, tr.b, normal);
addUnique(verts, tr.c, normal);
}
dbp("%d vertices", verts.size());
int id = 1;
//add the STL origin and normals
hEntity origin = newPoint(el, &id, Vector::From(0.0, 0.0, 0.0));
newNormal(el, &id, Quaternion::From(Vector::From(1,0,0),Vector::From(0,1,0)), origin);
newNormal(el, &id, Quaternion::From(Vector::From(0,1,0),Vector::From(0,0,1)), origin);
newNormal(el, &id, Quaternion::From(Vector::From(0,0,1),Vector::From(1,0,0)), origin);
BBox box = {};
box.minp = verts[0].p;
box.maxp = verts[0].p;
// determine the bounding box for all vertexes
for(unsigned int i=1; i<verts.size(); i++) {
box.Include(verts[i].p);
}
hEntity p[8];
p[0] = newPoint(el, &id, Vector::From(box.minp.x, box.minp.y, box.minp.z));
p[1] = newPoint(el, &id, Vector::From(box.maxp.x, box.minp.y, box.minp.z));
p[2] = newPoint(el, &id, Vector::From(box.minp.x, box.maxp.y, box.minp.z));
p[3] = newPoint(el, &id, Vector::From(box.maxp.x, box.maxp.y, box.minp.z));
p[4] = newPoint(el, &id, Vector::From(box.minp.x, box.minp.y, box.maxp.z));
p[5] = newPoint(el, &id, Vector::From(box.maxp.x, box.minp.y, box.maxp.z));
p[6] = newPoint(el, &id, Vector::From(box.minp.x, box.maxp.y, box.maxp.z));
p[7] = newPoint(el, &id, Vector::From(box.maxp.x, box.maxp.y, box.maxp.z));
newLine(el, &id, p[0], p[1]);
newLine(el, &id, p[0], p[2]);
newLine(el, &id, p[3], p[1]);
newLine(el, &id, p[3], p[2]);
newLine(el, &id, p[4], p[5]);
newLine(el, &id, p[4], p[6]);
newLine(el, &id, p[7], p[5]);
newLine(el, &id, p[7], p[6]);
newLine(el, &id, p[0], p[4]);
newLine(el, &id, p[1], p[5]);
newLine(el, &id, p[2], p[6]);
newLine(el, &id, p[3], p[7]);
for(unsigned int i=0; i<verts.size(); i++) {
// create point entities for edge vertexes
if(isEdgeVertex(verts[i])) {
addVertex(el, verts[i].p);
}
}
return true;
}
}

View File

@ -131,11 +131,15 @@ case SLVS_C_PT_ON_LINE: t = Constraint::Type::PT_ON_LINE; break;
case SLVS_C_PT_ON_FACE: t = Constraint::Type::PT_ON_FACE; break;
case SLVS_C_EQUAL_LENGTH_LINES: t = Constraint::Type::EQUAL_LENGTH_LINES; break;
case SLVS_C_LENGTH_RATIO: t = Constraint::Type::LENGTH_RATIO; break;
case SLVS_C_ARC_ARC_LEN_RATIO: t = Constraint::Type::ARC_ARC_LEN_RATIO; break;
case SLVS_C_ARC_LINE_LEN_RATIO: t = Constraint::Type::ARC_LINE_LEN_RATIO; break;
case SLVS_C_EQ_LEN_PT_LINE_D: t = Constraint::Type::EQ_LEN_PT_LINE_D; break;
case SLVS_C_EQ_PT_LN_DISTANCES: t = Constraint::Type::EQ_PT_LN_DISTANCES; break;
case SLVS_C_EQUAL_ANGLE: t = Constraint::Type::EQUAL_ANGLE; break;
case SLVS_C_EQUAL_LINE_ARC_LEN: t = Constraint::Type::EQUAL_LINE_ARC_LEN; break;
case SLVS_C_LENGTH_DIFFERENCE: t = Constraint::Type::LENGTH_DIFFERENCE; break;
case SLVS_C_ARC_ARC_DIFFERENCE: t = Constraint::Type::ARC_ARC_DIFFERENCE; break;
case SLVS_C_ARC_LINE_DIFFERENCE:t = Constraint::Type::ARC_LINE_DIFFERENCE; break;
case SLVS_C_SYMMETRIC: t = Constraint::Type::SYMMETRIC; break;
case SLVS_C_SYMMETRIC_HORIZ: t = Constraint::Type::SYMMETRIC_HORIZ; break;
case SLVS_C_SYMMETRIC_VERT: t = Constraint::Type::SYMMETRIC_VERT; break;

View File

@ -16,7 +16,7 @@ void SMesh::AddTriangle(STriMeta meta, Vector n, Vector a, Vector b, Vector c) {
Vector ab = b.Minus(a), bc = c.Minus(b);
Vector np = ab.Cross(bc);
if(np.Magnitude() < 1e-10) {
// ugh; gl sometimes tesselates to collinear triangles
// ugh; gl sometimes tessellates to collinear triangles
return;
}
if(np.Dot(n) > 0) {

View File

@ -103,7 +103,10 @@ void GraphicsWindow::MouseMoved(double x, double y, bool leftDown,
shiftDown = !shiftDown;
}
if(SS.showToolbar) {
// Not passing right-button and middle-button drags to the toolbar avoids
// some cosmetic issues with trackpad pans/rotates implemented with
// simulated right-button drag events causing spurious hover events.
if(SS.showToolbar && !middleDown) {
if(ToolbarMouseMoved((int)x, (int)y)) {
hover.Clear();
return;
@ -136,8 +139,9 @@ void GraphicsWindow::MouseMoved(double x, double y, bool leftDown,
double dy = (y - orig.mouse.y) / scale;
if(!(shiftDown || ctrlDown)) {
double s = 0.3*(PI/180)*scale; // degrees per pixel
if(SS.turntableNav) { // lock the Z to vertical
double sign = SS.cameraNav ? -1.0 : 1.0;
double s = 0.3*(PI/180)*scale*sign; // degrees per pixel
if(SS.turntableNav) { // lock the Z to vertical
projRight = orig.projRight.RotatedAbout(Vector::From(0, 0, 1), -s * dx);
projUp = orig.projUp.RotatedAbout(
Vector::From(orig.projRight.x, orig.projRight.y, orig.projRight.y), s * dy);
@ -821,7 +825,7 @@ Vector GraphicsWindow::SnapToEntityByScreenPoint(Point2d pp, hEntity he) {
SEdgeList *edges = e->GetOrGenerateEdges();
double minD = -1.0f;
double k;
double k = 0.0;
const SEdge *edge = NULL;
for(const auto &e : edges->l) {
Point2d p0 = ProjectPoint(e.a);
@ -911,7 +915,7 @@ bool GraphicsWindow::MouseEvent(Platform::MouseEvent event) {
break;
case MouseEvent::Type::SCROLL_VERT:
this->MouseScroll(event.x, event.y, event.shiftDown ? event.scrollDelta / 10 : event.scrollDelta);
this->MouseScroll(event.shiftDown ? event.scrollDelta / 10 : event.scrollDelta);
break;
case MouseEvent::Type::LEAVE:
@ -1134,6 +1138,7 @@ void GraphicsWindow::MouseLeftDown(double mx, double my, bool shiftDown, bool ct
AddToPending(hr);
Request *r = SK.GetRequest(hr);
r->file = pending.filename;
r->construction = true;
for(int i = 1; i <= 4; i++) {
SK.GetEntity(hr.entity(i))->PointForceTo(v);
@ -1373,12 +1378,12 @@ void GraphicsWindow::EditConstraint(hConstraint constraint) {
value /= 2;
// Try showing value with default number of digits after decimal first.
if(c->type == Constraint::Type::LENGTH_RATIO) {
if(c->type == Constraint::Type::LENGTH_RATIO || c->type == Constraint::Type::ARC_ARC_LEN_RATIO || c->type == Constraint::Type::ARC_LINE_LEN_RATIO) {
editValue = ssprintf("%.3f", value);
} else if(c->type == Constraint::Type::ANGLE) {
editValue = SS.DegreeToString(value);
} else {
editValue = SS.MmToString(value);
editValue = SS.MmToString(value, true);
value /= SS.MmPerUnit();
}
// If that's not enough to represent it exactly, show the value with as many
@ -1434,7 +1439,9 @@ void GraphicsWindow::EditControlDone(const std::string &s) {
case Constraint::Type::PT_LINE_DISTANCE:
case Constraint::Type::PT_FACE_DISTANCE:
case Constraint::Type::PT_PLANE_DISTANCE:
case Constraint::Type::LENGTH_DIFFERENCE: {
case Constraint::Type::LENGTH_DIFFERENCE:
case Constraint::Type::ARC_ARC_DIFFERENCE:
case Constraint::Type::ARC_LINE_DIFFERENCE: {
// The sign is not displayed to the user, but this is a signed
// distance internally. To flip the sign, the user enters a
// negative distance.
@ -1448,6 +1455,8 @@ void GraphicsWindow::EditControlDone(const std::string &s) {
}
case Constraint::Type::ANGLE:
case Constraint::Type::LENGTH_RATIO:
case Constraint::Type::ARC_ARC_LEN_RATIO:
case Constraint::Type::ARC_LINE_LEN_RATIO:
// These don't get the units conversion for distance, and
// they're always positive
c->valA = fabs(e->Eval());
@ -1471,17 +1480,10 @@ void GraphicsWindow::EditControlDone(const std::string &s) {
}
}
void GraphicsWindow::MouseScroll(double x, double y, double delta) {
double offsetRight = offset.Dot(projRight);
double offsetUp = offset.Dot(projUp);
double righti = x/scale - offsetRight;
double upi = y/scale - offsetUp;
// The default zoom factor is 1.2x for one scroll wheel click (delta==1).
void GraphicsWindow::MouseScroll(double zoomMultiplyer) {
// To support smooth scrolling where scroll wheel events come in increments
// smaller (or larger) than 1 we do:
// scale *= exp(ln(1.2) * delta);
// scale *= exp(ln(1.2) * zoomMultiplyer);
// to ensure that the same total scroll delta always results in the same
// total zoom irrespective of in how many increments the zoom was applied.
// For example if we scroll a total delta of a+b in two events vs. one then
@ -1489,21 +1491,7 @@ void GraphicsWindow::MouseScroll(double x, double y, double delta) {
// while
// scale * a * b != scale * (a+b)
// So this constant is ln(1.2) = 0.1823216 to make the default zoom 1.2x
scale *= exp(0.1823216 * delta);
double rightf = x/scale - offsetRight;
double upf = y/scale - offsetUp;
offset = offset.Plus(projRight.ScaledBy(rightf - righti));
offset = offset.Plus(projUp.ScaledBy(upf - upi));
if(SS.TW.shown.screen == TextWindow::Screen::EDIT_VIEW) {
if(havePainted) {
SS.ScheduleShowTW();
}
}
havePainted = false;
Invalidate();
ZoomToMouse(zoomMultiplyer);
}
void GraphicsWindow::MouseLeave() {

View File

@ -86,8 +86,10 @@ std::vector<FileFilter> SolveSpaceModelFileFilters = {
};
std::vector<FileFilter> SolveSpaceLinkFileFilters = {
{ CN_("file-type", "ALL"), { "slvs", "emn", "stl" } },
{ CN_("file-type", "SolveSpace models"), { "slvs" } },
{ CN_("file-type", "IDF circuit board"), { "emn" } },
{ CN_("file-type", "STL triangle mesh"), { "stl" } },
};
std::vector<FileFilter> RasterFileFilters = {

View File

@ -7,6 +7,7 @@
#ifndef SOLVESPACE_GUI_H
#define SOLVESPACE_GUI_H
namespace SolveSpace {
class RgbaColor;
namespace Platform {
@ -220,6 +221,7 @@ public:
std::function<bool(KeyboardEvent)> onKeyboardEvent;
std::function<void(std::string)> onEditingDone;
std::function<void(double)> onScrollbarAdjusted;
std::function<void()> onContextLost;
std::function<void()> onRender;
virtual ~Window() = default;
@ -228,7 +230,7 @@ public:
virtual double GetPixelDensity() = 0;
// Returns raster graphics and coordinate scale (already applied on the platform side),
// i.e. size of logical pixel in physical pixels, or device pixel ratio.
virtual int GetDevicePixelRatio() = 0;
virtual double GetDevicePixelRatio() = 0;
// Returns (fractional) font scale, to be applied on top of (integral) device pixel ratio.
virtual double GetDeviceFontScale() {
return GetPixelDensity() / GetDevicePixelRatio() / 96.0;
@ -386,5 +388,6 @@ void ExitGui();
void ClearGui();
}
} // namespace SolveSpace
#endif

View File

@ -33,7 +33,13 @@
#if defined(HAVE_SPACEWARE)
# include <spnav.h>
# include <gdk/gdkx.h>
# include <gdk/gdk.h>
# if defined(GDK_WINDOWING_X11)
# include <gdk/gdkx.h>
# endif
# if defined(GDK_WINDOWING_WAYLAND)
# include <gdk/gdkwayland.h>
# endif
# if GTK_CHECK_VERSION(3, 20, 0)
# include <gdkmm/seat.h>
# else
@ -511,8 +517,12 @@ protected:
}
bool on_motion_notify_event(GdkEventMotion *gdk_event) override {
if(process_pointer_event(MouseEvent::Type::MOTION,
gdk_event->x, gdk_event->y, gdk_event->state))
double x,y;
GdkModifierType state;
gdk_event_get_coords((GdkEvent*)gdk_event, &x, &y);
gdk_event_get_state((GdkEvent*)gdk_event, &state);
if(process_pointer_event(MouseEvent::Type::MOTION, x, y, state))
return true;
return Gtk::GLArea::on_motion_notify_event(gdk_event);
@ -520,51 +530,79 @@ protected:
bool on_button_press_event(GdkEventButton *gdk_event) override {
MouseEvent::Type type;
if(gdk_event->type == GDK_BUTTON_PRESS) {
GdkEventType gdk_type;
gdk_type = gdk_event_get_event_type((GdkEvent*)gdk_event);
if(gdk_type == GDK_BUTTON_PRESS) {
type = MouseEvent::Type::PRESS;
} else if(gdk_event->type == GDK_2BUTTON_PRESS) {
} else if(gdk_type == GDK_2BUTTON_PRESS) {
type = MouseEvent::Type::DBL_PRESS;
} else {
return Gtk::GLArea::on_button_press_event(gdk_event);
}
double x,y;
gdk_event_get_coords((GdkEvent*)gdk_event, &x, &y);
GdkModifierType state;
gdk_event_get_state((GdkEvent*)gdk_event, &state);
guint button;
gdk_event_get_button((GdkEvent*)gdk_event, &button);
if(process_pointer_event(type, gdk_event->x, gdk_event->y,
gdk_event->state, gdk_event->button))
if(process_pointer_event(type, x, y, state, button))
return true;
return Gtk::GLArea::on_button_press_event(gdk_event);
}
bool on_button_release_event(GdkEventButton *gdk_event) override {
if(process_pointer_event(MouseEvent::Type::RELEASE,
gdk_event->x, gdk_event->y,
gdk_event->state, gdk_event->button))
double x,y;
gdk_event_get_coords((GdkEvent*)gdk_event, &x, &y);
GdkModifierType state;
gdk_event_get_state((GdkEvent*)gdk_event, &state);
guint button;
gdk_event_get_button((GdkEvent*)gdk_event, &button);
if(process_pointer_event(MouseEvent::Type::RELEASE, x, y, state, button))
return true;
return Gtk::GLArea::on_button_release_event(gdk_event);
}
bool on_scroll_event(GdkEventScroll *gdk_event) override {
double dx, dy;
GdkScrollDirection dir;
// for gtk4 ??
// gdk_scroll_event_get_deltas((GdkEvent*)gdk_event, &dx, &dy);
// gdk_scroll_event_get_direction((GdkEvent*)gdk_event, &dir);
gdk_event_get_scroll_direction((GdkEvent*)gdk_event, &dir);
gdk_event_get_scroll_deltas((GdkEvent*)gdk_event, &dx, &dy);
double delta;
if(gdk_event->delta_y < 0 || gdk_event->direction == GDK_SCROLL_UP) {
if(dy < 0 || dir == GDK_SCROLL_UP) {
delta = 1;
} else if(gdk_event->delta_y > 0 || gdk_event->direction == GDK_SCROLL_DOWN) {
} else if(dy > 0 || dir == GDK_SCROLL_DOWN) {
delta = -1;
} else {
return false;
}
double x,y;
gdk_event_get_coords((GdkEvent*)gdk_event, &x, &y);
GdkModifierType state;
gdk_event_get_state((GdkEvent*)gdk_event, &state);
if(process_pointer_event(MouseEvent::Type::SCROLL_VERT,
gdk_event->x, gdk_event->y,
gdk_event->state, 0, delta))
x, y, state, 0, delta))
return true;
return Gtk::GLArea::on_scroll_event(gdk_event);
}
bool on_leave_notify_event(GdkEventCrossing *gdk_event) override {
if(process_pointer_event(MouseEvent::Type::LEAVE,
gdk_event->x, gdk_event->y, gdk_event->state))
double x,y;
gdk_event_get_coords((GdkEvent*)gdk_event, &x, &y);
GdkModifierType state;
gdk_event_get_state((GdkEvent*)gdk_event, &state);
if(process_pointer_event(MouseEvent::Type::LEAVE, x, y, state))
return true;
return Gtk::GLArea::on_leave_notify_event(gdk_event);
@ -574,22 +612,28 @@ protected:
KeyboardEvent event = {};
event.type = type;
GdkModifierType state;
gdk_event_get_state((GdkEvent*)gdk_event, &state);
Gdk::ModifierType mod_mask = get_modifier_mask(Gdk::MODIFIER_INTENT_DEFAULT_MOD_MASK);
if((gdk_event->state & mod_mask) & ~(GDK_SHIFT_MASK|GDK_CONTROL_MASK)) {
if((state & mod_mask) & ~(GDK_SHIFT_MASK|GDK_CONTROL_MASK)) {
return false;
}
event.shiftDown = (gdk_event->state & GDK_SHIFT_MASK) != 0;
event.controlDown = (gdk_event->state & GDK_CONTROL_MASK) != 0;
event.shiftDown = (state & GDK_SHIFT_MASK) != 0;
event.controlDown = (state & GDK_CONTROL_MASK) != 0;
char32_t chr = gdk_keyval_to_unicode(gdk_keyval_to_lower(gdk_event->keyval));
guint keyval;
gdk_event_get_keyval((GdkEvent*)gdk_event, &keyval);
char32_t chr = gdk_keyval_to_unicode(gdk_keyval_to_lower(keyval));
if(chr != 0) {
event.key = KeyboardEvent::Key::CHARACTER;
event.chr = chr;
} else if(gdk_event->keyval >= GDK_KEY_F1 &&
gdk_event->keyval <= GDK_KEY_F12) {
} else if(keyval >= GDK_KEY_F1 &&
keyval <= GDK_KEY_F12) {
event.key = KeyboardEvent::Key::FUNCTION;
event.num = gdk_event->keyval - GDK_KEY_F1 + 1;
event.num = keyval - GDK_KEY_F1 + 1;
} else {
return false;
}
@ -691,8 +735,11 @@ public:
protected:
bool on_key_press_event(GdkEventKey *gdk_event) override {
guint keyval;
gdk_event_get_keyval((GdkEvent*)gdk_event, &keyval);
if(is_editing()) {
if(gdk_event->keyval == GDK_KEY_Escape) {
if(keyval == GDK_KEY_Escape) {
return _gl_widget.event((GdkEvent *)gdk_event);
} else {
_entry.event((GdkEvent *)gdk_event);
@ -835,7 +882,11 @@ protected:
}
bool on_window_state_event(GdkEventWindowState *gdk_event) override {
_is_fullscreen = gdk_event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN;
// window state event is superseded by GdkWindow::state on GTK4
GdkWindowState new_window_state;
new_window_state = gdk_event->new_window_state;
_is_fullscreen = new_window_state & GDK_WINDOW_STATE_FULLSCREEN;
if(_receiver->onFullScreen) {
_receiver->onFullScreen(_is_fullscreen);
}
@ -883,7 +934,7 @@ public:
return gtkWindow.get_screen()->get_resolution();
}
int GetDevicePixelRatio() override {
double GetDevicePixelRatio() override {
return gtkWindow.get_scale_factor();
}
@ -1047,7 +1098,7 @@ WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) {
void Open3DConnexion() {}
void Close3DConnexion() {}
#if defined(HAVE_SPACEWARE) && defined(GDK_WINDOWING_X11)
#if defined(HAVE_SPACEWARE) && (defined(GDK_WINDOWING_X11) || defined(GDK_WINDOWING_WAYLAND))
static void ProcessSpnavEvent(WindowImplGtk *window, const spnav_event &spnavEvent, bool shiftDown, bool controlDown) {
switch(spnavEvent.type) {
case SPNAV_EVENT_MOTION: {
@ -1129,17 +1180,26 @@ void Request3DConnexionEventsForWindow(WindowRef window) {
std::static_pointer_cast<WindowImplGtk>(window);
Glib::RefPtr<Gdk::Window> gdkWindow = windowImpl->gtkWindow.get_window();
if(!GDK_IS_X11_DISPLAY(gdkWindow->get_display()->gobj())) {
return;
#if defined(GDK_WINDOWING_X11)
if(GDK_IS_X11_DISPLAY(gdkWindow->get_display()->gobj())) {
if(spnav_x11_open(gdk_x11_get_default_xdisplay(),
gdk_x11_window_get_xid(gdkWindow->gobj())) != -1) {
gdkWindow->add_filter(GdkSpnavFilter, windowImpl.get());
} else if(spnav_open() != -1) {
g_io_add_watch(g_io_channel_unix_new(spnav_fd()), G_IO_IN,
ConsumeSpnavQueue, windowImpl.get());
}
}
#endif
#if defined(GDK_WINDOWING_WAYLAND)
if(GDK_IS_WAYLAND_DISPLAY(gdkWindow->get_display()->gobj())) {
if(spnav_open() != -1) {
g_io_add_watch(g_io_channel_unix_new(spnav_fd()), G_IO_IN,
ConsumeSpnavQueue, windowImpl.get());
}
}
#endif
if(spnav_x11_open(gdk_x11_get_default_xdisplay(),
gdk_x11_window_get_xid(gdkWindow->gobj())) != -1) {
gdkWindow->add_filter(GdkSpnavFilter, windowImpl.get());
} else if(spnav_open() != -1) {
g_io_add_watch(g_io_channel_unix_new(spnav_fd()), G_IO_IN,
ConsumeSpnavQueue, windowImpl.get());
}
}
#else
void Request3DConnexionEventsForWindow(WindowRef window) {}
@ -1383,6 +1443,9 @@ public:
gtkDialog.add_button(isSave ? C_("button", "_Save")
: C_("button", "_Open"), Gtk::RESPONSE_OK);
gtkDialog.set_default_response(Gtk::RESPONSE_OK);
if(isSave) {
gtkDialog.set_do_overwrite_confirmation(true);
}
InitFileChooser(gtkDialog);
}
@ -1416,6 +1479,9 @@ public:
isSave ? C_("button", "_Save")
: C_("button", "_Open"),
C_("button", "_Cancel"));
if(isSave) {
gtkNative->set_do_overwrite_confirmation(true);
}
// Seriously, GTK?!
InitFileChooser(*gtkNative.operator->());
}
@ -1478,7 +1544,8 @@ std::vector<Platform::Path> GetFontFiles() {
}
void OpenInBrowser(const std::string &url) {
gtk_show_uri(Gdk::Screen::get_default()->gobj(), url.c_str(), GDK_CURRENT_TIME, NULL);
// first param should be our window?
gtk_show_uri_on_window(NULL, url.c_str(), GDK_CURRENT_TIME, NULL);
}
Gtk::Main *gtkMain;

1464
src/platform/guihtml.cpp Normal file

File diff suppressed because it is too large Load Diff

View File

@ -286,7 +286,8 @@ public:
}
void PopUp() override {
[NSMenu popUpContextMenu:nsMenu withEvent:[NSApp currentEvent] forView:nil];
NSEvent* event = [NSApp currentEvent];
[NSMenu popUpContextMenu:nsMenu withEvent:event forView:event.window.contentView];
}
void Clear() override {
@ -358,18 +359,27 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
- (void)didEdit:(NSString *)text;
@property double scrollerMin;
@property double scrollerMax;
@property double scrollerSize;
@property double pageSize;
@end
@implementation SSView
{
NSTrackingArea *trackingArea;
NSTextField *editor;
double magnificationGestureCurrentZ;
double rotationGestureCurrent;
Point2d trackpadPositionShift;
bool inTrackpadScrollGesture;
int activeTrackpadTouches;
bool scrollFromTrackpadTouch;
Platform::Window::Kind kind;
}
@synthesize acceptsFirstResponder;
- (id)initWithFrame:(NSRect)frameRect {
- (id)initWithKind:(Platform::Window::Kind)aKind {
NSOpenGLPixelFormatAttribute attrs[] = {
NSOpenGLPFADoubleBuffer,
NSOpenGLPFAColorSize, 24,
@ -377,7 +387,7 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
0
};
NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs];
if(self = [super initWithFrame:frameRect pixelFormat:pixelFormat]) {
if(self = [super initWithFrame:NSMakeRect(0, 0, 0, 0) pixelFormat:pixelFormat]) {
self.wantsBestResolutionOpenGLSurface = YES;
self.wantsLayer = YES;
editor = [[NSTextField alloc] init];
@ -387,6 +397,21 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
editor.bezeled = NO;
editor.target = self;
editor.action = @selector(didEdit:);
inTrackpadScrollGesture = false;
activeTrackpadTouches = 0;
scrollFromTrackpadTouch = false;
self.acceptsTouchEvents = YES;
kind = aKind;
if(kind == Platform::Window::Kind::TOPLEVEL) {
NSGestureRecognizer *mag = [[NSMagnificationGestureRecognizer alloc] initWithTarget:self
action:@selector(magnifyGesture:)];
[self addGestureRecognizer:mag];
NSRotationGestureRecognizer* rot = [[NSRotationGestureRecognizer alloc] initWithTarget:self
action:@selector(rotateGesture:)];
[self addGestureRecognizer:rot];
}
}
return self;
}
@ -427,9 +452,9 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
- (Platform::MouseEvent)convertMouseEvent:(NSEvent *)nsEvent {
Platform::MouseEvent event = {};
NSPoint nsPoint = [self convertPoint:nsEvent.locationInWindow fromView:self];
NSPoint nsPoint = [self convertPoint:nsEvent.locationInWindow fromView:nil];
event.x = nsPoint.x;
event.y = self.bounds.size.height - nsPoint.y;
event.y = nsPoint.y;
NSUInteger nsFlags = [nsEvent modifierFlags];
if(nsFlags & NSEventModifierFlagShift) event.shiftDown = true;
@ -553,14 +578,78 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
using Platform::MouseEvent;
MouseEvent event = [self convertMouseEvent:nsEvent];
if(nsEvent.phase == NSEventPhaseBegan) {
// If this scroll began on trackpad then touchesBeganWithEvent was called prior to this
// event and we have at least one active trackpad touch. We store this information so we
// can handle scroll originating from trackpad differently below.
scrollFromTrackpadTouch = activeTrackpadTouches > 0 &&
nsEvent.subtype == NSEventSubtypeTabletPoint &&
kind == Platform::Window::Kind::TOPLEVEL;
}
// Check if we are scrolling on trackpad and handle things differently.
if(scrollFromTrackpadTouch) {
// This is how Cocoa represents 2 finger trackpad drag gestures, rather than going via
// NSPanGestureRecognizer which is how you might expect this to work... We complicate this
// further by also handling shift-two-finger-drag to mean rotate. Fortunately we're using
// shift in the same way as right-mouse-button MouseEvent does (to converts a pan to a
// rotate) so we get the rotate support for free. It's a bit ugly having to fake mouse
// events and track the deviation from the actual mouse cursor with trackpadPositionShift,
// but in lieu of an event API that allows us to request a rotate/pan with relative
// coordinates, it's the best we can do.
event.button = MouseEvent::Button::RIGHT;
// Make sure control (actually cmd) isn't passed through, ctrl-right-click-drag has special
// meaning as rotate which we don't want to inadvertently trigger.
event.controlDown = false;
if(nsEvent.scrollingDeltaX == 0 && nsEvent.scrollingDeltaY == 0) {
// Cocoa represents the point where the user lifts their fingers off (and any inertial
// scrolling has finished) by an event with scrollingDeltaX and scrollingDeltaY both 0.
// Sometimes you also get a zero scroll at the start of a two-finger-rotate (probably
// reflecting the internal implementation of that being a cancelled possible pan
// gesture), which is why this conditional is structured the way it is.
if(inTrackpadScrollGesture) {
event.x += trackpadPositionShift.x;
event.y += trackpadPositionShift.y;
event.type = MouseEvent::Type::RELEASE;
receiver->onMouseEvent(event);
inTrackpadScrollGesture = false;
trackpadPositionShift = Point2d::From(0, 0);
}
return;
} else if(!inTrackpadScrollGesture) {
inTrackpadScrollGesture = true;
trackpadPositionShift = Point2d::From(0, 0);
event.type = MouseEvent::Type::PRESS;
receiver->onMouseEvent(event);
// And drop through
}
trackpadPositionShift.x += nsEvent.scrollingDeltaX;
trackpadPositionShift.y += nsEvent.scrollingDeltaY;
event.type = MouseEvent::Type::MOTION;
event.x += trackpadPositionShift.x;
event.y += trackpadPositionShift.y;
receiver->onMouseEvent(event);
return;
}
event.type = MouseEvent::Type::SCROLL_VERT;
bool isPrecise = [nsEvent hasPreciseScrollingDeltas];
event.scrollDelta = [nsEvent scrollingDeltaY] / (isPrecise ? 50 : 5);
if(receiver->onMouseEvent) {
receiver->onMouseEvent(event);
}
receiver->onMouseEvent(event);
}
- (void)touchesBeganWithEvent:(NSEvent *)event {
activeTrackpadTouches++;
}
- (void)touchesEndedWithEvent:(NSEvent *)event {
activeTrackpadTouches--;
}
- (void)touchesCancelledWithEvent:(NSEvent *)event {
activeTrackpadTouches--;
}
- (void)mouseExited:(NSEvent *)nsEvent {
@ -638,6 +727,50 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
[super keyUp:nsEvent];
}
- (void)magnifyGesture:(NSMagnificationGestureRecognizer *)gesture {
// The onSixDofEvent API doesn't allow us to specify the scaling's origin, so for expediency
// we fake out a scrollwheel MouseEvent with a suitably-scaled scrollDelta with a bit of
// absolute-to-relative positioning conversion tracked using magnificationGestureCurrentZ.
if(gesture.state == NSGestureRecognizerStateBegan) {
magnificationGestureCurrentZ = 0.0;
}
// Magic number to make gesture.magnification align roughly with what scrollDelta expects
constexpr double kScale = 10.0;
double z = ((double)gesture.magnification * kScale);
double zdelta = z - magnificationGestureCurrentZ;
magnificationGestureCurrentZ = z;
using Platform::MouseEvent;
MouseEvent event = {};
event.type = MouseEvent::Type::SCROLL_VERT;
NSPoint nsPoint = [gesture locationInView:self];
event.x = nsPoint.x;
event.y = nsPoint.y;
event.scrollDelta = zdelta;
if(receiver->onMouseEvent) {
receiver->onMouseEvent(event);
}
}
- (void)rotateGesture:(NSRotationGestureRecognizer *)gesture {
if(gesture.state == NSGestureRecognizerStateBegan) {
rotationGestureCurrent = 0.0;
}
double rotation = gesture.rotation;
double rotationDelta = rotation - rotationGestureCurrent;
rotationGestureCurrent = rotation;
using Platform::SixDofEvent;
SixDofEvent event = {};
event.type = SixDofEvent::Type::MOTION;
event.rotationZ = rotationDelta;
if(receiver->onSixDofEvent) {
receiver->onSixDofEvent(event);
}
}
@synthesize editing;
- (void)startEditing:(NSString *)text at:(NSPoint)origin withHeight:(double)fontHeight
@ -698,11 +831,27 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
}
@synthesize scrollerMin;
@synthesize scrollerMax;
@synthesize scrollerSize;
@synthesize pageSize;
- (void)didScroll:(NSScroller *)sender {
double pos;
switch(sender.hitPart) {
case NSScrollerKnob:
case NSScrollerKnobSlot:
pos = receiver->GetScrollbarPosition();
break;
case NSScrollerDecrementPage:
pos = receiver->GetScrollbarPosition() - pageSize;
break;
case NSScrollerIncrementPage:
pos = receiver->GetScrollbarPosition() + pageSize;
break;
default:
return;
}
if(receiver->onScrollbarAdjusted) {
double pos = scrollerMin + [sender doubleValue] * (scrollerMax - scrollerMin);
receiver->onScrollbarAdjusted(pos);
}
}
@ -769,7 +918,7 @@ public:
NSString *nsToolTip;
WindowImplCocoa(Window::Kind kind, std::shared_ptr<WindowImplCocoa> parentWindow) {
ssView = [[SSView alloc] init];
ssView = [[SSView alloc] initWithKind:kind];
ssView.translatesAutoresizingMaskIntoConstraints = NO;
ssView.receiver = this;
@ -838,10 +987,10 @@ public:
return (displayPixelSize.width / displayPhysicalSize.width) * 25.4f;
}
int GetDevicePixelRatio() override {
double GetDevicePixelRatio() override {
NSSize unitSize = { 1.0f, 0.0f };
unitSize = [ssView convertSizeToBacking:unitSize];
return (int)unitSize.width;
return unitSize.width;
}
bool IsVisible() override {
@ -962,21 +1111,22 @@ public:
void ConfigureScrollbar(double min, double max, double pageSize) override {
ssView.scrollerMin = min;
ssView.scrollerMax = max - pageSize;
[nsScroller setKnobProportion:(pageSize / (ssView.scrollerMax - ssView.scrollerMin))];
ssView.scrollerSize = max + 1 - min;
ssView.pageSize = pageSize;
nsScroller.knobProportion = pageSize / ssView.scrollerSize;
nsScroller.hidden = pageSize >= ssView.scrollerSize;
}
double GetScrollbarPosition() override {
// Platform::Window scrollbar positions are in the range [min, max+1 - pageSize] inclusive,
// and Cocoa scrollbars are from 0.0 to 1.0 inclusive, so we have to apply some scaling and
// transforming. (scrollerSize is max+1-min, see ConfigureScrollbar above)
return ssView.scrollerMin +
[nsScroller doubleValue] * (ssView.scrollerMax - ssView.scrollerMin);
nsScroller.doubleValue * (ssView.scrollerSize - ssView.pageSize);
}
void SetScrollbarPosition(double pos) override {
if(pos > ssView.scrollerMax)
pos = ssView.scrollerMax;
if(GetScrollbarPosition() == pos)
return;
[nsScroller setDoubleValue:(pos / (ssView.scrollerMax - ssView.scrollerMin))];
nsScroller.doubleValue = (pos - ssView.scrollerMin) / ( ssView.scrollerSize - ssView.pageSize);
}
void Invalidate() override {
@ -1426,9 +1576,22 @@ void OpenInBrowser(const std::string &url) {
- (IBAction)preferences:(id)sender;
- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename;
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender;
@property BOOL exiting;
@end
@implementation SSApplicationDelegate
@synthesize exiting;
- (id)init {
if (self = [super init]) {
self.exiting = false;
}
return self;
}
- (IBAction)preferences:(id)sender {
if (!SS.GW.showTextWindow) {
SolveSpace::SS.GW.MenuView(SolveSpace::Command::SHOW_TEXT_WND);
@ -1443,12 +1606,27 @@ void OpenInBrowser(const std::string &url) {
}
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
[[[NSApp mainWindow] delegate] windowShouldClose:[NSApp mainWindow]];
return NSTerminateCancel;
if(!SS.unsaved) {
return NSTerminateNow;
} else {
[self performSelectorOnMainThread:@selector(applicationTerminatePrompt) withObject:nil
waitUntilDone:NO modes:@[NSDefaultRunLoopMode, NSModalPanelRunLoopMode]];
return NSTerminateLater;
}
}
- (void)applicationWillTerminate:(NSNotification *)notification {
if(!exiting) {
// Prevent the Platform::ExitGui() call from SolveSpaceUI::Exit()
// triggering another terminate
exiting = true;
// Now let SS save settings etc
SS.Exit();
}
}
- (void)applicationTerminatePrompt {
SolveSpace::SS.MenuFile(SolveSpace::Command::EXIT);
[NSApp replyToApplicationShouldTerminate:SS.OkayToStartNewFile()];
}
@end
@ -1469,6 +1647,14 @@ std::vector<std::string> InitGui(int argc, char **argv) {
ssDelegate = [[SSApplicationDelegate alloc] init];
NSApplication.sharedApplication.delegate = ssDelegate;
// Setting this prevents "Show Tab Bar" and "Show All Tabs" items from being
// automagically added to the View menu
NSWindow.allowsAutomaticWindowTabbing = NO;
// And this prevents the duplicate "Enter Full Screen" menu item, see
// https://stackoverflow.com/questions/52154977/how-to-get-rid-of-enter-full-screen-menu-item
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"NSFullScreenMenuItemEverywhere"];
[NSBundle.mainBundle loadNibNamed:@"MainMenu" owner:nil topLevelObjects:nil];
NSArray *languages = NSLocale.preferredLanguages;
@ -1487,8 +1673,10 @@ void RunGui() {
}
void ExitGui() {
[NSApp setDelegate:nil];
[NSApp terminate:nil];
if(!ssDelegate.exiting) {
ssDelegate.exiting = true;
[NSApp terminate:nil];
}
}
void ClearGui() {}

View File

@ -793,7 +793,7 @@ public:
break;
case WM_SIZING: {
int pixelRatio = window->GetDevicePixelRatio();
double pixelRatio = window->GetDevicePixelRatio();
RECT rcw, rcc;
sscheck(GetWindowRect(window->hWindow, &rcw));
@ -806,10 +806,10 @@ public:
int adjHeight = rc->bottom - rc->top;
adjWidth -= nonClientWidth;
adjWidth = max(window->minWidth * pixelRatio, adjWidth);
adjWidth += nonClientWidth;
adjWidth = max((int)(window->minWidth * pixelRatio), adjWidth);
adjWidth += nonClientWidth;
adjHeight -= nonClientHeight;
adjHeight = max(window->minHeight * pixelRatio, adjHeight);
adjHeight = max((int)(window->minHeight * pixelRatio), adjHeight);
adjHeight += nonClientHeight;
switch(wParam) {
case WMSZ_RIGHT:
@ -868,7 +868,7 @@ public:
case WM_MOUSEMOVE:
case WM_MOUSEWHEEL:
case WM_MOUSELEAVE: {
int pixelRatio = window->GetDevicePixelRatio();
double pixelRatio = window->GetDevicePixelRatio();
MouseEvent event = {};
event.x = GET_X_LPARAM(lParam) / pixelRatio;
@ -941,7 +941,7 @@ public:
event.y = pt.y / pixelRatio;
event.type = MouseEvent::Type::SCROLL_VERT;
event.scrollDelta = GET_WHEEL_DELTA_WPARAM(wParam) / WHEEL_DELTA;
event.scrollDelta = GET_WHEEL_DELTA_WPARAM(wParam) / (double)WHEEL_DELTA;
break;
case WM_MOUSELEAVE:
@ -1109,10 +1109,10 @@ public:
return (double)dpi;
}
int GetDevicePixelRatio() override {
double GetDevicePixelRatio() override {
UINT dpi;
sscheck(dpi = ssGetDpiForWindow(hWindow));
return dpi / USER_DEFAULT_SCREEN_DPI;
return (double)dpi / USER_DEFAULT_SCREEN_DPI;
}
bool IsVisible() override {
@ -1177,27 +1177,27 @@ public:
}
void GetContentSize(double *width, double *height) override {
int pixelRatio = GetDevicePixelRatio();
double pixelRatio = GetDevicePixelRatio();
RECT rc;
sscheck(GetClientRect(hWindow, &rc));
*width = (rc.right - rc.left) / pixelRatio;
*height = (rc.bottom - rc.top) / pixelRatio;
*width = (rc.right - rc.left) / pixelRatio;
*height = (rc.bottom - rc.top) / pixelRatio;
}
void SetMinContentSize(double width, double height) {
minWidth = (int)width;
minHeight = (int)height;
int pixelRatio = GetDevicePixelRatio();
double pixelRatio = GetDevicePixelRatio();
RECT rc;
sscheck(GetClientRect(hWindow, &rc));
if(rc.right - rc.left < minWidth * pixelRatio) {
rc.right = rc.left + minWidth * pixelRatio;
rc.right = rc.left + (LONG)(minWidth * pixelRatio);
}
if(rc.bottom - rc.top < minHeight * pixelRatio) {
rc.bottom = rc.top + minHeight * pixelRatio;
rc.bottom = rc.top + (LONG)(minHeight * pixelRatio);
}
}
@ -1229,7 +1229,7 @@ public:
sscheck(GetMonitorInfo(MonitorFromRect(&rc, MONITOR_DEFAULTTONEAREST), &mi));
// If it somehow ended up off-screen, then put it back.
// and make it visible by at least this portion of the scrren
// and make it visible by at least this portion of the screen
const LONG movein = 40;
RECT mrc = mi.rcMonitor;
@ -1270,7 +1270,7 @@ public:
tooltipText = newText;
if(!newText.empty()) {
int pixelRatio = GetDevicePixelRatio();
double pixelRatio = GetDevicePixelRatio();
RECT toolRect;
toolRect.left = (int)(x * pixelRatio);
toolRect.top = (int)(y * pixelRatio);
@ -1301,9 +1301,9 @@ public:
bool isMonospace, const std::string &text) override {
if(IsEditorVisible()) return;
int pixelRatio = GetDevicePixelRatio();
double pixelRatio = GetDevicePixelRatio();
HFONT hFont = CreateFontW(-(LONG)fontHeight * GetDevicePixelRatio(), 0, 0, 0,
HFONT hFont = CreateFontW(-(int)(fontHeight * GetDevicePixelRatio()), 0, 0, 0,
FW_REGULAR, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
DEFAULT_QUALITY, FF_DONTCARE, isMonospace ? L"Lucida Console" : L"Arial");
if(hFont == NULL) {
@ -1324,12 +1324,12 @@ public:
sscheck(ReleaseDC(hEditor, hDc));
RECT rc;
rc.left = (LONG)x * pixelRatio;
rc.top = (LONG)y * pixelRatio - tm.tmAscent;
rc.left = (LONG)(x * pixelRatio);
rc.top = (LONG)(y * pixelRatio) - tm.tmAscent;
// Add one extra char width to avoid scrolling.
rc.right = (LONG)x * pixelRatio +
std::max((LONG)minWidth * pixelRatio, ts.cx + tm.tmAveCharWidth);
rc.bottom = (LONG)y * pixelRatio + tm.tmDescent;
rc.right = (LONG)(x * pixelRatio) +
std::max((LONG)(minWidth * pixelRatio), ts.cx + tm.tmAveCharWidth);
rc.bottom = (LONG)(y * pixelRatio) + tm.tmDescent;
sscheck(ssAdjustWindowRectExForDpi(&rc, 0, /*bMenu=*/FALSE, WS_EX_CLIENTEDGE,
ssGetDpiForWindow(hWindow)));
@ -1608,7 +1608,7 @@ public:
void AddFilter(std::string name, std::vector<std::string> extensions) override {
std::string desc, patterns;
for(auto extension : extensions) {
for(auto &extension : extensions) {
std::string pattern = "*." + extension;
if(!desc.empty()) desc += ", ";
desc += pattern;

View File

@ -0,0 +1,91 @@
<!doctype html>
<html><!--
--><head><!--
--><meta charset="utf-8"><!--
--><title>SolveSpace Web Edition (EXPERIMENTAL)</title><!--
--><link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.1/css/all.css" integrity="sha384-O8whS3fhG2OnA5Kas0Y9l3cfpmYjapjI0E4theH4iuMD+pLhbf6JI0jIMfYcK3yZ" crossorigin="anonymous"><!--
--><link rel="stylesheet" href="solvespaceui.css"><!--
--><script src="solvespaceui.js"></script><!--
--><script src="filemanagerui.js"></script><!--
--></head><!--
--><body><!--
--><div id="splash">
<div class="center">
<div id="spinner"></div>
<div id="status">Downloading...</div>
<div id="crash" style="display:none;">
SolveSpace has crashed. See console for details.<br>
The Web Edition of SolveSpace is experimental,<br>
and may not be as reliable as the Desktop Edition.<br>
<a href="javascript:window.location.reload()">Restart</a>
</div>
<progress id="progress" value="0" max="100" hidden="1"></progress>
</div>
</div><!--
--><main><!--
FIXME(emscripten): without this, a window resize is required in Chrome
to get the layout to update and canvas size to match up. What?
--><ul class="menu menubar" style="visibility: hidden"><li>None</li></ul><!--
--><div id="container"><!--
--><div id="container0"><canvas id="canvas0"></canvas></div><!--
--><div id="view_separator"></div><!--
--><div id="container1parent"><!--
--><div id="container1"><canvas id="canvas1"></canvas></div><!--
--><div id="canvas1scrollbarbox"><!--
--><div id="canvas1scrollbar"></div><!--
--></div><!--
--></div><!--
--></div><!--
--></main><!--
--><script type="text/javascript">
var splashElement = document.getElementById('splash');
var spinnerElement = document.getElementById('spinner');
var statusElement = document.getElementById('status');
var progressElement = document.getElementById('progress');
var crashElement = document.getElementById('crash');
var canvas0Element = document.getElementById('canvas0');
var canvas1Element = document.getElementById('canvas1');
canvas0Element.oncontextmenu = function(event) { event.preventDefault(); }
canvas1Element.oncontextmenu = function(event) { event.preventDefault(); }
var Module = {
preRun: [],
postRun: [],
print: console.log,
printErr: console.error,
state: 'loading',
setStatus: function(text) {
if(this.state == 'crashed') {
spinnerElement.style.display = 'none';
statusElement.style.display = 'none';
crashElement.style.display = '';
splashElement.style.display = '';
} else if(text != '') {
console.log('Status:', text);
statusElement.innerText = text;
} else if(this.state != 'done') {
console.log('Status: Done!');
splashElement.style.display = 'none';
this.state = 'done';
}
},
totalDependencies: 0,
monitorRunDependencies: function(remainingDependencies) {
this.totalDependencies = Math.max(this.totalDependencies, remainingDependencies);
if(remainingDependencies > 0) {
var completeDependencies = this.totalDependencies - remainingDependencies;
Module.setStatus('Preparing... (' + completeDependencies + '/' +
this.totalDependencies + ')');
}
}
};
Module.setStatus('Downloading...');
window.onerror = function() {
Module.state = 'crashed';
Module.setStatus();
return false;
};
</script><!--
-->{{{ SCRIPT }}}<!--
--></body></html>

View File

@ -0,0 +1,525 @@
"use strict";
const FileManagerUI_OPEN = 0;
const FileManagerUI_SAVE = FileManagerUI_OPEN + 1;
const FileManagerUI_BROWSE = FileManagerUI_SAVE + 1;
//FIXME(emscripten): File size thresholds. How large file can we accept safely ?
/** Maximum filesize for a uploaded file.
* @type {number} */
const FileManagerUI_UPLOAD_FILE_SIZE_LIMIT = 50 * 1000 * 1000;
const tryMakeDirectory = (path) => {
try {
FS.mkdir(path);
} catch {
// NOP
}
}
class FileManagerUI {
/**
* @param {number} mode - dialog mode FileManagerUI_[ OPEN, SAVE, BROWSE ]
*/
constructor(mode) {
/** @type {boolean} */
this.__isOpenDialog = false;
/** @type {boolean} */
this.__isSaveDialog = false;
/** @type {boolean} */
this.__isBrowseDialog = false;
if (mode == FileManagerUI_OPEN) {
this.__isOpenDialog = true;
} else if (mode == FileManagerUI_SAVE) {
this.__isSaveDialog = true;
} else {
this.__isBrowseDialog = true;
}
/** @type {boolean} true if the dialog is shown. */
this.__isShown = false;
/** @type {string[]} */
this.__extension_filters = [".slvs"];
/** @type {string} */
this.__basePathInFilesystem = "";
/** @type {string} filename user selected. empty if nothing selected */
this.__selectedFilename = "";
this.__closedWithCancel = false;
this.__defaultFilename = "untitled";
}
/** deconstructor
*/
dispose() {
if (this.__dialogRootElement) {
this.__dialogHeaderElement = null;
this.__descriptionElement = null;
this.__filelistElement = null;
this.__fileInputElement = null;
this.__saveFilenameInputElement = null;
this.__buttonContainerElement = null;
this.__dialogRootElement.parentElement.removeChild(this.__dialogRootElement);
this.__dialogRootElement = null;
}
}
/**
* @param {string} label
* @param {string} response
* @param {bool} isDefault
*/
__addButton(label, response, isDefault, onclick) {
const buttonElem = document.createElement("div");
addClass(buttonElem, "button");
setLabelWithMnemonic(buttonElem, label);
if (isDefault) {
addClass(buttonElem, "default");
addClass(buttonElem, "selected");
}
buttonElem.addEventListener("click", () => {
if (onclick) {
if (onclick()) {
this.__close();
}
} else {
this.__close();
}
});
this.__buttonContainerElement.appendChild(buttonElem);
}
/**
* @param {HTMLElement} div element that built
*/
buildDialog() {
const root = document.createElement('div');
addClass(root, "modal");
root.style.display = "none";
root.style.zIndex = 1000;
const dialog = document.createElement('div');
addClass(dialog, "dialog");
addClass(dialog, "wide");
root.appendChild(dialog);
const messageHeader = document.createElement('strong');
this.__dialogHeaderElement = messageHeader;
addClass(messageHeader, "dialog_header");
dialog.appendChild(messageHeader);
const description = document.createElement('p');
this.__descriptionElement = description;
dialog.appendChild(description);
const filelistheader = document.createElement('h3');
filelistheader.textContent = 'Files:';
dialog.appendChild(filelistheader);
const filelist = document.createElement('ul');
this.__filelistElement = filelist;
addClass(filelist, 'filelist');
dialog.appendChild(filelist);
const dummyfilelistitem = document.createElement('li');
dummyfilelistitem.textContent = "(No file in pseudo filesystem)";
filelist.appendChild(dummyfilelistitem);
if (this.__isOpenDialog) {
const fileuploadcontainer = document.createElement('div');
dialog.appendChild(fileuploadcontainer);
const fileuploadheader = document.createElement('h3');
fileuploadheader.textContent = "Upload file:";
fileuploadcontainer.appendChild(fileuploadheader);
const dragdropdescription = document.createElement('p');
dragdropdescription.textContent = "(Drag & drop file to the following box)";
dragdropdescription.style.fontSize = "0.8em";
dragdropdescription.style.margin = "0.1em";
fileuploadcontainer.appendChild(dragdropdescription);
const filedroparea = document.createElement('div');
addClass(filedroparea, 'filedrop');
filedroparea.addEventListener('dragstart', (ev) => this.__onFileDragDrop(ev));
filedroparea.addEventListener('dragover', (ev) => this.__onFileDragDrop(ev));
filedroparea.addEventListener('dragleave', (ev) => this.__onFileDragDrop(ev));
filedroparea.addEventListener('drop', (ev) => this.__onFileDragDrop(ev));
fileuploadcontainer.appendChild(filedroparea);
const fileinput = document.createElement('input');
this.__fileInputElement = fileinput;
fileinput.setAttribute('type', 'file');
fileinput.style.width = "100%";
fileinput.addEventListener('change', (ev) => this.__onFileInputChanged(ev));
filedroparea.appendChild(fileinput);
} else if (this.__isSaveDialog) {
const filenameinputcontainer = document.createElement('div');
dialog.appendChild(filenameinputcontainer);
const filenameinputheader = document.createElement('h3');
filenameinputheader.textContent = "Filename:";
filenameinputcontainer.appendChild(filenameinputheader);
const filenameinput = document.createElement('input');
filenameinput.setAttribute('type', 'input');
filenameinput.style.width = "90%";
filenameinput.style.margin = "auto 1em auto 1em";
this.__saveFilenameInputElement = filenameinput;
filenameinputcontainer.appendChild(filenameinput);
}
// Paragraph element for spacer
dialog.appendChild(document.createElement('p'));
const buttoncontainer = document.createElement('div');
this.__buttonContainerElement = buttoncontainer;
addClass(buttoncontainer, "buttons");
dialog.appendChild(buttoncontainer);
this.__addButton('OK', 0, false, () => {
if (this.__isOpenDialog) {
let selectedFilename = null;
const fileitems = document.querySelectorAll('input[type="radio"][name="filemanager_filelist"]');
Array.from(fileitems).forEach((radiobox) => {
if (radiobox.checked) {
selectedFilename = radiobox.parentElement.getAttribute('data-filename');
}
});
if (selectedFilename) {
return true;
} else {
return false;
}
} else {
return true;
}
});
this.__addButton('Cancel', 1, true, () => {
this.__closedWithCancel = true;
return true;
});
return root;
}
/**
* @param {string} text
*/
setTitle(text) {
this.__dialogHeaderText = text;
}
/**
* @param {string} text
*/
setDescription(text) {
this.__descriptionText = text;
}
/**
* @param {string} path file prefix. (ex) 'tmp/' to '/tmp/filename.txt'
*/
setBasePath(path) {
this.__basePathInFilesystem = path;
tryMakeDirectory(path);
}
/**
* @param {string} filename
*/
setDefaultFilename(filename) {
this.__defaultFilename = filename;
}
/**
*
* @param {string} filter comma-separated extensions like ".slvs,.stl;."
*/
setFilter(filter) {
const exts = filter.split(',');
this.__extension_filters = exts;
}
__buildFileEntry(filename) {
const lielem = document.createElement('li');
const label = document.createElement('label');
label.setAttribute('data-filename', filename);
lielem.appendChild(label);
const radiobox = document.createElement('input');
radiobox.setAttribute('type', 'radio');
if (!this.__isOpenDialog) {
radiobox.style.display = "none";
}
radiobox.setAttribute('name', 'filemanager_filelist');
label.appendChild(radiobox);
const filenametext = document.createTextNode(filename);
label.appendChild(filenametext);
return lielem;
}
/**
* @returns {string[]} filename array
*/
__getFileEntries() {
const basePath = this.__basePathInFilesystem;
/** @type {any[]} */
const nodes = FS.readdir(basePath);
/** @type {string[]} */
const files = nodes.filter((nodename) => {
return FS.isFile(FS.lstat(basePath + nodename).mode);
});
/*.map((filename) => {
return basePath + filename;
});*/
console.log(`__getFileEntries():`, files);
return files;
}
/**
* @param {string[]?} files file list already constructed
* @returns {string[]} filename array
*/
__getFileEntries_recurse(basePath) {
//FIXME:remove try catch block
try {
//const basePath = this.__basePathInFilesystem;
FS.currentPath = basePath;
/** @type {any[]} */
const nodes = FS.readdir(basePath);
const filesInThisDirectory = nodes.filter((nodename) => {
return FS.isFile(FS.lstat(basePath + "/" + nodename).mode);
}).map((filename) => {
return basePath + "/" + filename;
});
let files = filesInThisDirectory;
const directories = nodes.filter((nodename) => {
return FS.isDir(FS.lstat(basePath + "/" + nodename).mode);
});
for (let i = 0; i < directories.length; i++) {
const directoryname = directories[i];
if (directoryname == '.' || directoryname == '..') {
continue;
}
const orig_cwd = FS.currentPath;
const directoryfullpath = basePath + "/" + directoryname;
FS.currentPath = directoryfullpath;
files = files.concat(this.__getFileEntries_recurse(directoryfullpath));
FS.currentPath = orig_cwd;
}
console.log(`__getFileEntries_recurse(): in "${basePath}"`, files);
return files;
} catch (excep) {
console.log(excep);
throw excep;
}
}
__updateFileList() {
console.log(`__updateFileList()`);
Array.from(this.__filelistElement.children).forEach((elem) => {
this.__filelistElement.removeChild(elem);
});
// const files = this.__getFileEntries();
FS.currentPath = this.__basePathInFilesystem;
const files = this.__getFileEntries_recurse(this.__basePathInFilesystem);
if (files.length < 1) {
const dummyfilelistitem = document.createElement('li');
dummyfilelistitem.textContent = "(No file in pseudo filesystem)";
this.__filelistElement.appendChild(dummyfilelistitem);
} else {
files.forEach((entry) => {
this.__filelistElement.appendChild(this.__buildFileEntry(entry));
});
}
}
/**
* @param {File} file
*/
__getFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const filereader = new FileReader();
filereader.onerror = (ev) => {
reject(ev);
};
filereader.onload = (ev) => {
resolve(ev.target.result);
};
filereader.readAsArrayBuffer(file);
});
}
/**
*
* @param {File} file
*/
async __tryAddFile(file) {
return new Promise(async (resolve, reject) => {
if (!file) {
reject(new Error(`Invalid arg: file is ${file}`));
} else if (file.size > FileManagerUI_UPLOAD_FILE_SIZE_LIMIT) {
//FIXME(emscripten): Use our MessageDialog instead of browser's alert().
alert(`Specified file is larger than limit of ${FileManagerUI_UPLOAD_FILE_SIZE_LIMIT} bytes. Canceced.`);
reject(new Error(`File is too large: "${file.name} is ${file.size} bytes`));
} else {
// Just add to Filesystem
const path = `${this.__basePathInFilesystem}${file.name}`;
const blobArrayBuffer = await this.__getFileAsArrayBuffer(file);
const u8array = new Uint8Array(blobArrayBuffer);
const fs = FS.open(path, "w");
FS.write(fs, u8array, 0, u8array.length, 0);
FS.close(fs);
resolve();
}
});
}
__addSelectedFile() {
if (this.__fileInputElement.files.length < 1) {
console.warn(`No file selected.`);
return;
}
const file = this.__fileInputElement.files[0];
this.__tryAddFile(file)
.then(() => {
this.__updateFileList();
})
.catch((err) => {
this.__fileInputElement.value = null;
console.error(err);
})
}
/**
* @param {DragEvent} ev
*/
__onFileDragDrop(ev) {
ev.preventDefault();
if (ev.type == "dragenter" || ev.type == "dragover" || ev.type == "dragleave") {
return;
}
if (ev.dataTransfer.files.length < 1) {
return;
}
this.__fileInputElement.files = ev.dataTransfer.files;
this.__addSelectedFile();
}
/**
* @param {InputEvent} _ev
*/
__onFileInputChanged(_ev) {
this.__addSelectedFile();
}
/** Show the FileManager UI dialog */
__show() {
this.__closedWithCancel = false;
/** @type {HTMLElement} */
this.__dialogRootElement = this.buildDialog();
document.querySelector('body').appendChild(this.__dialogRootElement);
this.__dialogHeaderElement.textContent = this.__dialogHeaderText || "File manager";
this.__descriptionElement.textContent = this.__descriptionText || "Select a file.";
if (this.__extension_filters) {
this.__descriptionElement.textContent += "Requested filter is " + this.__extension_filters.join(", ");
}
if (this.__isOpenDialog && this.__extension_filters) {
this.__fileInputElement.accept = this.__extension_filters.concat(',');
}
if (this.__isSaveDialog) {
this.__saveFilenameInputElement.value = this.__defaultFilename;
}
this.__dialogRootElement.style.display = "block";
this.__isShown = true;
}
/** Close the dialog */
__close() {
this.__selectedFilename = "";
if (this.__isOpenDialog) {
Array.from(document.querySelectorAll('input[type="radio"][name="filemanager_filelist"]'))
.forEach((elem) => {
if (elem.checked) {
this.__selectedFilename = elem.parentElement.getAttribute("data-filename");
}
});
} else if (this.__isSaveDialog) {
if (!this.__closedWithCancel) {
this.__selectedFilename = this.__saveFilenameInputElement.value;
}
}
Array.from(this.__filelistElement.children).forEach((elem) => {
this.__filelistElement.removeChild(elem);
});
this.dispose();
this.__isShown = false;
}
/**
* @return {boolean}
*/
isShown() {
return this.__isShown;
}
/**
*
* @returns {Promise} filename string on resolved.
*/
showModalAsync() {
return new Promise((resolve, reject) => {
this.__show();
this.__updateFileList();
const intervalTimer = setInterval(() => {
if (!this.isShown()) {
clearInterval(intervalTimer);
resolve(this.__selectedFilename);
}
}, 50);
});
}
getSelectedFilename() {
return this.__selectedFilename;
}
show() {
this.__show();
this.__updateFileList();
}
};
window.FileManagerUI = FileManagerUI;

View File

@ -0,0 +1,344 @@
* {
font-family: sans;
}
html, body {
padding: 0;
margin: 0;
background: black;
display: flex;
flex-direction: column;
height: 100%;
}
html, body, canvas, #splash, #container {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
body {
overflow: hidden;
}
/* Splashscreen */
#splash {
z-index: 1000;
background: black;
color: white;
position: absolute;
}
#splash .center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
#splash a {
color: white;
}
#spinner {
height: 30px;
width: 30px;
margin: 0px auto;
border-left: 10px solid rgb(255, 255, 255);
border-top: 10px solid rgb(0, 255, 0);
border-right: 10px solid rgb(255, 0, 255);
border-bottom: 10px solid rgb(0, 255, 0);
border-radius: 100%;
animation: rotation 3s linear infinite;
margin-bottom: 5px;
}
@keyframes rotation {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Grid layout for main */
main {
height: 100%;
/* Use CSS Grid layout for vertical placement. */
display: grid;
/* Row 0 for menubar (fit to content), Row 1 for canvas0, canvas1 (rest of space) */
grid-template-rows: auto 1fr;
}
/* Buttons */
.button {
border: 1px solid hsl(0, 0%, 60%);
background: hsl(0, 0%, 10%);
color: white;
padding: 4px 8px;
cursor: default;
}
.button.selected {
background: hsl(0, 0%, 20%);
}
.button:hover {
background: hsl(0, 0%, 40%);
}
/* Editors */
.editor {
position: fixed;
padding: 0;
border: none;
}
/* Menus */
.menu {
font-size: 0;
margin: 0;
padding: 0;
padding-right: 10px;
list-style-type: none;
background: hsl(0, 0%, 20%);
color: white;
cursor: default;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* Normal menu items */
.menu > li {
z-index: 100;
font-size: 16px;
display: inline-flex;
justify-content: space-between;
align-items: center;
white-space: nowrap;
position: relative;
width: 100%;
height: 19px;
margin: 2px;
padding: 3px;
}
.menu > li::before, .menu > li::after {
font-family: 'Font Awesome 5 Free';
font-weight: 900;
font-size: 12px;
}
.menu > li.hover,
.menu > li.selected,
.menu.menubar > li:hover:not(.selected) {
background: hsl(0, 0%, 30%);
}
.menu > li.disabled {
color: hsl(0, 0%, 30%);
}
/* Check and radio menu items */
.menu > li {
padding-left: 24px;
}
.menu > li::before {
position: absolute;
text-align: center;
left: 0px;
width: 24px;
}
.menu > li.check::before {
content: '\f0c8';
}
.menu > li.check.active::before {
content: '\f14a';
}
.menu > li.radio::before {
content: '\f111';
}
.menu > li.radio.active::before {
content: '\f192';
}
/* Separator menu items */
.menu > li.separator {
height: 0px;
border-top: 1px solid hsl(0, 0%, 30%);
margin: 0 2px 0 2px;
padding-top: 0;
padding-bottom: 0;
}
/* Accelerators */
.menu > li > .accel {
text-align: right;
margin-left: 20px;
}
/* Submenus */
.menu > li > .menu,
.menu.popup {
display: none;
white-space: normal;
padding-right: 31px;
}
.menu > li.has-submenu::after {
content: '\f0da';
}
.menu > li.selected > .menu,
.menu > li.hover > .menu,
.menu.popup {
display: block;
background: hsl(0, 0%, 10%);
border: 1px solid hsl(0, 0%, 30%);
position: absolute;
left: 100%;
top: -3px;
}
/* Popup menus */
.menu.popup {
display: block;
position: absolute;
width: min-content;
}
/* Menubars */
.menubar {
padding-left: 5px;
}
.menubar > li {
width: auto;
width: fit-content;
margin: 0;
padding: 5px;
}
.menubar > li.selected {
background: hsl(0, 0%, 10%);
border: 1px solid hsl(0, 0%, 30%);
padding: 4px;
}
.menubar.menu > li.selected > .menu {
display: block;
position: absolute;
left: -1px;
top: 27px;
}
/* Modal popups */
.modal {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: hsla(0, 0%, 0%, 60%);
}
.modal > div {
position: absolute;
top: 15%;
left: 50%;
transform: translate(-50%, 0%);
}
/* Dialogs */
.dialog {
border: 1px solid hsl(0, 0%, 30%);
background: hsl(0, 0%, 10%);
color: white;
padding: 20px;
display: flex;
flex-direction: column;
min-width: 200px;
max-width: 400px;
white-space: pre-wrap;
max-height: 70%;
overflow-y: auto;
}
.dialog.wide {
width: 80%;
max-width: 1200px;
}
.dialog > .buttons {
display: flex;
justify-content: space-around;
}
.dialog .filedrop {
margin: 1em 0 1em 0;
padding: 1em;
border: 2px solid black;
background-color: hsl(0, 0%, 50%);
}
.dialog .filelist {
display: flex;
flex-flow: row wrap;
list-style: none;
margin: 0;
padding: 0;
}
.dialog .filelist li {
padding: 0.2em 0.5em 0.2em 0.5em;
break-inside: avoid;
}
/* Mnemonics */
.label > u {
position: relative;
top: 0px;
text-decoration: none;
}
body.mnemonic .label > u {
border-bottom: 1px solid;
}
/* Canvases */
canvas {
border: 0px none;
background-color: black;
}
#container {
display: flex;
overflow: hidden;
}
/* FIXME(emscripten): this should be dynamically adjustable, not hardcoded in CSS */
#container0 {
flex-basis: 80%;
height: 100%;
position: relative;
overflow: hidden;
}
#container1parent {
flex-basis: 20%;
height: 100%;
position: relative;
overflow: hidden;
min-width: 410px;
display: grid;
grid-template-columns: auto 19px;
grid-template-rows: 100%;
}
#container1 {
height: 100%;
}
#canvas1scrollbarbox {
/* 19px is a magic number for scrollbar width (Yes, this is platform-dependent value but looks almost working.) */
width: 19px;
min-width: 19px;
height: 100%;
overflow-x: hidden;
overflow-y: scroll;
background-color: lightgray;
-webkit-overflow-scrolling: auto;
}
#canvas1scrollbar {
/* 0px will disable the scrollbar by browser. */
width: 1px;
/* Disable scrollbar as default. This value will be overwritten by program. */
height: 100%;
}
#view_separator {
width: 4px;
background: hsl(0, 0%, 20%);
}

View File

@ -0,0 +1,813 @@
function isModal() {
var hasModal = !!document.querySelector('.modal');
var hasMenuBar = !!document.querySelector('.menubar .selected');
var hasPopupMenu = !!document.querySelector('.menu.popup');
return hasModal || hasMenuBar || hasPopupMenu;
}
/* String helpers */
/**
* @param {string} s - original string
* @param {number} digits - char length of generating string
* @param {string} ch - string to be used for padding
* @return {string} generated string ($digits chars length) or $s
*/
function stringPadLeft(s, digits, ch) {
if (s.length > digits) {
return s;
}
for (let i = s.length; i < digits; i++) {
s = ch + s;
}
return s;
}
/** Generate a string expression of now
* @return {string} like a "2022_08_31_2245" string (for 2022-08-31 22:45; local time)
*/
function GetCurrentDateTimeString() {
const now = new Date();
const padLeft2 = (num) => { return stringPadLeft(num.toString(), 2, '0') };
return (`${now.getFullYear()}_${padLeft2(now.getMonth()+1)}_${padLeft2(now.getDate())}` +
`_` + `${padLeft2(now.getHours())}${padLeft2(now.getMinutes())}`);
}
/* CSS helpers */
function hasClass(element, className) {
return element.classList.contains(className);
}
function addClass(element, className) {
element.classList.add(className);
}
function removeClass(element, className) {
element.classList.remove(className);
}
function removeClassFromAllChildren(element, className) {
element.querySelectorAll('.' + className).forEach(function(element) {
removeClass(element, className);
})
}
/* Mnemonic helpers */
function setLabelWithMnemonic(element, labelText) {
var label = document.createElement('span');
addClass(label, 'label');
element.appendChild(label);
var matches = labelText.match('(.*?)&(.)(.*)?');
if(matches) {
label.appendChild(document.createTextNode(matches[1]));
if(matches[2]) {
var mnemonic = document.createElement('u');
mnemonic.innerText = matches[2];
label.appendChild(mnemonic);
addClass(element, 'mnemonic-Key' + matches[2].toUpperCase());
}
if(matches[3]) {
label.appendChild(document.createTextNode(matches[3]));
}
} else {
label.appendChild(document.createTextNode(labelText))
}
}
/** Touchevent helper
* @param {TouchEvent} event
* @return {boolean} true if same element is target of touchstart and touchend
*/
function isSameElementOnTouchstartAndTouchend(event) {
const elementOnTouchStart = event.target;
const elementOnTouchEnd = document.elementFromPoint(event.changedTouches[0].clientX, event.changedTouches[0].clientY);
return elementOnTouchStart == elementOnTouchEnd;
}
/* Button helpers */
function isButton(element) {
return hasClass(element, 'button');
}
/* Button DOM traversal helpers */
function getButton(element) {
if(!element) return;
if(element.tagName == 'U') {
element = element.parentElement;
}
if(hasClass(element, 'label')) {
return getButton(element.parentElement);
} else if(isButton(element)) {
return element;
}
}
/* Button behavior */
window.addEventListener('click', function(event) {
var button = getButton(event.target);
if(button) {
button.dispatchEvent(new Event('trigger'));
}
});
window.addEventListener("touchend", (event) => {
if (!isSameElementOnTouchstartAndTouchend(event)) {
return;
}
const button = getButton(event.target);
if (button) {
button.dispatchEvent(new Event('trigger'));
}
});
window.addEventListener('keydown', function(event) {
var selected = document.querySelector('.button.selected');
if(!selected) return;
var outSelected, newSelected;
if(event.key == 'ArrowRight') {
outSelected = selected;
newSelected = selected.nextElementSibling;
if(!newSelected) {
newSelected = outSelected.parentElement.firstElementChild;
}
} else if(event.key == 'ArrowLeft') {
outSelected = selected;
newSelected = selected.previousElementSibling;
if(!newSelected) {
newSelected = outSelected.parentElement.lastElementChild;
}
} else if(event.key == 'Enter') {
selected.dispatchEvent(new Event('trigger'));
} else if(event.key == 'Escape' && hasClass(selected, 'default')) {
selected.dispatchEvent(new Event('trigger'));
}
if(outSelected) removeClass(outSelected, 'selected');
if(newSelected) addClass(newSelected, 'selected');
event.stopPropagation();
});
/* Editor helpers */
function isEditor(element) {
return hasClass(element, 'editor');
}
/* Editor DOM traversal helpers */
function getEditor(element) {
if(!element) return;
if(isEditor(element)) {
return element;
}
}
/* Editor behavior */
window.addEventListener('keydown', function(event) {
var editor = getEditor(event.target);
if(editor) {
if(event.key == 'Enter') {
editor.dispatchEvent(new Event('trigger'));
} else if(event.key == 'Escape') {
editor.style.display = 'none';
}
event.stopPropagation();
}
}, {capture: true});
/* Menu helpers */
function isMenubar(element) {
return hasClass(element, 'menubar');
}
function isMenu(element) {
return hasClass(element, 'menu');
}
function isPopupMenu(element) {
return isMenu(element) && hasClass(element, 'popup')
}
function hasSubmenu(menuItem) {
return !!menuItem.querySelector('.menu');
}
/* Menu item helpers */
function isMenuItemSelectable(menuItem) {
return !(hasClass(menuItem, 'disabled') || hasClass(menuItem, 'separator'));
}
function isMenuItemSelected(menuItem) {
return hasClass(menuItem, 'selected') || hasClass(menuItem, 'hover');
}
function deselectMenuItem(menuItem) {
removeClass(menuItem, 'selected');
removeClass(menuItem, 'hover');
removeClassFromAllChildren(menuItem, 'selected');
removeClassFromAllChildren(menuItem, 'hover');
}
function selectMenuItem(menuItem) {
var menu = menuItem.parentElement;
removeClassFromAllChildren(menu, 'selected');
removeClassFromAllChildren(menu, 'hover');
if(isMenubar(menu)) {
addClass(menuItem, 'selected');
} else {
addClass(menuItem, 'hover');
}
}
function triggerMenuItem(menuItem) {
selectMenuItem(menuItem);
if(hasSubmenu(menuItem)) {
selectMenuItem(menuItem.querySelector('li:first-child'));
} else {
var parent = menuItem.parentElement;
while(!isMenubar(parent) && !isPopupMenu(parent)) {
parent = parent.parentElement;
}
removeClassFromAllChildren(parent, 'selected');
removeClassFromAllChildren(parent, 'hover');
if(isPopupMenu(parent)) {
parent.remove();
}
menuItem.dispatchEvent(new Event('trigger'));
}
}
/* Menu DOM traversal helpers */
function getMenuItem(element) {
if(!element) return;
if(element.tagName == 'U') {
element = element.parentElement;
}
if(hasClass(element, 'label')) {
return getMenuItem(element.parentElement);
} else if(element.tagName == 'LI' && isMenu(element.parentElement)) {
return element;
}
}
function getMenu(element) {
if(!element) return;
if(isMenu(element)) {
return element;
} else {
var menuItem = getMenuItem(element);
if(menuItem && isMenu(menuItem.parentElement)) {
return menuItem.parentElement;
}
}
}
/* Menu behavior */
window.addEventListener('click', function(event) {
var menuItem = getMenuItem(event.target);
var menu = getMenu(menuItem);
if(menu && isMenubar(menu)) {
if(hasClass(menuItem, 'selected')) {
removeClass(menuItem, 'selected');
} else {
selectMenuItem(menuItem);
}
event.stopPropagation();
} else if(menu) {
if(!hasSubmenu(menuItem)) {
triggerMenuItem(menuItem);
}
event.stopPropagation();
} else {
document.querySelectorAll('.menu .selected, .menu .hover')
.forEach(function(menuItem) {
deselectMenuItem(menuItem);
event.stopPropagation();
});
document.querySelectorAll('.menu.popup')
.forEach(function(menu) {
menu.remove();
});
}
});
window.addEventListener("touchend", (event) => {
if (!isSameElementOnTouchstartAndTouchend(event)) {
return;
}
var menuItem = getMenuItem(event.target);
var menu = getMenu(menuItem);
if(menu && isMenubar(menu)) {
if(hasClass(menuItem, 'selected')) {
removeClass(menuItem, 'selected');
} else {
selectMenuItem(menuItem);
}
event.stopPropagation();
event.preventDefault();
} else if(menu) {
if(!hasSubmenu(menuItem)) {
triggerMenuItem(menuItem);
} else {
addClass(menuItem, "selected");
addClass(menuItem, "hover");
}
event.stopPropagation();
} else {
document.querySelectorAll('.menu .selected, .menu .hover')
.forEach(function(menuItem) {
deselectMenuItem(menuItem);
event.stopPropagation();
});
document.querySelectorAll('.menu.popup')
.forEach(function(menu) {
menu.remove();
});
}
});
window.addEventListener('mouseover', function(event) {
var menuItem = getMenuItem(event.target);
var menu = getMenu(menuItem);
if(menu) {
var selected = menu.querySelectorAll('.selected, .hover');
if(isMenubar(menu)) {
if(selected.length > 0) {
selected.forEach(function(menuItem) {
if(selected != menuItem) {
deselectMenuItem(menuItem);
}
});
addClass(menuItem, 'selected');
}
} else {
if(isMenuItemSelectable(menuItem)) {
selectMenuItem(menuItem);
}
}
}
});
window.addEventListener('keydown', function(event) {
var allSelected = document.querySelectorAll('.menubar .selected, .menubar .hover,' +
'.menu.popup .selected, .menu.popup .hover');
if(allSelected.length == 0) return;
var selected = allSelected[allSelected.length - 1];
var outSelected, newSelected;
var isMenubarItem = isMenubar(getMenu(selected));
if(isMenubarItem && event.key == 'ArrowRight' ||
!isMenubarItem && event.key == 'ArrowDown') {
outSelected = selected;
newSelected = selected.nextElementSibling;
while(newSelected && !isMenuItemSelectable(newSelected)) {
newSelected = newSelected.nextElementSibling;
}
if(!newSelected) {
newSelected = outSelected.parentElement.firstElementChild;
}
} else if(isMenubarItem && event.key == 'ArrowLeft' ||
!isMenubarItem && event.key == 'ArrowUp') {
outSelected = selected;
newSelected = selected.previousElementSibling;
while(newSelected && !isMenuItemSelectable(newSelected)) {
newSelected = newSelected.previousElementSibling;
}
if(!newSelected) {
newSelected = outSelected.parentElement.lastElementChild;
}
} else if(!isMenubarItem && event.key == 'ArrowRight') {
if(hasSubmenu(selected)) {
selectMenuItem(selected.querySelector('li:first-child'));
} else {
outSelected = allSelected[0];
newSelected = outSelected.nextElementSibling;
if(!newSelected) {
newSelected = outSelected.parentElement.firstElementChild;
}
}
} else if(!isMenubarItem && event.key == 'ArrowLeft') {
if(allSelected.length > 2) {
outSelected = selected;
} else {
outSelected = allSelected[0];
newSelected = outSelected.previousElementSibling;
if(!newSelected) {
newSelected = outSelected.parentElement.lastElementChild;
}
}
} else if(isMenubarItem && event.key == 'ArrowDown') {
newSelected = selected.querySelector('li:first-child');
} else if(event.key == 'Enter') {
triggerMenuItem(selected);
} else if(event.key == 'Escape') {
outSelected = allSelected[0];
} else {
var withMnemonic = getMenu(selected).querySelector('.mnemonic-' + event.key);
if(withMnemonic) {
triggerMenuItem(withMnemonic);
}
}
if(outSelected) deselectMenuItem(outSelected);
if(newSelected) selectMenuItem(newSelected);
event.stopPropagation();
});
/* Mnemonic behavior */
window.addEventListener('keydown', function(event) {
var withMnemonic;
if(event.altKey && event.key == 'Alt') {
addClass(document.body, 'mnemonic');
} else if(!isModal() && event.altKey && (withMnemonic =
document.querySelector('.menubar > .mnemonic-' + event.code))) {
triggerMenuItem(withMnemonic);
event.stopPropagation();
} else {
removeClass(document.body, 'mnemonic');
}
});
window.addEventListener('keyup', function(event) {
if(event.key == 'Alt') {
removeClass(document.body, 'mnemonic');
}
});
// FIXME(emscripten): Should be implemented in guihtmlcpp ?
class FileUploadHelper {
constructor() {
this.modalRoot = document.createElement("div");
addClass(this.modalRoot, "modal");
this.modalRoot.style.display = "none";
this.modalRoot.style.zIndex = 1000;
this.dialogRoot = document.createElement("div");
addClass(this.dialogRoot, "dialog");
this.modalRoot.appendChild(this.dialogRoot);
this.messageHeader = document.createElement("strong");
this.dialogRoot.appendChild(this.messageHeader);
this.descriptionParagraph = document.createElement("p");
this.dialogRoot.appendChild(this.descriptionParagraph);
this.currentFileListHeader = document.createElement("p");
this.currentFileListHeader.textContent = "Current uploaded files:";
this.dialogRoot.appendChild(this.currentFileListHeader);
this.currentFileList = document.createElement("div");
this.dialogRoot.appendChild(this.currentFileList);
this.fileInputContainer = document.createElement("div");
this.fileInputElement = document.createElement("input");
this.fileInputElement.setAttribute("type", "file");
this.fileInputElement.addEventListener("change", (ev)=> this.onFileInputChanged(ev));
this.fileInputContainer.appendChild(this.fileInputElement);
this.dialogRoot.appendChild(this.fileInputContainer);
this.buttonHolder = document.createElement("div");
addClass(this.buttonHolder, "buttons");
this.dialogRoot.appendChild(this.buttonHolder);
this.AddButton("OK", 0, false);
this.AddButton("Cancel", 1, true);
this.closeDialog();
document.querySelector("body").appendChild(this.modalRoot);
this.currentFilename = null;
// FIXME(emscripten): For debugging
this.title = "";
this.filename = "";
this.filters = "";
}
dispose() {
document.querySelector("body").removeChild(this.modalRoot);
}
AddButton(label, response, isDefault) {
// FIXME(emscripten): implement
const buttonElem = document.createElement("div");
addClass(buttonElem, "button");
setLabelWithMnemonic(buttonElem, label);
if (isDefault) {
addClass(buttonElem, "default");
addClass(buttonElem, "selected");
}
buttonElem.addEventListener("click", () => {
this.closeDialog();
});
this.buttonHolder.appendChild(buttonElem);
}
getFileEntries() {
const basePath = '/';
/** @type {Array<object} */
const nodes = FS.readdir(basePath);
const files = nodes.filter((nodename) => {
return FS.isFile(FS.lstat(basePath + nodename).mode);
}).map((filename) => {
return basePath + filename;
});
return files;
}
generateFileList() {
let filepaths = this.getFileEntries();
const listElem = document.createElement("ul");
for (let i = 0; i < filepaths.length; i++) {
const listitemElem = document.createElement("li");
const stat = FS.lstat(filepaths[i]);
const text = `"${filepaths[i]}" (${stat.size} bytes)`;
listitemElem.textContent = text;
listElem.appendChild(listitemElem);
}
return listElem;
}
updateFileList() {
this.currentFileList.innerHTML = "";
this.currentFileList.appendChild(this.generateFileList());
}
onFileInputChanged(ev) {
const selectedFiles = ev.target.files;
if (selectedFiles.length < 1) {
return;
}
const selectedFile = selectedFiles[0];
const selectedFilename = selectedFile.name;
this.filename = selectedFilename;
this.currentFilename = selectedFilename;
// Prepare FileReader
const fileReader = new FileReader();
const fileReaderReadAsArrayBufferPromise = new Promise((resolve, reject) => {
fileReader.addEventListener("load", (ev) => {
resolve(ev.target.result);
});
fileReader.addEventListener("abort", (err) => {
reject(err);
});
fileReader.readAsArrayBuffer(selectedFile);
});
fileReaderReadAsArrayBufferPromise
.then((arrayBuffer) => {
// Write selected file to FS
console.log(`Write uploaded file blob to filesystem. "${selectedFilename}" (${arrayBuffer.byteLength} bytes)`);
const u8array = new Uint8Array(arrayBuffer);
const fs = FS.open("/" + selectedFilename, "w");
FS.write(fs, u8array, 0, u8array.length, 0);
FS.close(fs);
// Update file list in dialog
this.updateFileList();
})
.catch((err) => {
console.error("Error while fileReader.readAsArrayBuffer():", err);
});
}
showDialog() {
this.updateFileList();
this.is_shown = true;
this.modalRoot.style.display = "block";
}
closeDialog() {
this.is_shown = false;
this.modalRoot.style.display = "none";
}
};
// FIXME(emscripten): Workaround
function createFileUploadHelperInstance() {
return new FileUploadHelper();
}
// FIXME(emscripten): Should be implemented in guihtmlcpp ?
class FileDownloadHelper {
constructor() {
this.modalRoot = document.createElement("div");
addClass(this.modalRoot, "modal");
this.modalRoot.style.display = "none";
this.modalRoot.style.zIndex = 1000;
this.dialogRoot = document.createElement("div");
addClass(this.dialogRoot, "dialog");
this.modalRoot.appendChild(this.dialogRoot);
this.messageHeader = document.createElement("strong");
this.dialogRoot.appendChild(this.messageHeader);
this.descriptionParagraph = document.createElement("p");
this.dialogRoot.appendChild(this.descriptionParagraph);
this.buttonHolder = document.createElement("div");
addClass(this.buttonHolder, "buttons");
this.dialogRoot.appendChild(this.buttonHolder);
this.closeDialog();
document.querySelector("body").appendChild(this.modalRoot);
}
dispose() {
document.querySelector("body").removeChild(this.modalRoot);
}
AddButton(label, response, isDefault) {
// FIXME(emscripten): implement
const buttonElem = document.createElement("div");
addClass(buttonElem, "button");
setLabelWithMnemonic(buttonElem, label);
if (isDefault) {
addClass(buttonElem, "default");
addClass(buttonElem, "selected");
}
buttonElem.addEventListener("click", () => {
this.closeDialog();
this.dispose();
});
this.buttonHolder.appendChild(buttonElem);
}
createBlobURLFromArrayBuffer(arrayBuffer) {
const u8array = new Uint8Array(arrayBuffer);
let dataUrl = "data:application/octet-stream;base64,";
let binaryString = "";
for (let i = 0; i < u8array.length; i++) {
binaryString += String.fromCharCode(u8array[i]);
}
dataUrl += btoa(binaryString);
return dataUrl;
}
prepareFile(filename) {
this.messageHeader.textContent = "Your file ready";
const stat = FS.lstat(filename);
const filesize = stat.size;
const fs = FS.open(filename, "r");
const readbuffer = new Uint8Array(filesize);
FS.read(fs, readbuffer, 0, filesize, 0);
FS.close(fs);
const blobURL = this.createBlobURLFromArrayBuffer(readbuffer.buffer);
this.descriptionParagraph.innerHTML = "";
const linkElem = document.createElement("a");
//let downloadfilename = "solvespace_browser-";
//downloadfilename += `${GetCurrentDateTimeString()}.slvs`;
let downloadfilename = filename;
linkElem.setAttribute("download", downloadfilename);
linkElem.setAttribute("href", blobURL);
// WORKAROUND: FIXME(emscripten)
linkElem.style.color = "lightblue";
linkElem.textContent = downloadfilename;
this.descriptionParagraph.appendChild(linkElem);
}
showDialog() {
this.is_shown = true;
this.modalRoot.style.display = "block";
}
closeDialog() {
this.is_shown = false;
this.modalRoot.style.display = "none";
}
};
function saveFileDone(filename, isSaveAs, isAutosave) {
console.log(`saveFileDone(${filename}, ${isSaveAs}, ${isAutosave})`);
if (isAutosave) {
return;
}
const fileDownloadHelper = new FileDownloadHelper();
fileDownloadHelper.AddButton("OK", 0, true);
fileDownloadHelper.prepareFile(filename);
console.log(`Calling shoDialog()...`);
fileDownloadHelper.showDialog();
console.log(`shoDialog() finished.`);
}
class ScrollbarHelper {
/**
* @param {HTMLElement} elementquery CSS query string for the element that has scrollbar.
*/
constructor(elementquery) {
this.target = document.querySelector(elementquery);
this.rangeMin = 0;
this.rangeMax = 0;
this.currentRatio = 0;
this.onScrollCallback = null;
this.onScrollCallbackTicking = false;
if (this.target) {
// console.log("addEventListner scroll");
this.target.parentElement.addEventListener('scroll', () => {
if (this.onScrollCallbackTicking) {
return;
}
window.requestAnimationFrame(() => {
if (this.onScrollCallback) {
this.onScrollCallback();
}
this.onScrollCallbackTicking = false;
});
this.onScrollCallbackTicking = true;
});
}
}
/**
*
* @param {number} ratio how long against to the viewport height (1.0 to exact same as viewport's height)
*/
setScrollbarSize(ratio) {
// if (isNaN(ratio)) {
// console.warn(`setScrollbarSize(): ratio is Nan = ${ratio}`);
// }
// if (ratio < 0 || ratio > 1) {
// console.warn(`setScrollbarSize(): ratio is out of range 0-1 but ${ratio}`);
// }
// console.log(`ScrollbarHelper.setScrollbarSize(): ratio=${ratio}`);
this.target.style.height = `${100 * ratio}%`;
}
getScrollbarPosition() {
const scrollbarElem = this.target.parentElement;
const scrollTopMin = 0;
const scrollTopMax = scrollbarElem.scrollHeight - scrollbarElem.clientHeight;
const ratioOnScrollbar = (scrollbarElem.scrollTop - scrollTopMin) / (scrollTopMax - scrollTopMin);
this.currentRatio = (scrollbarElem.scrollTop - scrollTopMin) / (scrollTopMax - scrollTopMin);
let pos = this.currentRatio * (this.rangeMax - this.pageSize - this.rangeMin) + this.rangeMin;
// console.log(`ScrollbarHelper.getScrollbarPosition(): ratio=${ratioOnScrollbar}, pos=${pos}, scrollTop=${scrollbarElem.scrollTop}, scrollTopMin=${scrollTopMin}, scrollTopMax=${scrollTopMax}, rangeMin=${this.rangeMin}, rangeMax=${this.rangeMax}, pageSize=${this.pageSize}`);
if (isNaN(pos)) {
return 0;
} else {
return pos;
}
}
/**
* @param {number} value in range of rangeMin and rangeMax
*/
setScrollbarPosition(position) {
const positionMin = this.rangeMin;
const positionMax = this.rangeMax - this.pageSize;
const currentPositionRatio = (position - positionMin) / (positionMax - positionMin);
const scrollbarElement = this.target.parentElement;
const scrollTopMin = 0;
const scrollTopMax = scrollbarElement.scrollHeight - scrollbarElement.clientHeight;
const scrollWidth = scrollTopMax - scrollTopMin;
const newScrollTop = currentPositionRatio * scrollWidth;
scrollbarElement.scrollTop = currentPositionRatio * scrollWidth;
// console.log(`ScrollbarHelper.setScrollbarPosition(): pos=${position}, currentPositionRatio=${currentPositionRatio}, calculated scrollTop=${newScrollTop}`);
if (false) {
// const ratio = (position - this.rangeMin) * ((this.rangeMax - this.pageSize) - this.rangeMin);
const scrollTopMin = 0;
const scrollTopMax = this.target.scrollHeight - this.target.clientHeight;
const scrollWidth = scrollTopMax - scrollTopMin;
const newScrollTop = ratio * scrollWidth;
// this.target.parentElement.scrollTop = ratio * scrollWidth;
this.target.scrollTop = ratio * scrollWidth;
console.log(`ScrollbarHelper.setScrollbarPosition(): pos=${position}, ratio=${ratio}, calculated scrollTop=${newScrollTop}`);
}
}
/** */
setRange(min, max, pageSize) {
this.rangeMin = min;
this.rangeMax = max;
this.currentRatio = 0;
this.setPageSize(pageSize);
}
setPageSize(pageSize) {
if (this.rangeMin == this.rangeMax) {
// console.log(`ScrollbarHelper::setPageSize(): size=${size}, but rangeMin == rangeMax`);
return;
}
this.pageSize = pageSize;
const ratio = (this.rangeMax - this.rangeMin) / this.pageSize;
// console.log(`ScrollbarHelper::setPageSize(): pageSize=${pageSize}, ratio=${ratio}`);
this.setScrollbarSize(ratio);
}
setScrollbarEnabled(enabled) {
if (!enabled) {
this.target.style.height = "100%";
}
}
};
window.ScrollbarHelper = ScrollbarHelper;

View File

@ -93,9 +93,7 @@ static std::vector<std::string> Split(const std::string &joined, char separator)
pos += 1;
}
if(oldpos != joined.length() - 1) {
parts.push_back(joined.substr(oldpos));
}
parts.push_back(joined.substr(oldpos));
return parts;
}
@ -239,7 +237,8 @@ Path Path::Parent() const {
}
// Concatenates a component to this path.
// Returns an empty path if this path or the component is empty.
// Returns a relative path if this path is empty.
// Returns an empty path if the component is absolute.
Path Path::Join(const std::string &component) const {
ssassert(component.find(SEPARATOR) == std::string::npos,
"Use the Path::Join(const Path &) overload to append an entire path");
@ -247,13 +246,20 @@ Path Path::Join(const std::string &component) const {
}
// Concatenates a relative path to this path.
// Returns an empty path if either path is empty, or the other path is absolute.
// Returns a relative path if this path is empty.
// Returns an empty path if the other path is absolute.
Path Path::Join(const Path &other) const {
if(IsEmpty() || other.IsEmpty() || other.IsAbsolute()) {
if(other.IsAbsolute()) {
return From("");
}
Path joined = { raw };
Path joined;
if(IsEmpty()) {
joined.raw = ".";
} else {
joined.raw = raw;
}
if(joined.raw.back() != SEPARATOR) {
joined.raw += SEPARATOR;
}
@ -321,16 +327,15 @@ static std::string FilesystemNormalize(const std::string &str) {
std::transform(strW.begin(), strW.end(), strW.begin(), towlower);
return Narrow(strW);
#elif defined(__APPLE__)
CFStringRef cfStr = CFStringCreateWithBytesNoCopy(NULL, (const UInt8*)str.data(), str.size(),
kCFStringEncodingUTF8, /*isExternalRepresentation=*/false, kCFAllocatorNull);
CFMutableStringRef cfmStr = CFStringCreateMutableCopy(NULL, 0, cfStr);
CFStringLowercase(cfmStr, NULL);
CFMutableStringRef cfStr =
CFStringCreateMutableCopy(NULL, 0,
CFStringCreateWithBytesNoCopy(NULL, (const UInt8*)str.data(), str.size(),
kCFStringEncodingUTF8, /*isExternalRepresentation=*/false, kCFAllocatorNull));
CFStringLowercase(cfStr, NULL);
std::string normalizedStr;
normalizedStr.resize(CFStringGetMaximumSizeOfFileSystemRepresentation(cfmStr));
CFStringGetFileSystemRepresentation(cfmStr, &normalizedStr[0], normalizedStr.size());
normalizedStr.resize(CFStringGetMaximumSizeOfFileSystemRepresentation(cfStr));
CFStringGetFileSystemRepresentation(cfStr, &normalizedStr[0], normalizedStr.size());
normalizedStr.erase(normalizedStr.find('\0'));
CFRelease(cfmStr);
CFRelease(cfStr);
return normalizedStr;
#else
return str;
@ -517,6 +522,12 @@ static Platform::Path ResourcePath(const std::string &name) {
return path;
}
#elif defined(__EMSCRIPTEN__)
static Platform::Path ResourcePath(const std::string &name) {
return Path::From("res/" + name);
}
#elif !defined(WIN32)
# if defined(__linux__)
@ -640,6 +651,12 @@ std::vector<std::string> InitCli(int argc, char **argv) {
#if defined(WIN32)
#if !defined(_alloca)
// Fix for compiling with MinGW.org GCC-6.3.0-1
#define _alloca alloca
#include <malloc.h>
#endif
void DebugPrint(const char *fmt, ...)
{
va_list va;

View File

@ -7,6 +7,7 @@
#ifndef SOLVESPACE_PLATFORM_H
#define SOLVESPACE_PLATFORM_H
namespace SolveSpace {
namespace Platform {
// UTF-8 ⟷ UTF-16 conversion, for Windows.
@ -80,6 +81,7 @@ void DebugPrint(const char *fmt, ...);
void *AllocTemporary(size_t size);
void FreeAllTemporary();
}
} // namespace Platform
} // namespace SolveSpace
#endif

View File

@ -6,7 +6,7 @@
#ifndef SOLVESPACE_GL3SHADER_H
#define SOLVESPACE_GL3SHADER_H
#if defined(WIN32)
#if defined(WIN32) || defined(__EMSCRIPTEN__)
# define GL_APICALL /*static linkage*/
# define GL_GLEXT_PROTOTYPES
# include <GLES2/gl2.h>

View File

@ -90,7 +90,8 @@ void Request::Generate(IdList<Entity,hEntity> *entity,
// Request-specific generation.
switch(type) {
case Type::TTF_TEXT: {
double actualAspectRatio = SS.fonts.AspectRatio(font, str);
// `extraPoints` is storing kerning boolean
double actualAspectRatio = SS.fonts.AspectRatio(font, str, extraPoints);
if(EXACT(actualAspectRatio != 0.0)) {
// We could load the font, so use the actual value.
aspectRatio = actualAspectRatio;

View File

@ -179,7 +179,8 @@ void Pixmap::ConvertTo(Format newFormat) {
static std::shared_ptr<Pixmap> ReadPngIntoPixmap(png_struct *png_ptr, png_info *info_ptr,
bool flip) {
png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_EXPAND | PNG_TRANSFORM_GRAY_TO_RGB, NULL);
png_read_png(png_ptr, info_ptr,
PNG_TRANSFORM_EXPAND | PNG_TRANSFORM_GRAY_TO_RGB | PNG_TRANSFORM_SCALE_16, NULL);
std::shared_ptr<Pixmap> pixmap = std::make_shared<Pixmap>();
pixmap->width = png_get_image_width(png_ptr, info_ptr);
@ -272,8 +273,8 @@ std::shared_ptr<Pixmap> Pixmap::ReadPng(const Platform::Path &filename, bool fli
}
bool Pixmap::WritePng(FILE *f, bool flip) {
int colorType = 0;
bool bgr = false;
colorType = 0;
bgr = false;
switch(format) {
case Format::RGBA: colorType = PNG_COLOR_TYPE_RGBA; bgr = false; break;
case Format::BGRA: colorType = PNG_COLOR_TYPE_RGBA; bgr = true; break;
@ -564,7 +565,7 @@ const BitmapFont::Glyph &BitmapFont::GetGlyph(char32_t codepoint) {
// Find the hex representation in the (sorted) Unifont file.
auto first = unifontData.cbegin(),
last = unifontData.cend();
while(first <= last) {
while(first < last) {
auto mid = first + (last - first) / 2;
while(mid != unifontData.cbegin()) {
if(*mid == '\n') {
@ -588,7 +589,10 @@ const BitmapFont::Glyph &BitmapFont::GetGlyph(char32_t codepoint) {
if(foundCodepoint < codepoint) {
first = mid + 1;
while(first != unifontData.cend()) {
if(*first == '\n') break;
if(*first == '\n') {
first++;
break;
}
first++;
}
continue; // and last stays the same

View File

@ -7,11 +7,23 @@
#ifndef SOLVESPACE_RESOURCE_H
#define SOLVESPACE_RESOURCE_H
#include <functional>
#include <map>
#include <memory>
#include <stdint.h>
#include <string>
#include <vector>
namespace SolveSpace {
class Camera;
class Point2d;
class Pixmap;
class Vector;
class RgbaColor;
namespace Platform {
class Path;
} // namespace Platform
std::string LoadString(const std::string &name);
std::string LoadStringFromGzip(const std::string &name);
@ -26,6 +38,8 @@ public:
size_t height;
size_t stride;
std::vector<uint8_t> data;
int colorType;
bool bgr;
static std::shared_ptr<Pixmap> Create(Format format, size_t width, size_t height);
static std::shared_ptr<Pixmap> FromPng(const uint8_t *data, size_t size, bool flip = false);
@ -109,4 +123,5 @@ public:
const std::function<void(Vector, Vector)> &traceEdge, const Camera &camera);
};
}
#endif

View File

@ -175,6 +175,7 @@ public:
bool suppress;
bool relaxConstraints;
bool allowRedundant;
bool suppressDofCalculation;
bool allDimsReference;
double scale;
@ -198,6 +199,9 @@ public:
// For drawings in 2d
WORKPLANE_BY_POINT_ORTHO = 6000,
WORKPLANE_BY_LINE_SEGMENTS = 6001,
WORKPLANE_BY_POINT_NORMAL = 6002,
//WORKPLANE_BY_POINT_FACE = 6003,
//WORKPLANE_BY_FACE = 6004,
// For extrudes, translates, and rotates
ONE_SIDED = 7000,
TWO_SIDED = 7001
@ -266,6 +270,7 @@ public:
void Generate(EntityList *entity, ParamList *param);
bool IsSolvedOkay();
void TransformImportedBy(Vector t, Quaternion q);
bool IsTriangleMeshAssembly() const;
bool IsForcedToMeshBySource() const;
bool IsForcedToMesh() const;
// When a request generates entities from entities, and the source
@ -323,6 +328,7 @@ public:
void DrawPolyError(Canvas *canvas);
void DrawFilledPaths(Canvas *canvas);
void DrawContourAreaLabels(Canvas *canvas);
bool ShouldDrawExploded() const;
SPolygon GetPolygon();
@ -368,6 +374,7 @@ public:
std::string font;
Platform::Path file;
double aspectRatio;
int groupRequestIndex;
static hParam AddParam(ParamList *param, hParam hp);
void Generate(EntityList *entity, ParamList *param);
@ -591,6 +598,10 @@ public:
beziers.l.Clear();
edges.l.Clear();
}
bool ShouldDrawExploded() const;
Vector ExplodeOffset() const;
Vector PointGetDrawNum() const;
};
class EntReqTable {
@ -612,7 +623,7 @@ public:
bool free;
// Used only in the solver
hParam substd;
Param *substd;
static const hParam NO_PARAM;
@ -673,7 +684,10 @@ public:
CURVE_CURVE_TANGENT = 125,
EQUAL_RADIUS = 130,
WHERE_DRAGGED = 200,
ARC_ARC_LEN_RATIO = 210,
ARC_LINE_LEN_RATIO = 211,
ARC_ARC_DIFFERENCE = 212,
ARC_LINE_DIFFERENCE = 213,
COMMENT = 1000
};
@ -757,7 +771,7 @@ public:
Vector p0, Vector p1, Vector pt, double salient);
void DoArcForAngle(Canvas *canvas, Canvas::hStroke hcs,
Vector a0, Vector da, Vector b0, Vector db,
Vector offset, Vector *ref, bool trim);
Vector offset, Vector *ref, bool trim, Vector explodeOffset);
void DoArrow(Canvas *canvas, Canvas::hStroke hcs,
Vector p, Vector dir, Vector n, double width, double angle, double da);
void DoLineWithArrows(Canvas *canvas, Canvas::hStroke hcs,
@ -779,6 +793,8 @@ public:
std::string DescriptionString() const;
bool ShouldDrawExploded() const;
static hConstraint AddConstraint(Constraint *c, bool rememberForUndo = true);
static void MenuConstrain(Command id);
static void DeleteAllConstraintsFor(Constraint::Type type, hEntity entityA, hEntity ptA);

View File

@ -19,6 +19,7 @@ void SolveSpaceUI::Init() {
Platform::SettingsRef settings = Platform::GetSettings();
SS.tangentArcRadius = 10.0;
SS.explodeDistance = 1.0;
// Then, load the registry settings.
// Default list of colors for the model material
@ -68,12 +69,18 @@ void SolveSpaceUI::Init() {
exportScale = settings->ThawFloat("ExportScale", 1.0);
// Export offset (cutter radius comp)
exportOffset = settings->ThawFloat("ExportOffset", 0.0);
// Dimensions on arcs default to diameter vs radius
arcDimDefaultDiameter = settings->ThawBool("ArcDimDefaultDiameter", false);
// Show full file path in the menu bar
showFullFilePath = settings->ThawBool("ShowFullFilePath", true);
// Rewrite exported colors close to white into black (assuming white bg)
fixExportColors = settings->ThawBool("FixExportColors", true);
// Export background color
exportBackgroundColor = settings->ThawBool("ExportBackgroundColor", false);
// Draw back faces of triangles (when mesh is leaky/self-intersecting)
drawBackFaces = settings->ThawBool("DrawBackFaces", true);
// Use camera mouse navigation
cameraNav = settings->ThawBool("CameraNav", false);
// Use turntable mouse navigation
turntableNav = settings->ThawBool("TurntableNav", false);
// Immediately edit dimension
@ -104,6 +111,7 @@ void SolveSpaceUI::Init() {
exportCanvas.dy = settings->ThawFloat("ExportCanvas_Dy", 5.0);
// Extra parameters when exporting G code
gCode.depth = settings->ThawFloat("GCode_Depth", 10.0);
gCode.safeHeight = settings->ThawFloat("GCode_SafeHeight", 5.0);
gCode.passes = settings->ThawInt("GCode_Passes", 1);
gCode.feed = settings->ThawFloat("GCode_Feed", 10.0);
gCode.plungeFeed = settings->ThawFloat("GCode_PlungeFeed", 10.0);
@ -123,12 +131,8 @@ void SolveSpaceUI::Init() {
SetLocale(locale);
}
generateAllTimer = Platform::CreateTimer();
generateAllTimer->onTimeout = std::bind(&SolveSpaceUI::GenerateAll, &SS, Generate::DIRTY,
/*andFindFree=*/false, /*genForBBox=*/false);
showTWTimer = Platform::CreateTimer();
showTWTimer->onTimeout = std::bind(&TextWindow::Show, &TW);
refreshTimer = Platform::CreateTimer();
refreshTimer->onTimeout = std::bind(&SolveSpaceUI::Refresh, &SS);
autosaveTimer = Platform::CreateTimer();
autosaveTimer->onTimeout = std::bind(&SolveSpaceUI::Autosave, &SS);
@ -250,6 +254,10 @@ void SolveSpaceUI::Exit() {
settings->FreezeFloat("ExportScale", exportScale);
// Export offset (cutter radius comp)
settings->FreezeFloat("ExportOffset", exportOffset);
// Rewrite the default arc dimension setting
settings->FreezeBool("ArcDimDefaultDiameter", arcDimDefaultDiameter);
// Show full file path in the menu bar
settings->FreezeBool("ShowFullFilePath", showFullFilePath);
// Rewrite exported colors close to white into black (assuming white bg)
settings->FreezeBool("FixExportColors", fixExportColors);
// Export background color
@ -260,6 +268,8 @@ void SolveSpaceUI::Exit() {
settings->FreezeBool("ShowContourAreas", showContourAreas);
// Check that contours are closed and not self-intersecting
settings->FreezeBool("CheckClosedContour", checkClosedContour);
// Use camera mouse navigation
settings->FreezeBool("CameraNav", cameraNav);
// Use turntable mouse navigation
settings->FreezeBool("TurntableNav", turntableNav);
// Immediately edit dimensions
@ -300,12 +310,28 @@ void SolveSpaceUI::Exit() {
Platform::ExitGui();
}
void SolveSpaceUI::Refresh() {
// generateAll must happen bfore updating displays
if(scheduledGenerateAll) {
// Clear the flag so that if the call to GenerateAll is blocked by a Message or Error,
// subsequent refreshes do not try to Generate again.
scheduledGenerateAll = false;
GenerateAll(Generate::DIRTY, /*andFindFree=*/false, /*genForBBox=*/false);
}
if(scheduledShowTW) {
scheduledShowTW = false;
TW.Show();
}
}
void SolveSpaceUI::ScheduleGenerateAll() {
generateAllTimer->RunAfterProcessingEvents();
scheduledGenerateAll = true;
refreshTimer->RunAfterProcessingEvents();
}
void SolveSpaceUI::ScheduleShowTW() {
showTWTimer->RunAfterProcessingEvents();
scheduledShowTW = true;
refreshTimer->RunAfterProcessingEvents();
}
void SolveSpaceUI::ScheduleAutosave() {
@ -315,6 +341,7 @@ void SolveSpaceUI::ScheduleAutosave() {
double SolveSpaceUI::MmPerUnit() {
switch(viewUnits) {
case Unit::INCHES: return 25.4;
case Unit::FEET_INCHES: return 25.4; // The 'unit' is still inches
case Unit::METERS: return 1000.0;
case Unit::MM: return 1.0;
}
@ -323,14 +350,47 @@ double SolveSpaceUI::MmPerUnit() {
const char *SolveSpaceUI::UnitName() {
switch(viewUnits) {
case Unit::INCHES: return "in";
case Unit::FEET_INCHES: return "in";
case Unit::METERS: return "m";
case Unit::MM: return "mm";
}
return "";
}
std::string SolveSpaceUI::MmToString(double v) {
std::string SolveSpaceUI::MmToString(double v, bool editable) {
v /= MmPerUnit();
// The syntax 2' 6" for feet and inches is not something we can (currently)
// parse back from a string so if editable is true, we treat FEET_INCHES the
// same as INCHES and just return the unadorned decimal number of inches.
if(viewUnits == Unit::FEET_INCHES && !editable) {
// Now convert v from inches to 64'ths of an inch, to make rounding easier.
v = floor((v + (1.0 / 128.0)) * 64.0);
int feet = (int)(v / (12.0 * 64.0));
v = v - (feet * 12.0 * 64.0);
// v is now the feet-less remainder in 1/64 inches
int inches = (int)(v / 64.0);
int numerator = (int)(v - ((double)inches * 64.0));
int denominator = 64;
// Divide down to smallest denominator where the numerator is still a whole number
while ((numerator != 0) && ((numerator & 1) == 0)) {
numerator /= 2;
denominator /= 2;
}
std::ostringstream str;
if(feet != 0) {
str << feet << "'-";
}
// For something like 0.5, show 1/2" rather than 0 1/2"
if(!(feet == 0 && inches == 0 && numerator != 0)) {
str << inches;
}
if(numerator != 0) {
str << " " << numerator << "/" << denominator;
}
str << "\"";
return str.str();
}
int digits = UnitDigitsAfterDecimal();
double minimum = 0.5 * pow(10,-digits);
while ((v < minimum) && (v > LENGTH_EPS)) {
@ -349,7 +409,7 @@ static const char *DimToString(int dim) {
}
static std::pair<int, std::string> SelectSIPrefixMm(int ord, int dim) {
// decide what units to use depending on the order of magnitude of the
// measure in meters and the dimmension (1,2,3 lenear, area, volume)
// measure in meters and the dimension (1,2,3 lenear, area, volume)
switch(dim) {
case 0:
case 1:
@ -394,17 +454,22 @@ std::string SolveSpaceUI::MmToStringSI(double v, int dim) {
dim = 1;
}
v /= pow((viewUnits == Unit::INCHES) ? 25.4 : 1000, dim);
bool inches = (viewUnits == Unit::INCHES) || (viewUnits == Unit::FEET_INCHES);
v /= pow(inches ? 25.4 : 1000, dim);
int vdeg = (int)(log10(fabs(v)));
std::string unit;
if(fabs(v) > 0.0) {
int sdeg = 0;
std::tie(sdeg, unit) =
(viewUnits == Unit::INCHES)
inches
? SelectSIPrefixInch(vdeg/dim)
: SelectSIPrefixMm(vdeg, dim);
v /= pow(10.0, sdeg * dim);
}
if(viewUnits == Unit::FEET_INCHES && fabs(v) > pow(12.0, dim)) {
unit = "ft";
v /= pow(12.0, dim);
}
int pdeg = (int)ceil(log10(fabs(v) + 1e-10));
return ssprintf("%.*g%s%s%s", pdeg + UnitDigitsAfterDecimal(), v,
compact ? "" : " ", unit.c_str(), DimToString(dim));
@ -434,10 +499,11 @@ int SolveSpaceUI::GetMaxSegments() {
return maxSegments;
}
int SolveSpaceUI::UnitDigitsAfterDecimal() {
return (viewUnits == Unit::INCHES) ? afterDecimalInch : afterDecimalMm;
return (viewUnits == Unit::INCHES || viewUnits == Unit::FEET_INCHES) ?
afterDecimalInch : afterDecimalMm;
}
void SolveSpaceUI::SetUnitDigitsAfterDecimal(int v) {
if(viewUnits == Unit::INCHES) {
if(viewUnits == Unit::INCHES || viewUnits == Unit::FEET_INCHES) {
afterDecimalInch = v;
} else {
afterDecimalMm = v;
@ -508,12 +574,18 @@ bool SolveSpaceUI::GetFilenameAndSave(bool saveAs) {
if(saveAs || saveFile.IsEmpty()) {
Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(GW.window);
// FIXME(emscripten):
dbp("Calling AddFilter()...");
dialog->AddFilter(C_("file-type", "SolveSpace models"), { SKETCH_EXT });
dbp("Calling ThawChoices()...");
dialog->ThawChoices(settings, "Sketch");
if(!newSaveFile.IsEmpty()) {
dbp("Calling SetFilename()...");
dialog->SetFilename(newSaveFile);
}
dbp("Calling RunModal()...");
if(dialog->RunModal()) {
dbp("Calling FreezeChoices()...");
dialog->FreezeChoices(settings, "Sketch");
newSaveFile = dialog->GetFilename();
} else {
@ -526,6 +598,9 @@ bool SolveSpaceUI::GetFilenameAndSave(bool saveAs) {
RemoveAutosave();
saveFile = newSaveFile;
unsaved = false;
if (this->OnSaveFinished) {
this->OnSaveFinished(newSaveFile, saveAs, false);
}
return true;
} else {
return false;
@ -537,7 +612,11 @@ void SolveSpaceUI::Autosave()
ScheduleAutosave();
if(!saveFile.IsEmpty() && unsaved) {
SaveToFile(saveFile.WithExtension(BACKUP_EXT));
Platform::Path saveFileName = saveFile.WithExtension(BACKUP_EXT);
SaveToFile(saveFileName);
if (this->OnSaveFinished) {
this->OnSaveFinished(saveFileName, false, true);
}
}
}
@ -589,7 +668,11 @@ void SolveSpaceUI::UpdateWindowTitles() {
GW.window->SetTitle(C_("title", "(new sketch)"));
} else {
if(!GW.window->SetTitleForFilename(saveFile)) {
GW.window->SetTitle(saveFile.raw);
if(SS.showFullFilePath) {
GW.window->SetTitle(saveFile.raw);
} else {
GW.window->SetTitle(saveFile.raw.substr(saveFile.raw.find_last_of("/\\") + 1));
}
}
}
@ -637,6 +720,9 @@ void SolveSpaceUI::MenuFile(Command id) {
if(dialog->RunModal()) {
dialog->FreezeChoices(settings, "ExportImage");
SS.ExportAsPngTo(dialog->GetFilename());
if (SS.OnSaveFinished) {
SS.OnSaveFinished(dialog->GetFilename(), false, false);
}
}
break;
}
@ -651,10 +737,8 @@ void SolveSpaceUI::MenuFile(Command id) {
// If the user is exporting something where it would be
// inappropriate to include the constraints, then warn.
if(SS.GW.showConstraints &&
(dialog->GetFilename().HasExtension("txt") ||
fabs(SS.exportOffset) > LENGTH_EPS))
{
if(SS.GW.showConstraints != GraphicsWindow::ShowConstraintMode::SCM_NOSHOW &&
(dialog->GetFilename().HasExtension("txt") || fabs(SS.exportOffset) > LENGTH_EPS)) {
Message(_("Constraints are currently shown, and will be exported "
"in the toolpath. This is probably not what you want; "
"hide them by clicking the link at the top of the "
@ -662,6 +746,9 @@ void SolveSpaceUI::MenuFile(Command id) {
}
SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe=*/false);
if (SS.OnSaveFinished) {
SS.OnSaveFinished(dialog->GetFilename(), false, false);
}
break;
}
@ -674,6 +761,9 @@ void SolveSpaceUI::MenuFile(Command id) {
dialog->FreezeChoices(settings, "ExportWireframe");
SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe*/true);
if (SS.OnSaveFinished) {
SS.OnSaveFinished(dialog->GetFilename(), false, false);
}
break;
}
@ -686,6 +776,9 @@ void SolveSpaceUI::MenuFile(Command id) {
dialog->FreezeChoices(settings, "ExportSection");
SS.ExportSectionTo(dialog->GetFilename());
if (SS.OnSaveFinished) {
SS.OnSaveFinished(dialog->GetFilename(), false, false);
}
break;
}
@ -698,6 +791,10 @@ void SolveSpaceUI::MenuFile(Command id) {
dialog->FreezeChoices(settings, "ExportMesh");
SS.ExportMeshTo(dialog->GetFilename());
if (SS.OnSaveFinished) {
SS.OnSaveFinished(dialog->GetFilename(), false, false);
}
break;
}
@ -711,6 +808,9 @@ void SolveSpaceUI::MenuFile(Command id) {
StepFileWriter sfw = {};
sfw.ExportSurfacesTo(dialog->GetFilename());
if (SS.OnSaveFinished) {
SS.OnSaveFinished(dialog->GetFilename(), false, false);
}
break;
}
@ -764,7 +864,11 @@ void SolveSpaceUI::MenuAnalyze(Command id) {
SS.TW.stepDim.isDistance =
(c->type != Constraint::Type::ANGLE) &&
(c->type != Constraint::Type::LENGTH_RATIO) &&
(c->type != Constraint::Type::LENGTH_DIFFERENCE);
(c->type != Constraint::Type::ARC_ARC_LEN_RATIO) &&
(c->type != Constraint::Type::ARC_LINE_LEN_RATIO) &&
(c->type != Constraint::Type::LENGTH_DIFFERENCE) &&
(c->type != Constraint::Type::ARC_ARC_DIFFERENCE) &&
(c->type != Constraint::Type::ARC_LINE_DIFFERENCE) ;
SS.TW.shown.constraint = c->h;
SS.TW.shown.screen = TextWindow::Screen::STEP_DIMENSION;
@ -1007,7 +1111,11 @@ void SolveSpaceUI::MenuHelp(Command id) {
"law. For details, visit http://gnu.org/licenses/\n"
"\n"
"© 2008-%d Jonathan Westhues and other authors.\n"),
PACKAGE_VERSION, 2021);
PACKAGE_VERSION, 2024);
break;
case Command::GITHUB:
Platform::OpenInBrowser(GIT_HASH_URL);
break;
default: ssassert(false, "Unexpected menu ID");
@ -1026,12 +1134,14 @@ void SolveSpaceUI::Clear() {
GW.showGridMenuItem = NULL;
GW.dimSolidModelMenuItem = NULL;
GW.perspectiveProjMenuItem = NULL;
GW.explodeMenuItem = NULL;
GW.showToolbarMenuItem = NULL;
GW.showTextWndMenuItem = NULL;
GW.fullScreenMenuItem = NULL;
GW.unitsMmMenuItem = NULL;
GW.unitsMetersMenuItem = NULL;
GW.unitsInchesMenuItem = NULL;
GW.unitsFeetInchesMenuItem = NULL;
GW.inWorkplaneMenuItem = NULL;
GW.in3dMenuItem = NULL;
GW.undoMenuItem = NULL;

View File

@ -7,6 +7,10 @@
#ifndef SOLVESPACE_H
#define SOLVESPACE_H
#include "resource.h"
#include "platform/platform.h"
#include "platform/gui.h"
#include <cctype>
#include <climits>
#include <cmath>
@ -30,6 +34,10 @@
#include <unordered_set>
#include <vector>
#define EIGEN_NO_DEBUG
#undef Success
#include <Eigen/SparseCore>
// We declare these in advance instead of simply using FT_Library
// (defined as typedef FT_LibraryRec_* FT_Library) because including
// freetype.h invokes indescribable horrors and we would like to avoid
@ -122,9 +130,6 @@ static constexpr double LENGTH_EPS = 1e-6;
static constexpr double VERY_POSITIVE = 1e10;
static constexpr double VERY_NEGATIVE = -1e10;
#include "platform/platform.h"
#include "platform/gui.h"
#include "resource.h"
using Platform::AllocTemporary;
using Platform::FreeAllTemporary;
@ -138,7 +143,8 @@ enum class Command : uint32_t;
enum class Unit : uint32_t {
MM = 0,
INCHES,
METERS
METERS,
FEET_INCHES
};
template<class Key, class T>
@ -209,7 +215,7 @@ void Error(const char *fmt, ...);
class System {
public:
enum { MAX_UNKNOWNS = 1024 };
enum { MAX_UNKNOWNS = 2048 };
EntityList entity;
ParamList param;
@ -231,37 +237,34 @@ public:
// The system Jacobian matrix
struct {
// The corresponding equation for each row
hEquation eq[MAX_UNKNOWNS];
std::vector<Equation *> eq;
// The corresponding parameter for each column
hParam param[MAX_UNKNOWNS];
std::vector<hParam> param;
// We're solving AX = B
int m, n;
struct {
Expr *sym[MAX_UNKNOWNS][MAX_UNKNOWNS];
double num[MAX_UNKNOWNS][MAX_UNKNOWNS];
} A;
// This only observes the Expr - does not own them!
Eigen::SparseMatrix<Expr *> sym;
Eigen::SparseMatrix<double> num;
} A;
double scale[MAX_UNKNOWNS];
// Some helpers for the least squares solve
double AAt[MAX_UNKNOWNS][MAX_UNKNOWNS];
double Z[MAX_UNKNOWNS];
double X[MAX_UNKNOWNS];
Eigen::VectorXd scale;
Eigen::VectorXd X;
struct {
Expr *sym[MAX_UNKNOWNS];
double num[MAX_UNKNOWNS];
} B;
// This only observes the Expr - does not own them!
std::vector<Expr *> sym;
Eigen::VectorXd num;
} B;
} mat;
static const double RANK_MAG_TOLERANCE, CONVERGE_TOLERANCE;
static const double CONVERGE_TOLERANCE;
int CalculateRank();
bool TestRank(int *rank = NULL);
static bool SolveLinearSystem(double X[], double A[][MAX_UNKNOWNS],
double B[], int N);
bool TestRank(int *dof = NULL);
static bool SolveLinearSystem(const Eigen::SparseMatrix<double> &A,
const Eigen::VectorXd &B, Eigen::VectorXd *X);
bool SolveLeastSquares();
bool WriteJacobian(int tag);
@ -277,7 +280,6 @@ public:
bool NewtonSolve(int tag);
void MarkParamsFree(bool findFree);
int CalculateDof();
SolveResult Solve(Group *g, int *rank = NULL, int *dof = NULL,
List<hConstraint> *bad = NULL,
@ -289,6 +291,9 @@ public:
bool andFindBad = false, bool andFindFree = false);
void Clear();
Param *GetLastParamSubstitution(Param *p);
void SubstituteParamsByLast(Expr *e);
void SortSubstitutionByDragged(Param *p);
};
#include "ttf.h"
@ -568,11 +573,14 @@ public:
double gridSpacing;
double exportScale;
double exportOffset;
bool arcDimDefaultDiameter;
bool showFullFilePath;
bool fixExportColors;
bool exportBackgroundColor;
bool drawBackFaces;
bool showContourAreas;
bool checkClosedContour;
bool cameraNav;
bool turntableNav;
bool immediatelyEditDimension;
bool automaticLineConstraints;
@ -597,6 +605,7 @@ public:
} exportCanvas;
struct {
double depth;
double safeHeight;
int passes;
double feed;
double plungeFeed;
@ -608,8 +617,10 @@ public:
int afterDecimalDegree;
bool useSIPrefixes;
int autosaveInterval; // in minutes
bool explode;
double explodeDistance;
std::string MmToString(double v);
std::string MmToString(double v, bool editable=false);
std::string MmToStringSI(double v, int dim = 0);
std::string DegreeToString(double v);
double ExprToMm(Expr *e);
@ -675,6 +686,7 @@ public:
void NewFile();
bool SaveToFile(const Platform::Path &filename);
bool LoadAutosaveFor(const Platform::Path &filename);
std::function<void(const Platform::Path &filename, bool is_saveAs, bool is_autosave)> OnSaveFinished;
bool LoadFromFile(const Platform::Path &filename, bool canCancel = false);
void UpgradeLegacyData();
bool LoadEntitiesFromFile(const Platform::Path &filename, EntityList *le,
@ -785,9 +797,11 @@ public:
// the sketch!
bool allConsistent;
Platform::TimerRef showTWTimer;
Platform::TimerRef generateAllTimer;
bool scheduledGenerateAll;
bool scheduledShowTW;
Platform::TimerRef refreshTimer;
Platform::TimerRef autosaveTimer;
void Refresh();
void ScheduleShowTW();
void ScheduleGenerateAll();
void ScheduleAutosave();
@ -812,6 +826,7 @@ public:
void ImportDxf(const Platform::Path &file);
void ImportDwg(const Platform::Path &file);
bool LinkIDF(const Platform::Path &filename, EntityList *le, SMesh *m, SShell *sh);
bool LinkStl(const Platform::Path &filename, EntityList *le, SMesh *m, SShell *sh);
extern SolveSpaceUI SS;
extern Sketch SK;

View File

@ -29,7 +29,7 @@ void SCurve::GetAxisAlignedBounding(Vector *ptMax, Vector *ptMin) const {
}
}
// We will be inserting other curve verticies into our curves to split them.
// We will be inserting other curve vertices into our curves to split them.
// This is helpful when curved surfaces become tangent along a trim and the
// usual tests for curve-surface intersection don't split the curve at a vertex.
// This is faster than the previous version that split at surface corners and
@ -270,32 +270,35 @@ void SSurface::TrimFromEdgeList(SEdgeList *el, bool asUv) {
static bool KeepRegion(SSurface::CombineAs type, bool opA, SShell::Class shell, SShell::Class orig)
{
bool inShell = (shell == SShell::Class::INSIDE),
outSide = (shell == SShell::Class::OUTSIDE),
inSame = (shell == SShell::Class::COINC_SAME),
inOrig = (orig == SShell::Class::INSIDE);
bool inShell = (shell == SShell::Class::SURF_INSIDE),
outSide = (shell == SShell::Class::SURF_OUTSIDE),
coincSame = (shell == SShell::Class::SURF_COINC_SAME),
coincOpp = (shell == SShell::Class::SURF_COINC_OPP),
inOrig = (orig == SShell::Class::SURF_INSIDE);
// This one line is not really part of this functions logic
if(!inOrig) return false;
switch(type) {
case SSurface::CombineAs::UNION:
if(opA) {
return outSide;
} else {
return outSide || inSame;
return outSide || coincSame;
}
case SSurface::CombineAs::DIFFERENCE:
if(opA) {
return outSide;
return outSide || coincOpp;
} else {
return inShell || inSame;
return inShell;
}
case SSurface::CombineAs::INTERSECTION:
if(opA) {
return inShell;
} else {
return inShell || inSame;
return inShell || coincSame;
}
default: ssassert(false, "Unexpected combine type");
@ -318,29 +321,29 @@ static void TagByClassifiedEdge(SBspUv::Class bspclass, SShell::Class *indir, SS
{
switch(bspclass) {
case SBspUv::Class::INSIDE:
*indir = SShell::Class::INSIDE;
*outdir = SShell::Class::INSIDE;
*indir = SShell::Class::SURF_INSIDE;
*outdir = SShell::Class::SURF_INSIDE;
break;
case SBspUv::Class::OUTSIDE:
*indir = SShell::Class::OUTSIDE;
*outdir = SShell::Class::OUTSIDE;
*indir = SShell::Class::SURF_OUTSIDE;
*outdir = SShell::Class::SURF_OUTSIDE;
break;
case SBspUv::Class::EDGE_PARALLEL:
*indir = SShell::Class::INSIDE;
*outdir = SShell::Class::OUTSIDE;
*indir = SShell::Class::SURF_INSIDE;
*outdir = SShell::Class::SURF_OUTSIDE;
break;
case SBspUv::Class::EDGE_ANTIPARALLEL:
*indir = SShell::Class::OUTSIDE;
*outdir = SShell::Class::INSIDE;
*indir = SShell::Class::SURF_OUTSIDE;
*outdir = SShell::Class::SURF_INSIDE;
break;
default:
dbp("TagByClassifiedEdge: fail!");
*indir = SShell::Class::OUTSIDE;
*outdir = SShell::Class::OUTSIDE;
*indir = SShell::Class::SURF_OUTSIDE;
*outdir = SShell::Class::SURF_OUTSIDE;
break;
}
}
@ -438,13 +441,12 @@ void SSurface::EdgeNormalsWithinSurface(Point2d auv, Point2d buv,
double t;
sc->exact.ClosestPointTo(*pt, &t, /*mustConverge=*/false);
*pt = sc->exact.PointAt(t);
ClosestPointTo(*pt, &muv);
} else if(!sc->isExact) {
SSurface *trimmedA = sc->GetSurfaceA(sha, shb),
*trimmedB = sc->GetSurfaceB(sha, shb);
*pt = trimmedA->ClosestPointOnThisAndSurface(trimmedB, *pt);
ClosestPointTo(*pt, &muv);
}
ClosestPointTo(*pt, &muv);
*surfn = NormalAt(muv.x, muv.y);
@ -472,6 +474,9 @@ void SSurface::EdgeNormalsWithinSurface(Point2d auv, Point2d buv,
pout = PointAt(muv.Plus(enuv));
*enin = pin.Minus(*pt),
*enout = pout.Minus(*pt);
// ideally this should work (fail screwdriver file)
// *enin = enxyz.ScaledBy(-1.0);
// *enout = enxyz;
}
//-----------------------------------------------------------------------------
@ -612,8 +617,8 @@ SSurface SSurface::MakeCopyTrimAgainst(SShell *parent,
SShell::Class indir_shell, outdir_shell, indir_orig, outdir_orig;
indir_orig = SShell::Class::INSIDE;
outdir_orig = SShell::Class::OUTSIDE;
indir_orig = SShell::Class::SURF_INSIDE;
outdir_orig = SShell::Class::SURF_OUTSIDE;
agnst->ClassifyEdge(&indir_shell, &outdir_shell,
ret.PointAt(auv), ret.PointAt(buv), pt,
@ -796,7 +801,7 @@ void SShell::MakeFromBoolean(SShell *a, SShell *b, SSurface::CombineAs type) {
b->MakeClassifyingBsps(NULL);
// Copy over all the original curves, splitting them so that a
// piecwise linear segment never crosses a surface from the other
// piecewise linear segment never crosses a surface from the other
// shell.
a->CopyCurvesSplitAgainst(/*opA=*/true, b, this);
b->CopyCurvesSplitAgainst(/*opA=*/false, a, this);

View File

@ -817,7 +817,7 @@ void SCurve::RemoveShortSegments(SSurface *srfA, SSurface *srfB) {
continue;
}
// if the curve is exact and points are >0.05 appart wrt t, point is there
// if the curve is exact and points are >0.05 apart wrt t, point is there
// deliberately regardless of chord tolerance (ex: small circles)
tprev = t = tnext = 0;
if (isExact) {

View File

@ -398,14 +398,14 @@ SShell::Class SShell::ClassifyRegion(Vector edge_n, Vector inter_surf_n,
// are coincident. Test the edge's surface normal
// to see if it's with same or opposite normals.
if(inter_surf_n.Dot(edge_surf_n) > 0) {
return Class::COINC_SAME;
return Class::SURF_COINC_SAME;
} else {
return Class::COINC_OPP;
return Class::SURF_COINC_OPP;
}
} else if(dot > 0) {
return Class::OUTSIDE;
return Class::SURF_OUTSIDE;
} else {
return Class::INSIDE;
return Class::SURF_INSIDE;
}
}
@ -474,7 +474,7 @@ bool SShell::ClassifyEdge(Class *indir, Class *outdir,
swap(inter_edge_n[0], inter_edge_n[1]);
}
Class coinc = (surf_n.Dot(inter_surf_n[0])) > 0 ? Class::COINC_SAME : Class::COINC_OPP;
Class coinc = (surf_n.Dot(inter_surf_n[0])) > 0 ? Class::SURF_COINC_SAME : Class::SURF_COINC_OPP;
if(fabs(dotp[0]) < DOTP_TOL && fabs(dotp[1]) < DOTP_TOL) {
// This is actually an edge on face case, just that the face
@ -484,25 +484,25 @@ bool SShell::ClassifyEdge(Class *indir, Class *outdir,
} else if(fabs(dotp[0]) < DOTP_TOL && dotp[1] > DOTP_TOL) {
if(edge_n_out.Dot(inter_edge_n[0]) > 0) {
*indir = coinc;
*outdir = Class::OUTSIDE;
*outdir = Class::SURF_OUTSIDE;
} else {
*indir = Class::INSIDE;
*indir = Class::SURF_INSIDE;
*outdir = coinc;
}
} else if(fabs(dotp[0]) < DOTP_TOL && dotp[1] < -DOTP_TOL) {
if(edge_n_out.Dot(inter_edge_n[0]) > 0) {
*indir = coinc;
*outdir = Class::INSIDE;
*outdir = Class::SURF_INSIDE;
} else {
*indir = Class::OUTSIDE;
*indir = Class::SURF_OUTSIDE;
*outdir = coinc;
}
} else if(dotp[0] > DOTP_TOL && dotp[1] > DOTP_TOL) {
*indir = Class::INSIDE;
*outdir = Class::OUTSIDE;
*indir = Class::SURF_INSIDE;
*outdir = Class::SURF_OUTSIDE;
} else if(dotp[0] < -DOTP_TOL && dotp[1] < -DOTP_TOL) {
*indir = Class::OUTSIDE;
*outdir = Class::INSIDE;
*indir = Class::SURF_OUTSIDE;
*outdir = Class::SURF_INSIDE;
} else {
// Edge is tangent to the shell at shell's edge, so can't be
// a boundary of the surface.
@ -530,7 +530,7 @@ bool SShell::ClassifyEdge(Class *indir, Class *outdir,
SBspUv::Class c = (srf.bsp) ? srf.bsp->ClassifyPoint(puv, dummy, &srf) : SBspUv::Class::OUTSIDE;
if(c == SBspUv::Class::OUTSIDE) continue;
// Edge-on-face (unless edge-on-edge above superceded)
// Edge-on-face (unless edge-on-edge above superseded)
Point2d pin, pout;
srf.ClosestPointTo(p.Plus(edge_n_in), &pin, /*mustConverge=*/false);
srf.ClosestPointTo(p.Plus(edge_n_out), &pout, /*mustConverge=*/false);
@ -557,8 +557,8 @@ bool SShell::ClassifyEdge(Class *indir, Class *outdir,
/*asSegment=*/false, /*trimmed=*/true, /*inclTangent=*/false);
// no intersections means it's outside
*indir = Class::OUTSIDE;
*outdir = Class::OUTSIDE;
*indir = Class::SURF_OUTSIDE;
*outdir = Class::SURF_OUTSIDE;
double dmin = VERY_POSITIVE;
bool onEdge = false;
edge_inters = 0;
@ -584,11 +584,11 @@ bool SShell::ClassifyEdge(Class *indir, Class *outdir,
// Edge does not lie on surface; either strictly inside
// or strictly outside
if((si->surfNormal).Dot(ray) > 0) {
*indir = Class::INSIDE;
*outdir = Class::INSIDE;
*indir = Class::SURF_INSIDE;
*outdir = Class::SURF_INSIDE;
} else {
*indir = Class::OUTSIDE;
*outdir = Class::OUTSIDE;
*indir = Class::SURF_OUTSIDE;
*outdir = Class::SURF_OUTSIDE;
}
onEdge = si->onEdge;
}

614
src/srf/shell.cpp Normal file
View File

@ -0,0 +1,614 @@
//-----------------------------------------------------------------------------
// Anything involving NURBS shells (i.e., shells); except
// for the real math, which is in ratpoly.cpp.
//
// Copyright 2008-2013 Jonathan Westhues.
//-----------------------------------------------------------------------------
#include "../solvespace.h"
typedef struct {
hSCurve hc;
hSSurface hs;
} TrimLine;
void SShell::MakeFromExtrusionOf(SBezierLoopSet *sbls, Vector t0, Vector t1, RgbaColor color)
{
// Make the extrusion direction consistent with respect to the normal
// of the sketch we're extruding.
if((t0.Minus(t1)).Dot(sbls->normal) < 0) {
swap(t0, t1);
}
// Define a coordinate system to contain the original sketch, and get
// a bounding box in that csys
Vector n = sbls->normal.ScaledBy(-1);
Vector u = n.Normal(0), v = n.Normal(1);
Vector orig = sbls->point;
double umax = VERY_NEGATIVE, umin = VERY_POSITIVE;
sbls->GetBoundingProjd(u, orig, &umin, &umax);
double vmax = VERY_NEGATIVE, vmin = VERY_POSITIVE;
sbls->GetBoundingProjd(v, orig, &vmin, &vmax);
// and now fix things up so that all u and v lie between 0 and 1
orig = orig.Plus(u.ScaledBy(umin));
orig = orig.Plus(v.ScaledBy(vmin));
u = u.ScaledBy(umax - umin);
v = v.ScaledBy(vmax - vmin);
// So we can now generate the top and bottom surfaces of the extrusion,
// planes within a translated (and maybe mirrored) version of that csys.
SSurface s0, s1;
s0 = SSurface::FromPlane(orig.Plus(t0), u, v);
s0.color = color;
s1 = SSurface::FromPlane(orig.Plus(t1).Plus(u), u.ScaledBy(-1), v);
s1.color = color;
hSSurface hs0 = surface.AddAndAssignId(&s0),
hs1 = surface.AddAndAssignId(&s1);
// Now go through the input curves. For each one, generate its surface
// of extrusion, its two translated trim curves, and one trim line. We
// go through by loops so that we can assign the lines correctly.
SBezierLoop *sbl;
for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
SBezier *sb;
List<TrimLine> trimLines = {};
for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
// Generate the surface of extrusion of this curve, and add
// it to the list
SSurface ss = SSurface::FromExtrusionOf(sb, t0, t1);
ss.color = color;
hSSurface hsext = surface.AddAndAssignId(&ss);
// Translate the curve by t0 and t1 to produce two trim curves
SCurve sc = {};
sc.isExact = true;
sc.exact = sb->TransformedBy(t0, Quaternion::IDENTITY, 1.0);
(sc.exact).MakePwlInto(&(sc.pts));
sc.surfA = hs0;
sc.surfB = hsext;
hSCurve hc0 = curve.AddAndAssignId(&sc);
sc = {};
sc.isExact = true;
sc.exact = sb->TransformedBy(t1, Quaternion::IDENTITY, 1.0);
(sc.exact).MakePwlInto(&(sc.pts));
sc.surfA = hs1;
sc.surfB = hsext;
hSCurve hc1 = curve.AddAndAssignId(&sc);
STrimBy stb0, stb1;
// The translated curves trim the flat top and bottom surfaces.
stb0 = STrimBy::EntireCurve(this, hc0, /*backwards=*/false);
stb1 = STrimBy::EntireCurve(this, hc1, /*backwards=*/true);
(surface.FindById(hs0))->trim.Add(&stb0);
(surface.FindById(hs1))->trim.Add(&stb1);
// The translated curves also trim the surface of extrusion.
stb0 = STrimBy::EntireCurve(this, hc0, /*backwards=*/true);
stb1 = STrimBy::EntireCurve(this, hc1, /*backwards=*/false);
(surface.FindById(hsext))->trim.Add(&stb0);
(surface.FindById(hsext))->trim.Add(&stb1);
// And form the trim line
Vector pt = sb->Finish();
sc = {};
sc.isExact = true;
sc.exact = SBezier::From(pt.Plus(t0), pt.Plus(t1));
(sc.exact).MakePwlInto(&(sc.pts));
hSCurve hl = curve.AddAndAssignId(&sc);
// save this for later
TrimLine tl;
tl.hc = hl;
tl.hs = hsext;
trimLines.Add(&tl);
}
int i;
for(i = 0; i < trimLines.n; i++) {
TrimLine *tl = &(trimLines[i]);
SSurface *ss = surface.FindById(tl->hs);
TrimLine *tlp = &(trimLines[WRAP(i-1, trimLines.n)]);
STrimBy stb;
stb = STrimBy::EntireCurve(this, tl->hc, /*backwards=*/true);
ss->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, tlp->hc, /*backwards=*/false);
ss->trim.Add(&stb);
(curve.FindById(tl->hc))->surfA = ss->h;
(curve.FindById(tlp->hc))->surfB = ss->h;
}
trimLines.Clear();
}
}
bool SShell::CheckNormalAxisRelationship(SBezierLoopSet *sbls, Vector pt, Vector axis, double da, double dx)
// Check that the direction of revolution/extrusion ends up parallel to the normal of
// the sketch, on the side of the axis where the sketch is.
{
SBezierLoop *sbl;
Vector pto;
double md = VERY_NEGATIVE;
for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
SBezier *sb;
for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
// Choose the point farthest from the axis; we'll get garbage
// if we choose a point that lies on the axis, for example.
// (And our surface will be self-intersecting if the sketch
// spans the axis, so don't worry about that.)
for(int i = 0; i <= sb->deg; i++) {
Vector p = sb->ctrl[i];
double d = p.DistanceToLine(pt, axis);
if(d > md) {
md = d;
pto = p;
}
}
}
}
Vector ptc = pto.ClosestPointOnLine(pt, axis),
up = axis.Cross(pto.Minus(ptc)).ScaledBy(da),
vp = up.Plus(axis.ScaledBy(dx));
return (vp.Dot(sbls->normal) > 0);
}
// sketch must not contain the axis of revolution as a non-construction line for helix
void SShell::MakeFromHelicalRevolutionOf(SBezierLoopSet *sbls, Vector pt, Vector axis,
RgbaColor color, Group *group, double angles,
double anglef, double dists, double distf) {
int i0 = surface.n; // number of pre-existing surfaces
SBezierLoop *sbl;
// for testing - hard code the axial distance, and number of sections.
// distance will need to be parameters in the future.
double dist = distf - dists;
int sections = (int)(fabs(anglef - angles) / (PI / 2) + 1);
double wedge = (anglef - angles) / sections;
int startMapping = Group::REMAP_LATHE_START, endMapping = Group::REMAP_LATHE_END;
if(CheckNormalAxisRelationship(sbls, pt, axis, anglef-angles, distf-dists)) {
swap(angles, anglef);
swap(dists, distf);
dist = -dist;
wedge = -wedge;
swap(startMapping, endMapping);
}
// Define a coordinate system to contain the original sketch, and get
// a bounding box in that csys
Vector n = sbls->normal.ScaledBy(-1);
Vector u = n.Normal(0), v = n.Normal(1);
Vector orig = sbls->point;
double umax = VERY_NEGATIVE, umin = VERY_POSITIVE;
sbls->GetBoundingProjd(u, orig, &umin, &umax);
double vmax = VERY_NEGATIVE, vmin = VERY_POSITIVE;
sbls->GetBoundingProjd(v, orig, &vmin, &vmax);
// and now fix things up so that all u and v lie between 0 and 1
orig = orig.Plus(u.ScaledBy(umin));
orig = orig.Plus(v.ScaledBy(vmin));
u = u.ScaledBy(umax - umin);
v = v.ScaledBy(vmax - vmin);
// So we can now generate the end caps of the extrusion within
// a translated and rotated (and maybe mirrored) version of that csys.
SSurface s0, s1;
s0 = SSurface::FromPlane(orig.RotatedAbout(pt, axis, angles).Plus(axis.ScaledBy(dists)),
u.RotatedAbout(axis, angles), v.RotatedAbout(axis, angles));
s0.color = color;
hEntity face0 = group->Remap(Entity::NO_ENTITY, startMapping);
s0.face = face0.v;
s1 = SSurface::FromPlane(
orig.Plus(u).RotatedAbout(pt, axis, anglef).Plus(axis.ScaledBy(distf)),
u.ScaledBy(-1).RotatedAbout(axis, anglef), v.RotatedAbout(axis, anglef));
s1.color = color;
hEntity face1 = group->Remap(Entity::NO_ENTITY, endMapping);
s1.face = face1.v;
hSSurface hs0 = surface.AddAndAssignId(&s0);
hSSurface hs1 = surface.AddAndAssignId(&s1);
// Now we actually build and trim the swept surfaces. One loop at a time.
for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
int i, j;
SBezier *sb;
List<std::vector<hSSurface>> hsl = {};
// This is where all the NURBS are created and Remapped to the generating curve
for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
std::vector<hSSurface> revs(sections);
for(j = 0; j < sections; j++) {
if((dist == 0) && sb->deg == 1 &&
(sb->ctrl[0]).DistanceToLine(pt, axis) < LENGTH_EPS &&
(sb->ctrl[1]).DistanceToLine(pt, axis) < LENGTH_EPS) {
// This is a line on the axis of revolution; it does
// not contribute a surface.
revs[j].v = 0;
} else {
SSurface ss = SSurface::FromRevolutionOf(
sb, pt, axis, angles + (wedge)*j, angles + (wedge) * (j + 1),
dists + j * dist / sections, dists + (j + 1) * dist / sections);
ss.color = color;
if(sb->entity != 0) {
hEntity he;
he.v = sb->entity;
hEntity hface = group->Remap(he, Group::REMAP_LINE_TO_FACE);
if(SK.entity.FindByIdNoOops(hface) != NULL) {
ss.face = hface.v;
}
}
revs[j] = surface.AddAndAssignId(&ss);
}
}
hsl.Add(&revs);
}
// Still the same loop. Need to create trim curves
for(i = 0; i < sbl->l.n; i++) {
std::vector<hSSurface> revs = hsl[i], revsp = hsl[WRAP(i - 1, sbl->l.n)];
sb = &(sbl->l[i]);
// we will need the grid t-values for this entire row of surfaces
List<double> t_values;
t_values = {};
if (revs[0].v) {
double ps = 0.0;
t_values.Add(&ps);
(surface.FindById(revs[0]))->MakeTriangulationGridInto(
&t_values, 0.0, 1.0, true, 0);
}
// we generate one more curve than we did surfaces
for(j = 0; j <= sections; j++) {
SCurve sc;
Quaternion qs = Quaternion::From(axis, angles + wedge * j);
// we want Q*(x - p) + p = Q*x + (p - Q*p)
Vector ts =
pt.Minus(qs.Rotate(pt)).Plus(axis.ScaledBy(dists + j * dist / sections));
// If this input curve generated a surface, then trim that
// surface with the rotated version of the input curve.
if(revs[0].v) { // not d[j] because crash on j==sections
sc = {};
sc.isExact = true;
sc.exact = sb->TransformedBy(ts, qs, 1.0);
// make the PWL for the curve based on t value list
for(int x = 0; x < t_values.n; x++) {
SCurvePt scpt;
scpt.tag = 0;
scpt.p = sc.exact.PointAt(t_values[x]);
scpt.vertex = (x == 0) || (x == (t_values.n - 1));
sc.pts.Add(&scpt);
}
// the surfaces already exists so trim with this curve
if(j < sections) {
sc.surfA = revs[j];
} else {
sc.surfA = hs1; // end cap
}
if(j > 0) {
sc.surfB = revs[j - 1];
} else {
sc.surfB = hs0; // staring cap
}
hSCurve hcb = curve.AddAndAssignId(&sc);
STrimBy stb;
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/true);
(surface.FindById(sc.surfA))->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/false);
(surface.FindById(sc.surfB))->trim.Add(&stb);
} else if(j == 0) { // curve was on the rotation axis and is shared by the end caps.
sc = {};
sc.isExact = true;
sc.exact = sb->TransformedBy(ts, qs, 1.0);
(sc.exact).MakePwlInto(&(sc.pts));
sc.surfA = hs1; // end cap
sc.surfB = hs0; // staring cap
hSCurve hcb = curve.AddAndAssignId(&sc);
STrimBy stb;
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/true);
(surface.FindById(sc.surfA))->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/false);
(surface.FindById(sc.surfB))->trim.Add(&stb);
}
// And if this input curve and the one after it both generated
// surfaces, then trim both of those by the appropriate
// curve based on the control points.
if((j < sections) && revs[j].v && revsp[j].v) {
SSurface *ss = surface.FindById(revs[j]);
sc = {};
sc.isExact = true;
sc.exact = SBezier::From(ss->ctrl[0][0], ss->ctrl[0][1], ss->ctrl[0][2]);
sc.exact.weight[1] = ss->weight[0][1];
double max_dt = 0.5;
if (sc.exact.deg > 1) max_dt = 0.125;
(sc.exact).MakePwlInto(&(sc.pts), 0.0, max_dt);
sc.surfA = revs[j];
sc.surfB = revsp[j];
hSCurve hcc = curve.AddAndAssignId(&sc);
STrimBy stb;
stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/false);
(surface.FindById(sc.surfA))->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/true);
(surface.FindById(sc.surfB))->trim.Add(&stb);
}
}
t_values.Clear();
}
hsl.Clear();
}
if(dist == 0) {
MakeFirstOrderRevolvedSurfaces(pt, axis, i0);
}
}
void SShell::MakeFromRevolutionOf(SBezierLoopSet *sbls, Vector pt, Vector axis, RgbaColor color,
Group *group) {
int i0 = surface.n; // number of pre-existing surfaces
SBezierLoop *sbl;
if(CheckNormalAxisRelationship(sbls, pt, axis, 1.0, 0.0)) {
axis = axis.ScaledBy(-1);
}
// Now we actually build and trim the surfaces.
for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
int i, j;
SBezier *sb;
List<std::vector<hSSurface>> hsl = {};
for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
std::vector<hSSurface> revs(4);
for(j = 0; j < 4; j++) {
if(sb->deg == 1 &&
(sb->ctrl[0]).DistanceToLine(pt, axis) < LENGTH_EPS &&
(sb->ctrl[1]).DistanceToLine(pt, axis) < LENGTH_EPS)
{
// This is a line on the axis of revolution; it does
// not contribute a surface.
revs[j].v = 0;
} else {
SSurface ss = SSurface::FromRevolutionOf(sb, pt, axis, (PI / 2) * j,
(PI / 2) * (j + 1), 0.0, 0.0);
ss.color = color;
if(sb->entity != 0) {
hEntity he;
he.v = sb->entity;
hEntity hface = group->Remap(he, Group::REMAP_LINE_TO_FACE);
if(SK.entity.FindByIdNoOops(hface) != NULL) {
ss.face = hface.v;
}
}
revs[j] = surface.AddAndAssignId(&ss);
}
}
hsl.Add(&revs);
}
for(i = 0; i < sbl->l.n; i++) {
std::vector<hSSurface> revs = hsl[i],
revsp = hsl[WRAP(i-1, sbl->l.n)];
sb = &(sbl->l[i]);
for(j = 0; j < 4; j++) {
SCurve sc;
Quaternion qs = Quaternion::From(axis, (PI/2)*j);
// we want Q*(x - p) + p = Q*x + (p - Q*p)
Vector ts = pt.Minus(qs.Rotate(pt));
// If this input curve generate a surface, then trim that
// surface with the rotated version of the input curve.
if(revs[j].v) {
sc = {};
sc.isExact = true;
sc.exact = sb->TransformedBy(ts, qs, 1.0);
(sc.exact).MakePwlInto(&(sc.pts));
sc.surfA = revs[j];
sc.surfB = revs[WRAP(j-1, 4)];
hSCurve hcb = curve.AddAndAssignId(&sc);
STrimBy stb;
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/true);
(surface.FindById(sc.surfA))->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/false);
(surface.FindById(sc.surfB))->trim.Add(&stb);
}
// And if this input curve and the one after it both generated
// surfaces, then trim both of those by the appropriate
// circle.
if(revs[j].v && revsp[j].v) {
SSurface *ss = surface.FindById(revs[j]);
sc = {};
sc.isExact = true;
sc.exact = SBezier::From(ss->ctrl[0][0],
ss->ctrl[0][1],
ss->ctrl[0][2]);
sc.exact.weight[1] = ss->weight[0][1];
(sc.exact).MakePwlInto(&(sc.pts));
sc.surfA = revs[j];
sc.surfB = revsp[j];
hSCurve hcc = curve.AddAndAssignId(&sc);
STrimBy stb;
stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/false);
(surface.FindById(sc.surfA))->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/true);
(surface.FindById(sc.surfB))->trim.Add(&stb);
}
}
}
hsl.Clear();
}
MakeFirstOrderRevolvedSurfaces(pt, axis, i0);
}
void SShell::MakeFirstOrderRevolvedSurfaces(Vector pt, Vector axis, int i0) {
int i;
for(i = i0; i < surface.n; i++) {
SSurface *srf = &(surface[i]);
// Revolution of a line; this is potentially a plane, which we can
// rewrite to have degree (1, 1).
if(srf->degm == 1 && srf->degn == 2) {
// close start, far start, far finish
Vector cs, fs, ff;
double d0, d1;
d0 = (srf->ctrl[0][0]).DistanceToLine(pt, axis);
d1 = (srf->ctrl[1][0]).DistanceToLine(pt, axis);
if(d0 > d1) {
cs = srf->ctrl[1][0];
fs = srf->ctrl[0][0];
ff = srf->ctrl[0][2];
} else {
cs = srf->ctrl[0][0];
fs = srf->ctrl[1][0];
ff = srf->ctrl[1][2];
}
// origin close, origin far
Vector oc = cs.ClosestPointOnLine(pt, axis),
of = fs.ClosestPointOnLine(pt, axis);
if(oc.Equals(of)) {
// This is a plane, not a (non-degenerate) cone.
Vector oldn = srf->NormalAt(0.5, 0.5);
Vector u = fs.Minus(of), v;
v = (axis.Cross(u)).WithMagnitude(1);
double vm = (ff.Minus(of)).Dot(v);
v = v.ScaledBy(vm);
srf->degm = 1;
srf->degn = 1;
srf->ctrl[0][0] = of;
srf->ctrl[0][1] = of.Plus(u);
srf->ctrl[1][0] = of.Plus(v);
srf->ctrl[1][1] = of.Plus(u).Plus(v);
srf->weight[0][0] = 1;
srf->weight[0][1] = 1;
srf->weight[1][0] = 1;
srf->weight[1][1] = 1;
if(oldn.Dot(srf->NormalAt(0.5, 0.5)) < 0) {
swap(srf->ctrl[0][0], srf->ctrl[1][0]);
swap(srf->ctrl[0][1], srf->ctrl[1][1]);
}
continue;
}
if(fabs(d0 - d1) < LENGTH_EPS) {
// This is a cylinder; so transpose it so that we'll recognize
// it as a surface of extrusion.
SSurface sn = *srf;
// Transposing u and v flips the normal, so reverse u to
// flip it again and put it back where we started.
sn.degm = 2;
sn.degn = 1;
int dm, dn;
for(dm = 0; dm <= 1; dm++) {
for(dn = 0; dn <= 2; dn++) {
sn.ctrl [dn][dm] = srf->ctrl [1-dm][dn];
sn.weight[dn][dm] = srf->weight[1-dm][dn];
}
}
*srf = sn;
continue;
}
}
}
}
void SShell::MakeFromCopyOf(SShell *a) {
ssassert(this != a, "Can't make from copy of self");
MakeFromTransformationOf(a,
Vector::From(0, 0, 0), Quaternion::IDENTITY, 1.0);
}
void SShell::MakeFromTransformationOf(SShell *a,
Vector t, Quaternion q, double scale)
{
booleanFailed = false;
surface.ReserveMore(a->surface.n);
for(SSurface &s : a->surface) {
SSurface n;
n = SSurface::FromTransformationOf(&s, t, q, scale, /*includingTrims=*/true);
surface.Add(&n); // keeping the old ID
}
curve.ReserveMore(a->curve.n);
for(SCurve &c : a->curve) {
SCurve n;
n = SCurve::FromTransformationOf(&c, t, q, scale);
curve.Add(&n); // keeping the old ID
}
}
void SShell::MakeEdgesInto(SEdgeList *sel) {
for(SSurface &s : surface) {
s.MakeEdgesInto(this, sel, SSurface::MakeAs::XYZ);
}
}
void SShell::MakeSectionEdgesInto(Vector n, double d, SEdgeList *sel, SBezierList *sbl)
{
for(SSurface &s : surface) {
if(s.CoincidentWithPlane(n, d)) {
s.MakeSectionEdgesInto(this, sel, sbl);
}
}
}
void SShell::TriangulateInto(SMesh *sm) {
#pragma omp parallel for
for(int i=0; i<surface.n; i++) {
SSurface *s = &surface[i];
SMesh m;
s->TriangulateInto(this, &m);
#pragma omp critical
sm->MakeFromCopyOf(&m);
m.Clear();
}
}
bool SShell::IsEmpty() const {
return surface.IsEmpty();
}
void SShell::Clear() {
for(SSurface &s : surface) {
s.Clear();
}
surface.Clear();
for(SCurve &c : curve) {
c.Clear();
}
curve.Clear();
}

View File

@ -489,608 +489,4 @@ void SSurface::Clear() {
trim.Clear();
}
typedef struct {
hSCurve hc;
hSSurface hs;
} TrimLine;
void SShell::MakeFromExtrusionOf(SBezierLoopSet *sbls, Vector t0, Vector t1, RgbaColor color)
{
// Make the extrusion direction consistent with respect to the normal
// of the sketch we're extruding.
if((t0.Minus(t1)).Dot(sbls->normal) < 0) {
swap(t0, t1);
}
// Define a coordinate system to contain the original sketch, and get
// a bounding box in that csys
Vector n = sbls->normal.ScaledBy(-1);
Vector u = n.Normal(0), v = n.Normal(1);
Vector orig = sbls->point;
double umax = VERY_NEGATIVE, umin = VERY_POSITIVE;
sbls->GetBoundingProjd(u, orig, &umin, &umax);
double vmax = VERY_NEGATIVE, vmin = VERY_POSITIVE;
sbls->GetBoundingProjd(v, orig, &vmin, &vmax);
// and now fix things up so that all u and v lie between 0 and 1
orig = orig.Plus(u.ScaledBy(umin));
orig = orig.Plus(v.ScaledBy(vmin));
u = u.ScaledBy(umax - umin);
v = v.ScaledBy(vmax - vmin);
// So we can now generate the top and bottom surfaces of the extrusion,
// planes within a translated (and maybe mirrored) version of that csys.
SSurface s0, s1;
s0 = SSurface::FromPlane(orig.Plus(t0), u, v);
s0.color = color;
s1 = SSurface::FromPlane(orig.Plus(t1).Plus(u), u.ScaledBy(-1), v);
s1.color = color;
hSSurface hs0 = surface.AddAndAssignId(&s0),
hs1 = surface.AddAndAssignId(&s1);
// Now go through the input curves. For each one, generate its surface
// of extrusion, its two translated trim curves, and one trim line. We
// go through by loops so that we can assign the lines correctly.
SBezierLoop *sbl;
for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
SBezier *sb;
List<TrimLine> trimLines = {};
for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
// Generate the surface of extrusion of this curve, and add
// it to the list
SSurface ss = SSurface::FromExtrusionOf(sb, t0, t1);
ss.color = color;
hSSurface hsext = surface.AddAndAssignId(&ss);
// Translate the curve by t0 and t1 to produce two trim curves
SCurve sc = {};
sc.isExact = true;
sc.exact = sb->TransformedBy(t0, Quaternion::IDENTITY, 1.0);
(sc.exact).MakePwlInto(&(sc.pts));
sc.surfA = hs0;
sc.surfB = hsext;
hSCurve hc0 = curve.AddAndAssignId(&sc);
sc = {};
sc.isExact = true;
sc.exact = sb->TransformedBy(t1, Quaternion::IDENTITY, 1.0);
(sc.exact).MakePwlInto(&(sc.pts));
sc.surfA = hs1;
sc.surfB = hsext;
hSCurve hc1 = curve.AddAndAssignId(&sc);
STrimBy stb0, stb1;
// The translated curves trim the flat top and bottom surfaces.
stb0 = STrimBy::EntireCurve(this, hc0, /*backwards=*/false);
stb1 = STrimBy::EntireCurve(this, hc1, /*backwards=*/true);
(surface.FindById(hs0))->trim.Add(&stb0);
(surface.FindById(hs1))->trim.Add(&stb1);
// The translated curves also trim the surface of extrusion.
stb0 = STrimBy::EntireCurve(this, hc0, /*backwards=*/true);
stb1 = STrimBy::EntireCurve(this, hc1, /*backwards=*/false);
(surface.FindById(hsext))->trim.Add(&stb0);
(surface.FindById(hsext))->trim.Add(&stb1);
// And form the trim line
Vector pt = sb->Finish();
sc = {};
sc.isExact = true;
sc.exact = SBezier::From(pt.Plus(t0), pt.Plus(t1));
(sc.exact).MakePwlInto(&(sc.pts));
hSCurve hl = curve.AddAndAssignId(&sc);
// save this for later
TrimLine tl;
tl.hc = hl;
tl.hs = hsext;
trimLines.Add(&tl);
}
int i;
for(i = 0; i < trimLines.n; i++) {
TrimLine *tl = &(trimLines[i]);
SSurface *ss = surface.FindById(tl->hs);
TrimLine *tlp = &(trimLines[WRAP(i-1, trimLines.n)]);
STrimBy stb;
stb = STrimBy::EntireCurve(this, tl->hc, /*backwards=*/true);
ss->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, tlp->hc, /*backwards=*/false);
ss->trim.Add(&stb);
(curve.FindById(tl->hc))->surfA = ss->h;
(curve.FindById(tlp->hc))->surfB = ss->h;
}
trimLines.Clear();
}
}
bool SShell::CheckNormalAxisRelationship(SBezierLoopSet *sbls, Vector pt, Vector axis, double da, double dx)
// Check that the direction of revolution/extrusion ends up parallel to the normal of
// the sketch, on the side of the axis where the sketch is.
{
SBezierLoop *sbl;
Vector pto;
double md = VERY_NEGATIVE;
for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
SBezier *sb;
for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
// Choose the point farthest from the axis; we'll get garbage
// if we choose a point that lies on the axis, for example.
// (And our surface will be self-intersecting if the sketch
// spans the axis, so don't worry about that.)
for(int i = 0; i <= sb->deg; i++) {
Vector p = sb->ctrl[i];
double d = p.DistanceToLine(pt, axis);
if(d > md) {
md = d;
pto = p;
}
}
}
}
Vector ptc = pto.ClosestPointOnLine(pt, axis),
up = axis.Cross(pto.Minus(ptc)).ScaledBy(da),
vp = up.Plus(axis.ScaledBy(dx));
return (vp.Dot(sbls->normal) > 0);
}
// sketch must not contain the axis of revolution as a non-construction line for helix
void SShell::MakeFromHelicalRevolutionOf(SBezierLoopSet *sbls, Vector pt, Vector axis,
RgbaColor color, Group *group, double angles,
double anglef, double dists, double distf) {
int i0 = surface.n; // number of pre-existing surfaces
SBezierLoop *sbl;
// for testing - hard code the axial distance, and number of sections.
// distance will need to be parameters in the future.
double dist = distf - dists;
int sections = (int)(fabs(anglef - angles) / (PI / 2) + 1);
double wedge = (anglef - angles) / sections;
int startMapping = Group::REMAP_LATHE_START, endMapping = Group::REMAP_LATHE_END;
if(CheckNormalAxisRelationship(sbls, pt, axis, anglef-angles, distf-dists)) {
swap(angles, anglef);
swap(dists, distf);
dist = -dist;
wedge = -wedge;
swap(startMapping, endMapping);
}
// Define a coordinate system to contain the original sketch, and get
// a bounding box in that csys
Vector n = sbls->normal.ScaledBy(-1);
Vector u = n.Normal(0), v = n.Normal(1);
Vector orig = sbls->point;
double umax = VERY_NEGATIVE, umin = VERY_POSITIVE;
sbls->GetBoundingProjd(u, orig, &umin, &umax);
double vmax = VERY_NEGATIVE, vmin = VERY_POSITIVE;
sbls->GetBoundingProjd(v, orig, &vmin, &vmax);
// and now fix things up so that all u and v lie between 0 and 1
orig = orig.Plus(u.ScaledBy(umin));
orig = orig.Plus(v.ScaledBy(vmin));
u = u.ScaledBy(umax - umin);
v = v.ScaledBy(vmax - vmin);
// So we can now generate the end caps of the extrusion within
// a translated and rotated (and maybe mirrored) version of that csys.
SSurface s0, s1;
s0 = SSurface::FromPlane(orig.RotatedAbout(pt, axis, angles).Plus(axis.ScaledBy(dists)),
u.RotatedAbout(axis, angles), v.RotatedAbout(axis, angles));
s0.color = color;
hEntity face0 = group->Remap(Entity::NO_ENTITY, startMapping);
s0.face = face0.v;
s1 = SSurface::FromPlane(
orig.Plus(u).RotatedAbout(pt, axis, anglef).Plus(axis.ScaledBy(distf)),
u.ScaledBy(-1).RotatedAbout(axis, anglef), v.RotatedAbout(axis, anglef));
s1.color = color;
hEntity face1 = group->Remap(Entity::NO_ENTITY, endMapping);
s1.face = face1.v;
hSSurface hs0 = surface.AddAndAssignId(&s0);
hSSurface hs1 = surface.AddAndAssignId(&s1);
// Now we actually build and trim the swept surfaces. One loop at a time.
for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
int i, j;
SBezier *sb;
List<std::vector<hSSurface>> hsl = {};
// This is where all the NURBS are created and Remapped to the generating curve
for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
std::vector<hSSurface> revs(sections);
for(j = 0; j < sections; j++) {
if((dist == 0) && sb->deg == 1 &&
(sb->ctrl[0]).DistanceToLine(pt, axis) < LENGTH_EPS &&
(sb->ctrl[1]).DistanceToLine(pt, axis) < LENGTH_EPS) {
// This is a line on the axis of revolution; it does
// not contribute a surface.
revs[j].v = 0;
} else {
SSurface ss = SSurface::FromRevolutionOf(
sb, pt, axis, angles + (wedge)*j, angles + (wedge) * (j + 1),
dists + j * dist / sections, dists + (j + 1) * dist / sections);
ss.color = color;
if(sb->entity != 0) {
hEntity he;
he.v = sb->entity;
hEntity hface = group->Remap(he, Group::REMAP_LINE_TO_FACE);
if(SK.entity.FindByIdNoOops(hface) != NULL) {
ss.face = hface.v;
}
}
revs[j] = surface.AddAndAssignId(&ss);
}
}
hsl.Add(&revs);
}
// Still the same loop. Need to create trim curves
for(i = 0; i < sbl->l.n; i++) {
std::vector<hSSurface> revs = hsl[i], revsp = hsl[WRAP(i - 1, sbl->l.n)];
sb = &(sbl->l[i]);
// we will need the grid t-values for this entire row of surfaces
List<double> t_values;
t_values = {};
if (revs[0].v) {
double ps = 0.0;
t_values.Add(&ps);
(surface.FindById(revs[0]))->MakeTriangulationGridInto(
&t_values, 0.0, 1.0, true, 0);
}
// we generate one more curve than we did surfaces
for(j = 0; j <= sections; j++) {
SCurve sc;
Quaternion qs = Quaternion::From(axis, angles + wedge * j);
// we want Q*(x - p) + p = Q*x + (p - Q*p)
Vector ts =
pt.Minus(qs.Rotate(pt)).Plus(axis.ScaledBy(dists + j * dist / sections));
// If this input curve generated a surface, then trim that
// surface with the rotated version of the input curve.
if(revs[0].v) { // not d[j] because crash on j==sections
sc = {};
sc.isExact = true;
sc.exact = sb->TransformedBy(ts, qs, 1.0);
// make the PWL for the curve based on t value list
for(int x = 0; x < t_values.n; x++) {
SCurvePt scpt;
scpt.tag = 0;
scpt.p = sc.exact.PointAt(t_values[x]);
scpt.vertex = (x == 0) || (x == (t_values.n - 1));
sc.pts.Add(&scpt);
}
// the surfaces already exists so trim with this curve
if(j < sections) {
sc.surfA = revs[j];
} else {
sc.surfA = hs1; // end cap
}
if(j > 0) {
sc.surfB = revs[j - 1];
} else {
sc.surfB = hs0; // staring cap
}
hSCurve hcb = curve.AddAndAssignId(&sc);
STrimBy stb;
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/true);
(surface.FindById(sc.surfA))->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/false);
(surface.FindById(sc.surfB))->trim.Add(&stb);
} else if(j == 0) { // curve was on the rotation axis and is shared by the end caps.
sc = {};
sc.isExact = true;
sc.exact = sb->TransformedBy(ts, qs, 1.0);
(sc.exact).MakePwlInto(&(sc.pts));
sc.surfA = hs1; // end cap
sc.surfB = hs0; // staring cap
hSCurve hcb = curve.AddAndAssignId(&sc);
STrimBy stb;
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/true);
(surface.FindById(sc.surfA))->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/false);
(surface.FindById(sc.surfB))->trim.Add(&stb);
}
// And if this input curve and the one after it both generated
// surfaces, then trim both of those by the appropriate
// curve based on the control points.
if((j < sections) && revs[j].v && revsp[j].v) {
SSurface *ss = surface.FindById(revs[j]);
sc = {};
sc.isExact = true;
sc.exact = SBezier::From(ss->ctrl[0][0], ss->ctrl[0][1], ss->ctrl[0][2]);
sc.exact.weight[1] = ss->weight[0][1];
double max_dt = 0.5;
if (sc.exact.deg > 1) max_dt = 0.125;
(sc.exact).MakePwlInto(&(sc.pts), 0.0, max_dt);
sc.surfA = revs[j];
sc.surfB = revsp[j];
hSCurve hcc = curve.AddAndAssignId(&sc);
STrimBy stb;
stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/false);
(surface.FindById(sc.surfA))->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/true);
(surface.FindById(sc.surfB))->trim.Add(&stb);
}
}
t_values.Clear();
}
hsl.Clear();
}
if(dist == 0) {
MakeFirstOrderRevolvedSurfaces(pt, axis, i0);
}
}
void SShell::MakeFromRevolutionOf(SBezierLoopSet *sbls, Vector pt, Vector axis, RgbaColor color,
Group *group) {
int i0 = surface.n; // number of pre-existing surfaces
SBezierLoop *sbl;
if(CheckNormalAxisRelationship(sbls, pt, axis, 1.0, 0.0)) {
axis = axis.ScaledBy(-1);
}
// Now we actually build and trim the surfaces.
for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
int i, j;
SBezier *sb;
List<std::vector<hSSurface>> hsl = {};
for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
std::vector<hSSurface> revs(4);
for(j = 0; j < 4; j++) {
if(sb->deg == 1 &&
(sb->ctrl[0]).DistanceToLine(pt, axis) < LENGTH_EPS &&
(sb->ctrl[1]).DistanceToLine(pt, axis) < LENGTH_EPS)
{
// This is a line on the axis of revolution; it does
// not contribute a surface.
revs[j].v = 0;
} else {
SSurface ss = SSurface::FromRevolutionOf(sb, pt, axis, (PI / 2) * j,
(PI / 2) * (j + 1), 0.0, 0.0);
ss.color = color;
if(sb->entity != 0) {
hEntity he;
he.v = sb->entity;
hEntity hface = group->Remap(he, Group::REMAP_LINE_TO_FACE);
if(SK.entity.FindByIdNoOops(hface) != NULL) {
ss.face = hface.v;
}
}
revs[j] = surface.AddAndAssignId(&ss);
}
}
hsl.Add(&revs);
}
for(i = 0; i < sbl->l.n; i++) {
std::vector<hSSurface> revs = hsl[i],
revsp = hsl[WRAP(i-1, sbl->l.n)];
sb = &(sbl->l[i]);
for(j = 0; j < 4; j++) {
SCurve sc;
Quaternion qs = Quaternion::From(axis, (PI/2)*j);
// we want Q*(x - p) + p = Q*x + (p - Q*p)
Vector ts = pt.Minus(qs.Rotate(pt));
// If this input curve generate a surface, then trim that
// surface with the rotated version of the input curve.
if(revs[j].v) {
sc = {};
sc.isExact = true;
sc.exact = sb->TransformedBy(ts, qs, 1.0);
(sc.exact).MakePwlInto(&(sc.pts));
sc.surfA = revs[j];
sc.surfB = revs[WRAP(j-1, 4)];
hSCurve hcb = curve.AddAndAssignId(&sc);
STrimBy stb;
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/true);
(surface.FindById(sc.surfA))->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/false);
(surface.FindById(sc.surfB))->trim.Add(&stb);
}
// And if this input curve and the one after it both generated
// surfaces, then trim both of those by the appropriate
// circle.
if(revs[j].v && revsp[j].v) {
SSurface *ss = surface.FindById(revs[j]);
sc = {};
sc.isExact = true;
sc.exact = SBezier::From(ss->ctrl[0][0],
ss->ctrl[0][1],
ss->ctrl[0][2]);
sc.exact.weight[1] = ss->weight[0][1];
(sc.exact).MakePwlInto(&(sc.pts));
sc.surfA = revs[j];
sc.surfB = revsp[j];
hSCurve hcc = curve.AddAndAssignId(&sc);
STrimBy stb;
stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/false);
(surface.FindById(sc.surfA))->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/true);
(surface.FindById(sc.surfB))->trim.Add(&stb);
}
}
}
hsl.Clear();
}
MakeFirstOrderRevolvedSurfaces(pt, axis, i0);
}
void SShell::MakeFirstOrderRevolvedSurfaces(Vector pt, Vector axis, int i0) {
int i;
for(i = i0; i < surface.n; i++) {
SSurface *srf = &(surface[i]);
// Revolution of a line; this is potentially a plane, which we can
// rewrite to have degree (1, 1).
if(srf->degm == 1 && srf->degn == 2) {
// close start, far start, far finish
Vector cs, fs, ff;
double d0, d1;
d0 = (srf->ctrl[0][0]).DistanceToLine(pt, axis);
d1 = (srf->ctrl[1][0]).DistanceToLine(pt, axis);
if(d0 > d1) {
cs = srf->ctrl[1][0];
fs = srf->ctrl[0][0];
ff = srf->ctrl[0][2];
} else {
cs = srf->ctrl[0][0];
fs = srf->ctrl[1][0];
ff = srf->ctrl[1][2];
}
// origin close, origin far
Vector oc = cs.ClosestPointOnLine(pt, axis),
of = fs.ClosestPointOnLine(pt, axis);
if(oc.Equals(of)) {
// This is a plane, not a (non-degenerate) cone.
Vector oldn = srf->NormalAt(0.5, 0.5);
Vector u = fs.Minus(of), v;
v = (axis.Cross(u)).WithMagnitude(1);
double vm = (ff.Minus(of)).Dot(v);
v = v.ScaledBy(vm);
srf->degm = 1;
srf->degn = 1;
srf->ctrl[0][0] = of;
srf->ctrl[0][1] = of.Plus(u);
srf->ctrl[1][0] = of.Plus(v);
srf->ctrl[1][1] = of.Plus(u).Plus(v);
srf->weight[0][0] = 1;
srf->weight[0][1] = 1;
srf->weight[1][0] = 1;
srf->weight[1][1] = 1;
if(oldn.Dot(srf->NormalAt(0.5, 0.5)) < 0) {
swap(srf->ctrl[0][0], srf->ctrl[1][0]);
swap(srf->ctrl[0][1], srf->ctrl[1][1]);
}
continue;
}
if(fabs(d0 - d1) < LENGTH_EPS) {
// This is a cylinder; so transpose it so that we'll recognize
// it as a surface of extrusion.
SSurface sn = *srf;
// Transposing u and v flips the normal, so reverse u to
// flip it again and put it back where we started.
sn.degm = 2;
sn.degn = 1;
int dm, dn;
for(dm = 0; dm <= 1; dm++) {
for(dn = 0; dn <= 2; dn++) {
sn.ctrl [dn][dm] = srf->ctrl [1-dm][dn];
sn.weight[dn][dm] = srf->weight[1-dm][dn];
}
}
*srf = sn;
continue;
}
}
}
}
void SShell::MakeFromCopyOf(SShell *a) {
ssassert(this != a, "Can't make from copy of self");
MakeFromTransformationOf(a,
Vector::From(0, 0, 0), Quaternion::IDENTITY, 1.0);
}
void SShell::MakeFromTransformationOf(SShell *a,
Vector t, Quaternion q, double scale)
{
booleanFailed = false;
surface.ReserveMore(a->surface.n);
for(SSurface &s : a->surface) {
SSurface n;
n = SSurface::FromTransformationOf(&s, t, q, scale, /*includingTrims=*/true);
surface.Add(&n); // keeping the old ID
}
curve.ReserveMore(a->curve.n);
for(SCurve &c : a->curve) {
SCurve n;
n = SCurve::FromTransformationOf(&c, t, q, scale);
curve.Add(&n); // keeping the old ID
}
}
void SShell::MakeEdgesInto(SEdgeList *sel) {
for(SSurface &s : surface) {
s.MakeEdgesInto(this, sel, SSurface::MakeAs::XYZ);
}
}
void SShell::MakeSectionEdgesInto(Vector n, double d, SEdgeList *sel, SBezierList *sbl)
{
for(SSurface &s : surface) {
if(s.CoincidentWithPlane(n, d)) {
s.MakeSectionEdgesInto(this, sel, sbl);
}
}
}
void SShell::TriangulateInto(SMesh *sm) {
#pragma omp parallel for
for(int i=0; i<surface.n; i++) {
SSurface *s = &surface[i];
SMesh m;
s->TriangulateInto(this, &m);
#pragma omp critical
sm->MakeFromCopyOf(&m);
m.Clear();
}
}
bool SShell::IsEmpty() const {
return surface.IsEmpty();
}
void SShell::Clear() {
for(SSurface &s : surface) {
s.Clear();
}
surface.Clear();
for(SCurve &c : curve) {
c.Clear();
}
curve.Clear();
}

View File

@ -407,10 +407,10 @@ public:
// outside, or coincident (with parallel or antiparallel normal) with a
// shell.
enum class Class : uint32_t {
INSIDE = 100,
OUTSIDE = 200,
COINC_SAME = 300,
COINC_OPP = 400
SURF_INSIDE = 100,
SURF_OUTSIDE = 200,
SURF_COINC_SAME = 300,
SURF_COINC_OPP = 400
};
static const double DOTP_TOL;
Class ClassifyRegion(Vector edge_n, Vector inter_surf_n,

View File

@ -456,13 +456,11 @@ void SSurface::IntersectAgainst(SSurface *b, SShell *agnstA, SShell *agnstB,
}
}
Vector dp = nb.Cross(na).WithMagnitude(1.0);
if(!fwd) dp = dp.ScaledBy(-1.0);
int i;
for(i = 0; i < 20; i++) {
Vector dp = nb.Cross(na);
if(!fwd) dp = dp.ScaledBy(-1);
dp = dp.WithMagnitude(step);
np = start.Plus(dp);
np = start.Plus(dp.ScaledBy(step));
npc = ClosestPointOnThisAndSurface(b, np);
tol = (npc.Minus(np)).Magnitude();

Some files were not shown because too many files have changed in this diff Show More