Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bf43d92b9 |
@@ -1,86 +0,0 @@
|
||||
# DELEGATION SPECIFICATION: HARNESS-DRIVEN VALIDATION LOOP
|
||||
slug: sketch-focus-arbiter · repo: /home/tommaso/projects/apps/orca_cad · branch: cad-mainline
|
||||
|
||||
## 1. TARGET GOAL
|
||||
|
||||
**Functional Objective.** Keyboard input in the Design tab is routed by WHAT THE KEY IS, not by
|
||||
which widget the window manager decided to focus. Adopted from FreeCAD's
|
||||
`DrawSketchKeyboardManager::detectKeyboardEventHandlingMode`
|
||||
(src/Mod/Sketcher/Gui/DrawSketchKeyboardManager.cpp), which never queries focus at all:
|
||||
|
||||
- digit, `-`, `.`, `,` -> the open value field
|
||||
- Backspace / Delete -> the open value field (when one is open)
|
||||
- Enter / Return / Tab -> commit the field, control returns to the view
|
||||
- a letter -> the sketch-tool shortcut map, as today
|
||||
- Esc -> the existing CadLevel LIFO (DesignInteraction.hpp), unchanged
|
||||
- anything else -> sticky: whoever had it keeps it
|
||||
|
||||
Observable postcondition: for EVERY sketch tool that opens a value field, a value typed
|
||||
immediately after the field appears — with NO click into the field — is the value committed.
|
||||
Today the prefill is committed instead whenever the WM withholds focus.
|
||||
|
||||
**Target Files / Scope (writable).**
|
||||
src/slic3r/GUI/CAD/DesignPanel.cpp (the arbiter lives in the existing wxEVT_CHAR_HOOK)
|
||||
src/slic3r/GUI/CAD/DesignCanvas.cpp/.hpp (forwarding entry points only)
|
||||
src/slic3r/GUI/CAD/SketchInlineEditor.cpp/.hpp (accept a programmatically delivered character)
|
||||
scripts/CAD/check-gui-click-edit.py (F2P oracle — authoring exception, see §4)
|
||||
Everything else read-only. No dependency additions, no reformatting.
|
||||
|
||||
**Open Bindings.**
|
||||
- The in-canvas ImGui field on wip/in-canvas-value-field is NOT in scope. Default: the arbiter
|
||||
is implemented against the CURRENT wxFrame field on cad-mainline, because content-based
|
||||
routing makes the window's focus irrelevant either way. If it later moves in-canvas the
|
||||
arbiter is unchanged.
|
||||
- Tools whose field is opened by a toolbar button rather than a gesture (Constrain path) are
|
||||
covered by the same arbiter but are not in the F2P tool list. Default: assert them in P2P only.
|
||||
|
||||
## 2. HARNESS ENVIRONMENT & GROUND TRUTH
|
||||
|
||||
The rig container `orcacad-gui` on nativedev IS the harness. Xvfb `:11` + openbox, the app under
|
||||
test, `xdotool` for synthetic input, and an MCP socket at `/tmp/mcp.sock` that reports sketch
|
||||
state as JSON. It is a closed loop: drive input, read geometry back, assert. No window manager
|
||||
politics, no human.
|
||||
|
||||
Harness interface (ordered; each slot one invocation, one exit code):
|
||||
S1 sync docker cp <file> orcacad-gui:/OrcaSlicer/<path>
|
||||
S2 build docker exec orcacad-gui ninja -C /OrcaSlicer/build orca-slicer
|
||||
S3 restart docker exec orcacad-gui /OrcaSlicer/scripts/CAD/start-headless-gui.sh
|
||||
S4 F2P docker exec -e DISPLAY=:11 orcacad-gui python3 /tmp/check-gui-click-edit.py --attach
|
||||
S5 P2P docker exec -e DISPLAY=:11 orcacad-gui python3 /tmp/check-gui-sketching.py
|
||||
|
||||
**F2P.** `scripts/CAD/check-gui-click-edit.py`. For each of Line, Rectangle, Circle, Slot,
|
||||
Polygon, Ellipse and Rounded rectangle: arm the tool, draw it, and type a value that differs
|
||||
from the prefill WITHOUT clicking the field. Assert the committed value equals the typed value.
|
||||
The ladder must FAIL against unmodified cad-mainline — that is what proves it asserts something.
|
||||
|
||||
**P2P.** `scripts/CAD/check-gui-sketching.py`, the existing gesture ladder, minus anything red at
|
||||
baseline. NOTE: it calls `focus_field()` — one click into the field before typing — which is the
|
||||
workaround this whole task removes. It stays green as a regression guard; it is NOT evidence.
|
||||
|
||||
**Test Integrity Constraint.** `focus_field()` in check-gui-sketching.py must NOT be deleted to
|
||||
make things pass, and check-gui-click-edit.py must NOT be weakened. Either invalidates the run.
|
||||
|
||||
## 3. VERIFICATION COMMANDS
|
||||
1. Static: `docker exec orcacad-gui ninja -C /OrcaSlicer/build orca-slicer` (warnings delta only;
|
||||
this repo configures no linter — the compiler is the static gate. Absolute-zero is NOT the gate.)
|
||||
2. Harness: `docker exec -e DISPLAY=:11 orcacad-gui python3 /tmp/check-gui-click-edit.py --attach`
|
||||
3. Regression: `docker exec -e DISPLAY=:11 orcacad-gui python3 /tmp/check-gui-sketching.py`
|
||||
|
||||
## 4. CONVERGENCE LOOP — ceiling 8 iterations
|
||||
EDIT (scoped) -> EXECUTE S1..S5 -> PARSE the ladder's per-tool assertions and the [UX]/[KEYTRACE]
|
||||
lines -> PATCH from the parsed cause. On ceiling without convergence: stop, report the last diff
|
||||
and the unresolved failure set. Do not report success.
|
||||
|
||||
F2P authoring exception: check-gui-click-edit.py is writable, and must be shown RED against
|
||||
unmodified source before any source edit counts.
|
||||
|
||||
## 5. TERMINATION CRITERIA
|
||||
- [ ] S2 exits 0, and introduces no compiler warning absent from the baseline.
|
||||
- [ ] S4 ALL_PASSED — every tool commits the typed value, no click into the field.
|
||||
- [ ] S5 shows zero regressions against its recorded baseline pass count.
|
||||
- [ ] F2P proven red without the fix (source stashed, ladder re-run, must FAIL).
|
||||
|
||||
## 6. GUARDRAILS
|
||||
Zero-assumption: no completion claim without captured stdout and exit codes. Oracle supremacy:
|
||||
the ladder's verdict overrides my judgement. Blast radius: §1 files only. Baseline obligation:
|
||||
run §3 once before the first edit and record it.
|
||||
@@ -1,117 +0,0 @@
|
||||
---
|
||||
name: orca-profiles
|
||||
description: Use when creating, modifying, reviewing or debugging OrcaSlicer FFF system profiles under resources/profiles, including printer/vendor/nozzle/material additions, bundle indexes and versions, preset renames, setting_id and filament_id. Also use for missing presets or vendors, ignored profile settings, ambiguous AMS filament matches, and failures from orca_profile_tool.py, check_profile.sh/.bat, OrcaSlicer_profile_validator or the Check profiles CI job.
|
||||
---
|
||||
|
||||
# OrcaSlicer system profiles
|
||||
|
||||
A bundle is `resources/profiles/<Vendor>.json` plus `<Vendor>/`. The vendor id is the
|
||||
filename stem, not the index's display `name`. The index is the loader's only entry point:
|
||||
unindexed presets never load. `OrcaFilamentLibrary` is the shared filament bundle;
|
||||
`blacklist.json` is data, not a bundle.
|
||||
|
||||
## Choose the reference for the task
|
||||
|
||||
Read the relevant reference before editing; load others only when the task crosses those areas.
|
||||
Paths below are relative to this skill. Commands run from the repository root.
|
||||
|
||||
| Task | Read |
|
||||
| --- | --- |
|
||||
| Add or tune a filament, brand or material; fix compatibility / alias shadowing | [filament-profiles.md](references/filament-profiles.md) |
|
||||
| Add a printer or nozzle; change models, variants, assets or extruder vectors | [machine-profiles.md](references/machine-profiles.md) |
|
||||
| Add a quality tier or tune a process | [process-profiles.md](references/process-profiles.md) |
|
||||
| Create a vendor bundle; diagnose loading or inheritance; migrate preset names | [vendor-bundle.md](references/vendor-bundle.md) |
|
||||
| Change ids; diagnose AMS identity | [ids.md](references/ids.md), then `docs/HLSD/filament_id.md` for identity changes |
|
||||
| Review a profile diff | [review-checklist.md](references/review-checklist.md) |
|
||||
| Run checks, interpret failures, test another tree or verify in the app | [validation.md](references/validation.md) |
|
||||
|
||||
## Golden rules
|
||||
|
||||
1. **Bump every changed bundle's `version`**, including `OrcaFilamentLibrary.json` when affected.
|
||||
Increment the last component; carry `.99` into the third component (`02.04.00.99` →
|
||||
`02.04.01.00`). The updater requires a strictly newer version. CI does not check this.
|
||||
2. **Register every preset, bases included, parents before children.** `update-index` generates
|
||||
the four `*_list` arrays; `check` requires its output. Index names must equal file `name` fields.
|
||||
3. **Generate ids; never invent or copy them.** Keep existing ids during ordinary tuning. New
|
||||
presets normally omit them until `generate-id`; bases must have no `setting_id`.
|
||||
BBL's authoritative `setting_id` and a wrongly inherited `filament_id` need the explicit
|
||||
handling in [ids.md](references/ids.md).
|
||||
4. **Load failures can discard a whole vendor bundle.** Broken `inherits`, missing indexed files,
|
||||
duplicate names, invalid model/variant references and unresolved filament ids affect more than
|
||||
the edited preset. Inheritance stays within a bundle, except filaments may inherit the library.
|
||||
5. **Preserve shipped selectable names.** Renaming, deleting or changing `instantiation` from
|
||||
`"true"` to `"false"` needs `renamed_from` on a selectable successor. It is a `;`-separated string;
|
||||
update in-tree references too. See [migration rules](references/vendor-bundle.md#renamed_from).
|
||||
6. **Compatibility uses exact printer variant names.** Every instantiated non-library filament
|
||||
needs a non-empty `compatible_printers` in its own file. Library fallbacks may omit it;
|
||||
library printer-specific tunes use a non-empty list. Keep same-product tunes disjoint.
|
||||
7. **Preset values are strings or arrays of strings.** Use `"instantiation": "false"`, not `false`.
|
||||
Model `nozzle_diameter` is a `;`-separated string; machine `nozzle_diameter` is an array.
|
||||
Wrong types can abort loading; see [failure scopes](references/vendor-bundle.md#failure-modes-ranked-by-blast-radius).
|
||||
8. **Verify setting keys against the code.** Unknown keys are silently discarded. Check
|
||||
`PrintConfig.cpp` definitions and `PrintConfigDef::handle_legacy`; neighbours can contain dead
|
||||
keys. `normalize` removes known obsolete keys, but does not detect arbitrary misspellings.
|
||||
9. **Run the full profile checks before reporting completion.** A vendor-scoped pass is only a
|
||||
development loop. Review also covers version bumps, assets, non-default processes and hardware
|
||||
tuning that CI cannot establish.
|
||||
|
||||
## Creating or modifying a profile
|
||||
|
||||
1. **Inspect the diff and neighbouring presets.** Read their `name`, parent chain and children;
|
||||
edits to a base or a leaf with descendants propagate. Match the bundle's structure and write
|
||||
only overrides. New files use tab indentation, LF and a trailing newline; preserve unrelated
|
||||
formatting in existing files. Match filename case exactly and use cross-platform names.
|
||||
2. **Author explicit metadata.** Set `type` yourself, especially for `machine` vs `machine_model`.
|
||||
Use `"from": "system"` and string `instantiation` on config presets. Omit ids on new presets
|
||||
unless [ids.md](references/ids.md) requires special handling; retain them on existing ones.
|
||||
Complete compatibility, defaults, assets and any rename migration using the task reference.
|
||||
3. **Bump the version**, then run the authoring commands in order for each affected bundle:
|
||||
|
||||
```bash
|
||||
python3 scripts/orca_profile_tool.py normalize --vendor "<Vendor>"
|
||||
python3 scripts/orca_profile_tool.py update-index --vendor "<Vendor>"
|
||||
python3 scripts/orca_profile_tool.py generate-id --vendor "<Vendor>"
|
||||
python3 scripts/orca_profile_tool.py check
|
||||
```
|
||||
|
||||
Writing commands support `--dry-run`. Inspect their diffs: `normalize` changes content and can
|
||||
reformat entire files. Stop and resolve command errors before proceeding.
|
||||
|
||||
**Do not use `trim` in this workflow:** it can delete newly authored, unindexed profiles.
|
||||
Do not use `normalize --force` for routine edits.
|
||||
4. **Validate:**
|
||||
|
||||
```bash
|
||||
./scripts/check_profile.sh --vendor "<Vendor>" # development loop
|
||||
./scripts/check_profile.sh # full tree before the PR
|
||||
```
|
||||
|
||||
On Windows use `py -3` instead of `python3`, and `scripts\check_profile.bat -Vendor "<Vendor>"`
|
||||
/ `scripts\check_profile.bat`. Logs: `.test/check_profiles/logs/<check>.log`.
|
||||
Id checks remain tree-wide under `--vendor`; filament-only bundles skip the default slice check.
|
||||
See [validation.md](references/validation.md) for flags, coverage and error remedies.
|
||||
5. **Verify the changed behavior.** Slice newly added non-default processes explicitly, and
|
||||
[test in the app](references/validation.md#testing-in-the-app) for selection or UI behavior.
|
||||
Report checks actually run, failures/skips and any hardware tuning still unverified.
|
||||
|
||||
## Symptom → first reference
|
||||
|
||||
| Symptom | Start here |
|
||||
| --- | --- |
|
||||
| A vendor disappears | Loader log / `validate_system`; [bundle failure scopes](references/vendor-bundle.md#failure-modes-ranked-by-blast-radius) |
|
||||
| A setting has no effect | Key spelling/type, `handle_legacy`, or a config key placed on a `machine_model` |
|
||||
| A preset exists but is not selectable | Index registration, `instantiation`, installation and compatibility |
|
||||
| A filament is missing, duplicated, or matches the wrong spool | [Compatibility and alias shadowing](references/filament-profiles.md#compatible_printers); [ids](references/ids.md) |
|
||||
| A bed temperature is ignored | [Plate-specific temperature keys](references/filament-profiles.md#bed-temperature-is-twelve-keys-not-one) |
|
||||
| A change is absent from the running app | Version bump and [installed profile location](references/validation.md#testing-in-the-app) |
|
||||
| A check fails | [Error → remedy](references/validation.md#error--remedy) |
|
||||
|
||||
## Source of truth
|
||||
|
||||
When guidance and behavior disagree, inspect the current checkout:
|
||||
`scripts/orca_profile_tool.py` for tooling and flags; `src/libslic3r/Preset*.cpp` for loading and
|
||||
compatibility; `src/libslic3r/PrintConfig.cpp` for setting types and legacy handling;
|
||||
`src/dev-utils/OrcaSlicer_profile_validator.cpp` and `.github/workflows/check_profiles.yml` for
|
||||
validation coverage. `docs/HLSD/filament_id.md` defines filament identity. The
|
||||
[profile development guide](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/how_to_create_profiles.md)
|
||||
is a tutorial; confirm loader and CLI details against these sources.
|
||||
@@ -1,207 +0,0 @@
|
||||
# Filament profiles and OrcaFilamentLibrary
|
||||
|
||||
`OrcaFilamentLibrary` is the filament-only bundle the loader reads **first**; its config map
|
||||
becomes the base bundle, so any vendor may inherit a library preset by name. It is the only cross-bundle
|
||||
parent — vendor-to-vendor inheritance always fails.
|
||||
|
||||
## Where a filament goes
|
||||
|
||||
| Contribution | Location |
|
||||
| --- | --- |
|
||||
| Generic material for all printers | `OrcaFilamentLibrary/filament/Generic <mat> @System.json` |
|
||||
| A brand's product, all printers | `OrcaFilamentLibrary/filament/<Brand>/` |
|
||||
| A brand's tune for one printer | `OrcaFilamentLibrary/filament/<Brand>/<PrinterVendor>/` — recommended; `<PrinterVendor>/filament/<Brand>/` also works |
|
||||
| A printer vendor's tune of a generic or its own product | `<Vendor>/filament/` |
|
||||
|
||||
Both locations for the last-but-one row are supported: `OrcaFilamentLibrary/filament/<Brand>/<PrinterVendor>/<Name>.json`
|
||||
(the shape the wiki shows) and `<PrinterVendor>/filament/<Brand>/`. The library path is the one a
|
||||
filament vendor should contribute to — `OrcaFilamentLibrary/filament/<Brand>/` is the brand's own
|
||||
folder, while a printer vendor's folder belongs to that printer vendor. Brand tunes do ship under
|
||||
printer vendors' folders today (Polymaker and SUNLU among others).
|
||||
|
||||
Library layout: `filament/base/fdm_filament_*.json` type roots, root-level `Generic <mat> @System.json`
|
||||
generics, and one subfolder per brand, which may nest printer-specific tunes one level deeper. Adding
|
||||
a brand means adding a folder here; the folder name is a directory label only — `filament_vendor` inside the JSON is the real vendor string.
|
||||
|
||||
## The three-part shape
|
||||
|
||||
```jsonc
|
||||
// Fiberon PA6-CF @base.json — the product root, holds identity + material values
|
||||
{ "type": "filament", "name": "Fiberon PA6-CF @base", "from": "system",
|
||||
"instantiation": "false", "inherits": "fdm_filament_pa",
|
||||
"filament_id": "OFkOviHk", // generated here; variants inherit it
|
||||
"filament_vendor": ["Polymaker"], "filament_type": ["PA6-CF"], /* … */ }
|
||||
|
||||
// Fiberon PA6-CF @System.json — the selectable shim, 7 keys
|
||||
{ "type": "filament", "name": "Fiberon PA6-CF @System", "from": "system",
|
||||
"instantiation": "true", "inherits": "Fiberon PA6-CF @base",
|
||||
"setting_id": "…", "compatible_printers": [] }
|
||||
|
||||
// <PrinterVendor>/filament/Polymaker/Fiberon PA6-CF @BBL X1C.json — a printer tune
|
||||
{ … "inherits": "Fiberon PA6-CF @base", "filament_max_volumetric_speed": ["14"],
|
||||
"compatible_printers": ["Bambu Lab X1 Carbon 0.4 nozzle", …] }
|
||||
```
|
||||
|
||||
- `@base` is the convention for a root. A base carries **no** `setting_id`, no `compatible_printers`, no
|
||||
`filament_settings_id`. Only the `setting_id` half is enforced, and nothing violates it; the other two
|
||||
are unchecked and plenty of bases still carry them. Do not copy that from a neighbouring file.
|
||||
- Every `@System` must be `"instantiation": "true"`. DREMC ships `@System` presets set to `"false"`,
|
||||
which therefore ship but can never be selected; no check catches it.
|
||||
- A duplicated brand `@base` across bundles is normal and intentional (`Fiberon PA6-CF @base` exists in
|
||||
both the library and BBL with the same id, differing only in MVS) — bases never enter the preset
|
||||
collection, so there is no duplicate-name error.
|
||||
- You may inherit from an instantiated preset as well as from a base; it is common.
|
||||
|
||||
## The two most common contributions
|
||||
|
||||
**A printer vendor tuning a generic.** Keep the `Generic X` base name so the alias shadows the library
|
||||
preset on your printers, inherit `Generic X @System`, declare **no** `filament_id` (inheriting the
|
||||
library's is correct — the product really is the library's generic), and give it a non-empty
|
||||
`compatible_printers` in its own body:
|
||||
|
||||
```jsonc
|
||||
// <Vendor>/filament/Generic PETG @Acme One 0.4 nozzle.json
|
||||
{ "type": "filament", "name": "Generic PETG @Acme One 0.4 nozzle", "from": "system",
|
||||
"instantiation": "true", "inherits": "Generic PETG @System",
|
||||
"filament_flow_ratio": ["0.95"], "filament_max_volumetric_speed": ["10"],
|
||||
"compatible_printers": ["Acme One 0.4 nozzle"] }
|
||||
```
|
||||
|
||||
**A printer vendor's own branded product.** Give it a `@base` root so `generate-id` can mint the id (see
|
||||
[ids.md](ids.md) — inheriting `Generic X @System` directly makes the id unfixable by the tool), then one
|
||||
instantiated leaf per printer in the same bundle. No `@System` shim: that is only for a product entering
|
||||
OrcaFilamentLibrary.
|
||||
|
||||
```jsonc
|
||||
// <Vendor>/filament/Acme Aura PETG @base.json — instantiation false, no setting_id
|
||||
{ "type": "filament", "name": "Acme Aura PETG @base", "from": "system",
|
||||
"instantiation": "false", "inherits": "fdm_filament_pet",
|
||||
"filament_vendor": ["Acme"], "filament_type": ["PETG"] } // filament_id minted here
|
||||
|
||||
// <Vendor>/filament/Acme Aura PETG @Acme One 0.4 nozzle.json
|
||||
{ "type": "filament", "name": "Acme Aura PETG @Acme One 0.4 nozzle", "from": "system",
|
||||
"instantiation": "true", "inherits": "Acme Aura PETG @base",
|
||||
"filament_max_volumetric_speed": ["11"],
|
||||
"compatible_printers": ["Acme One 0.4 nozzle"] }
|
||||
```
|
||||
|
||||
Omit `filament_settings_id` from new presets — it is runtime bookkeeping the app rewrites to the preset
|
||||
name.
|
||||
|
||||
## `compatible_printers`
|
||||
|
||||
- **Library fallbacks:** empty `[]` or absent, so they are offered on all printers except where
|
||||
[alias shadowing](#alias-shadowing) supplies a printer-specific tune.
|
||||
- **Library printer-specific tunes:** non-empty, listing exact printer **variant** names. These can
|
||||
supersede a same-alias fallback just like a tune in a printer vendor's bundle.
|
||||
- **Instantiated filaments in every other vendor:** non-empty, listing exact printer **variant** names.
|
||||
Enforced twice but not identically: the C++ `has_errors` reads the *flattened* config, so an inherited list satisfies it,
|
||||
while the Python check reads the file's **own** key. Write the list in the file itself. This is the
|
||||
most common filament CI failure.
|
||||
- Emptying it to "make it apply everywhere" fails that check *and* creates a duplicate-`filament_id`
|
||||
collision against the library generic on every printer.
|
||||
- Copying a base's full printer list onto a nozzle-specific variant produces duplicate combobox entries —
|
||||
a real shipped bug twice over.
|
||||
|
||||
## Alias shadowing
|
||||
|
||||
A printer-specific filament in either the library or a vendor bundle supersedes the library fallback
|
||||
on the printers it lists. The matching key is the **alias**: the preset name up to the **first** `@`,
|
||||
right-trimmed (no `@` → the whole name). So
|
||||
`QIDI ABS-GF@Q2-Series` aliases to `QIDI ABS-GF`.
|
||||
|
||||
A library preset with an empty `compatible_printers` collects, into `m_excluded_from`, every printer named
|
||||
by any same-alias preset that *has* a non-empty list, and is then hidden on those printers.
|
||||
|
||||
Two consequences:
|
||||
|
||||
- **Only an unrestricted library fallback can be shadowed.** Two printer-specific presets sharing
|
||||
an alias do not exclude each other — overlapping lists for the same product trip the
|
||||
duplicate-`filament_id` check instead.
|
||||
- This is why adding `Generic PLA @<printer>` to a vendor silently removes the library `Generic PLA`
|
||||
from that printer. Intended — and the reason a vendor tuning a generic must **keep the `Generic X`
|
||||
base name**.
|
||||
|
||||
The literal spelling `Generic <mat> @System` is load-bearing beyond shadowing: `find_preset2` rewrites an
|
||||
unresolved name containing "Generic" into that form and retries against the library, which is how 3MF
|
||||
and project recovery works.
|
||||
|
||||
## `filament_id`, `filament_vendor`, `filament_type`
|
||||
|
||||
`filament_id` is minted from the triple `(filament_vendor, filament_type, name-before-first-@)`.
|
||||
`filament_vendor` and `filament_type` are therefore **identity, not decoration** — editing either
|
||||
re-mints the id. Read `docs/HLSD/filament_id.md` before changing any of them, and see
|
||||
[ids.md](ids.md) for the tooling.
|
||||
|
||||
A filament with no resolvable `filament_id` anywhere in its `inherits` chain is a **hard load error** that
|
||||
discards the vendor bundle. The id inherits across bundles, so a vendor's `Generic ABS @X` inheriting
|
||||
`Generic ABS @System` gets the library's id for free; a vendor's own product must resolve its own.
|
||||
|
||||
- `filament_type` **must be a JSON array** — the one vector key the Python check enforces. A scalar
|
||||
`"PP"` once hung the filament/printer selection UI.
|
||||
- It is an **open** enum: an unlisted value is accepted silently and falls back to 190–300 °C defaults
|
||||
and adhesion 1.0. Off-list values do ship. Prefer a value from `MaterialType::all()` in
|
||||
`src/libslic3r/MaterialType.cpp`, or add a row there.
|
||||
- Generics use `filament_vendor: ["Generic"]`, which `fdm_filament_common` already defaults to.
|
||||
|
||||
## `"nil"`
|
||||
|
||||
Legal in any key whose `ConfigOptionDef` is `nullable`. In a filament preset that is most of the
|
||||
`filament_*` family, plus `long_retractions_when_ec` and `retraction_distances_when_ec`. About half are
|
||||
the extruder overrides (`filament_retraction_length`, `filament_z_hop`, `filament_wipe`,
|
||||
`filament_retract_*`, `filament_retraction_speed`, `filament_deretraction_speed`,
|
||||
`filament_retraction_minimum_travel`, `filament_wipe_distance`, `filament_long_retractions_when_cut`,
|
||||
`filament_retraction_distances_when_cut`, …), where `nil` means *keep the printer/extruder's own value*.
|
||||
The rest are ordinary nullable options (`filament_flow_ratio`, `filament_flush_temp`,
|
||||
`filament_adaptive_volumetric_speed`, …) where it means *unset*.
|
||||
|
||||
Anywhere else it throws `Deserializing nil into a non-nullable object`. To not set a non-nullable key,
|
||||
omit it — do not write `nil`.
|
||||
|
||||
## What to review per nozzle
|
||||
|
||||
Across `@X` / `@X 0.N nozzle` sibling pairs the keys that differ, most often first, are
|
||||
`filament_max_volumetric_speed`, `filament_retraction_length`, `slow_down_min_speed`,
|
||||
`filament_flow_ratio`, `slow_down_layer_time`, `nozzle_temperature` and `pressure_advance`.
|
||||
`filament_cost`, `filament_density`, `filament_type` and `filament_vendor` belong on the `@base` and
|
||||
should not appear in a printer tune.
|
||||
|
||||
Use measured values for the material, hotend, extruder and nozzle combination. Neither maximum
|
||||
volumetric speed nor pressure advance has a universal nozzle-only lookup table. When cloning a
|
||||
0.4 preset for a 0.2 nozzle, explicitly revisit flow limits; do not infer a pressure-advance value
|
||||
or a required direction of change from diameter alone.
|
||||
|
||||
## Style
|
||||
|
||||
Overrides, not full copies: a typical instantiated filament preset carries around a dozen non-meta keys,
|
||||
and a library leaf two or three. Presets that restate fifty-plus keys from their parent do still ship —
|
||||
Phrozen's single filament preset is that style — but they are the pattern to move away from, not to
|
||||
copy. Commit `6943b6ddc3` is the stated model (flip true bases to `instantiation: "false"`, strip
|
||||
`compatible_printers`/`setting_id`/`filament_settings_id`, add `renamed_from` on the survivor).
|
||||
|
||||
Prefer the library's `fdm_filament_*` bases over a vendor-local copy. Phrozen's local
|
||||
`fdm_filament_common` has drifted from the library's.
|
||||
|
||||
Canonical key order, written by `orca_profile_tool.py normalize` when it rewrites a file: `type`, `name`,
|
||||
`renamed_from`, `inherits`, `from`, `setting_id`, `filament_id`, `instantiation`, then everything else in
|
||||
the order you wrote it. Not enforced — a file that leads with `compatible_printers` passes `check`.
|
||||
|
||||
**Every vector-typed (`co*s`) key must be a JSON array.** Only `filament_type` is an outright error, but
|
||||
`normalize` silently arrayifies five more (`filament_cost`, `filament_density`,
|
||||
`temperature_vitrification`, `filament_max_volumetric_speed`, `filament_vendor`) and `check` fails when
|
||||
it would. Every other vector key is on you — including `filament_start_gcode`, `filament_end_gcode`,
|
||||
`filament_extruder_variant`, `compatible_printers` and the plate temperatures.
|
||||
|
||||
## Bed temperature is twelve keys, not one
|
||||
|
||||
There is no single "bed temperature". Which plate key applies depends on `curr_bed_type`, whose six
|
||||
selectable values (`btPC`, `btEP`, `btPEI`, `btPTE`, `btPCT`, `btSuperTack`; `btDefault` maps to no key)
|
||||
`get_bed_temp_key()` turns into `cool_plate_temp`, `eng_plate_temp`, `hot_plate_temp`,
|
||||
`textured_plate_temp`, `textured_cool_plate_temp` and `supertack_plate_temp` — each with an
|
||||
`*_initial_layer` twin.
|
||||
|
||||
`textured_cool_plate_temp` is the one most often forgotten. A printer with `support_multi_bed_types` off
|
||||
hides the selector, and the printer preset's
|
||||
`default_bed_type` decides which plate is selected for it, but `curr_bed_type` can still hold a stale
|
||||
value carried over from another printer — so set every plate the printer plausibly has, as the sibling
|
||||
presets in the bundle do.
|
||||
@@ -1,149 +0,0 @@
|
||||
# `setting_id` and `filament_id`
|
||||
|
||||
Orca-generated ids are deterministic hashes of identity. **Never invent an id or copy a sibling's
|
||||
`setting_id`.** Use `scripts/orca_profile_tool.py`; the two special cases are
|
||||
[a wrongly inherited filament id](#what-generate-id-does-and-does-not-fix) and
|
||||
[BBL's authoritative setting ids](#bbls-exception-precisely).
|
||||
|
||||
`docs/HLSD/filament_id.md` is the authoritative design document for `filament_id` — the id landscape, the
|
||||
checks CI runs, and the Bambu catalog map. This page is the tooling half.
|
||||
|
||||
| | `setting_id` | `filament_id` |
|
||||
| --- | --- | --- |
|
||||
| Identifies | one selectable preset | one filament **product** |
|
||||
| Key hashed | `<vendor folder>/<type>/<name>` | `filament_product/<filament_vendor>/<filament_type>/<name-before-@>` |
|
||||
| Shape | 16 base62 chars | `OF` + 6 base62 chars |
|
||||
| Required on | every `instantiation: "true"` preset | every **instantiated** filament, own or inherited |
|
||||
| Forbidden on | bases (`instantiation != "true"`) | — (a base is exactly where it belongs) |
|
||||
| Scope | globally unique across the tree | shared by every variant of the product, in every bundle |
|
||||
|
||||
`<type>` is `machine` / `process` / `filament` — the vendor is the **folder** name (`BBL`), not the
|
||||
display name (`Bambulab`). Renaming a preset changes its `setting_id`; renaming a filament, or editing
|
||||
its `filament_vendor` or `filament_type`, also changes its `filament_id`.
|
||||
|
||||
## The tool
|
||||
|
||||
Use `scripts/orca_profile_tool.py` with a subcommand:
|
||||
|
||||
| Command | Does |
|
||||
| --- | --- |
|
||||
| `check` | everything CI's `profile_tool` step runs — see [validation.md](validation.md) |
|
||||
| `generate-id` | writes `setting_id` and `filament_id` |
|
||||
| `normalize` | rewrites profile files into their canonical shape |
|
||||
| `trim` | deletes profile files no `<vendor>.json` list references |
|
||||
| `update-index` | rebuilds the `*_list` sections from the files on disk |
|
||||
|
||||
The order after adding, renaming or deleting files — each step feeds the next, so it is not
|
||||
interchangeable — is `normalize` → `update-index` → `generate-id` → `check`.
|
||||
The [authoring workflow](../SKILL.md#creating-or-modifying-a-profile) has the commands.
|
||||
|
||||
> **`trim` deletes.** It removes every profile file the index does not list — including the one you just
|
||||
> added and have not registered yet. Register first, or skip `trim` entirely; it is a cleanup sweep, not
|
||||
> part of landing a profile. Preview with `--dry-run`.
|
||||
|
||||
**Register, then mint.** The `filament_id` pass reads `<Vendor>.json`'s `filament_list`, not the
|
||||
filesystem (the `setting_id` pass walks the filesystem, so a bundle whose index has not landed yet is
|
||||
still assignable). A new filament file is therefore invisible to `generate-id`'s filament_id pass until
|
||||
it is registered — its `setting_id` is written regardless.
|
||||
|
||||
- `--dry-run` works on every writing command (`generate-id`, `normalize`, `trim`, `update-index`)
|
||||
and writes nothing.
|
||||
- `--filament-id` / `--setting-id` narrow `generate-id`; they exclude each other, and passing neither
|
||||
writes both.
|
||||
- `--vendor` is repeatable and narrows **only what is written** — the id is a function of the triple
|
||||
alone, so a narrowed run writes exactly what a full run would. An unknown vendor exits 1 before any
|
||||
write. `--vendor` on `check` narrows the per-vendor checks only; the `setting_id` and `filament_id`
|
||||
passes stay tree-wide.
|
||||
- `--profiles DIR` points any command at another tree — see
|
||||
[Checking a copy of the tree](validation.md#checking-a-copy-of-the-tree).
|
||||
- `--profile-type` narrows `normalize`, `trim` and `update-index` to `machine_model`, `process`,
|
||||
`filament` or `machine`.
|
||||
- Exit codes: 0 clean, 1 errors found (`generate-id` still writes what it could), 2 argparse misuse.
|
||||
- Output is ANSI-coloured; searching for the literal `[ERROR]` still works.
|
||||
|
||||
`generate-id` is **idempotent and byte-preserving** — BOM and CRLF kept, one key line touched per pass.
|
||||
A legitimate `generate-id` diff is one or two changed lines per file: a new instantiated filament gets
|
||||
both a `filament_id` and a `setting_id`, and a BBL file with a misspelled `settings_id` has that line
|
||||
dropped and its value restored under the right key. `normalize` is the opposite by design — it rewrites
|
||||
whole files into canonical shape — which is why `check` demands it already be a no-op. Some bundles have
|
||||
CRLF committed (OrcaFilamentLibrary, Anycubic and RH3D among them), so a `normalize` pass there rewrites
|
||||
every line — read the diff before committing it.
|
||||
|
||||
On a clean tree `check` and `generate-id --dry-run` both exit 0 with zero findings. That is the
|
||||
baseline to restore before opening a PR.
|
||||
|
||||
## What `generate-id` does and does not fix
|
||||
|
||||
Writes:
|
||||
|
||||
- a `setting_id` into any instantiated preset that lacks one, or whose value does not match the formula;
|
||||
- strips a `setting_id` from a base;
|
||||
- deletes the misspelled `settings_id` key;
|
||||
- a `filament_id` into the id-less **root(s)** of an instantiated filament that resolves none;
|
||||
- rewrites a **declared** `filament_id` that is not the mint of its own triple.
|
||||
|
||||
Refuses to write (reports only): a base62 collision between two products, an empty `filament_vendor` or
|
||||
`filament_type`, a broken `inherits` chain, roots of one filament resolving divergent `(vendor, type)`
|
||||
pairs.
|
||||
|
||||
**Does not fix: a preset that *inherits* a wrong `filament_id`.** This is check 2b, and it is the trap
|
||||
most likely to bite. It happens when a branded filament inherits a generic for its settings:
|
||||
|
||||
```jsonc
|
||||
{ "name": "Phrozen Aura PETG @Phrozen Arco 0.4 nozzle",
|
||||
"inherits": "Generic PETG @System" } // resolves the OFL generic's id — wrong product
|
||||
```
|
||||
|
||||
The preset resolves *an* id, so `generate-id` neither inserts nor rewrites, and `check` fails with
|
||||
`inherits filament_id "X" but its own triple "V/T/N" mints "Y"`.
|
||||
|
||||
Two fixes, in order of preference:
|
||||
|
||||
1. **Give the product a `@base` root** inheriting a material base (`fdm_filament_pet`,
|
||||
`fdm_filament_pla`, …). No `fdm_filament_*` base carries a `filament_id`, so the filament now resolves
|
||||
none and `generate-id` mints it for you. This is also the shape the rest of the tree uses.
|
||||
2. **Declare the tool-computed key on the preset itself.** Use the expected value reported by `check`
|
||||
or compute it with the function below; this is not a manually chosen id. Make sure the preset
|
||||
resolves the right `filament_vendor` and `filament_type` first — with
|
||||
neither set, the triple resolves through the generic parent and the branded product is minted
|
||||
under vendor `Generic`. If you need the id before the file exists:
|
||||
|
||||
```bash
|
||||
python3 -c "import sys; sys.path.insert(0,'scripts'); from orca_profile_tool import generate_filament_id as g; print(g('Polymaker','PLA','PolyLite PLA'))"
|
||||
# -> OF5CgdDq
|
||||
```
|
||||
|
||||
The quoting works unchanged in cmd and PowerShell; only swap `python3` for `py -3`.
|
||||
|
||||
The `setting_id` equivalent is `generate_preset_setting_id('<vendor folder>', '<type>', '<name>')`.
|
||||
|
||||
## BBL's exception, precisely
|
||||
|
||||
`RESERVED_VENDORS = {"BBL"}` covers **`setting_id` assignment only**, keyed on the *folder* name:
|
||||
|
||||
- The tool never mints or replaces a BBL `setting_id`. A new instantiated BBL preset with no
|
||||
`setting_id` therefore **cannot be fixed by the tool**, yet the presence rule still applies to it —
|
||||
carry over Bambu's authoritative id by hand.
|
||||
- BBL is not exempt from anything else: bases still get their `setting_id` stripped, ids must still be
|
||||
globally unique, and BBL `filament_id`s are minted like everyone else's — every one of them is an
|
||||
`OF*`.
|
||||
|
||||
## Ids other systems compose
|
||||
|
||||
No id from another system is the mint of a triple, so `check` rejects it like any other bad id — same
|
||||
error, same remedy, whoever wrote it. Three such spaces exist near the tree; recognise them so you do
|
||||
not copy one into a profile:
|
||||
|
||||
- **Bambu's `GF*` catalog** — external and opaque, correlated to Orca's ids by the generated
|
||||
`resources/printers/bambu_filament_ids.json`. `GF` is a *prefix*, not a spelling the tree avoids: most
|
||||
BBL `setting_id`s start with `G`, and `blacklist.json` and
|
||||
`BBL/filament/filaments_color_codes.json` both reference Bambu catalog ids by design. The rule is
|
||||
about `filament_id` and nothing else.
|
||||
- **Qidi's `QD_*`** — composed at runtime by the box (`QD_<series>_<vendor>_<typeidx>`), not a preset id.
|
||||
- **`P` + 7 hex, and `"null"`** — what `CreatePresetsDialog.cpp` gives a *user*-created filament.
|
||||
|
||||
## Tests
|
||||
|
||||
`python3 -m unittest discover -s scripts/tests -t scripts` (`py -3 -m …` on Windows). Note the
|
||||
`-t scripts` argument; without it the imports fail. CI runs them as the first, non-`continue-on-error`
|
||||
step of the profile job — see [validation.md](validation.md#ci).
|
||||
@@ -1,194 +0,0 @@
|
||||
# Printer models and variants
|
||||
|
||||
Both live in `resources/profiles/<Vendor>/machine/*.json`; models go in `machine_model_list`, variants
|
||||
and shared bases in `machine_list`. Every one of them is registered. Some vendors (Elegoo, Eryone,
|
||||
InfiMech, FlyingBear) nest a further subfolder under `machine/`, so recurse rather than globbing
|
||||
`machine/*.json`.
|
||||
|
||||
## A `machine_model` is not a config preset
|
||||
|
||||
It is parsed by a hand-written key switch, and only these keys are stored (`version` and `url` are
|
||||
matched and discarded):
|
||||
|
||||
`name`, `model_id`, `nozzle_diameter`, `machine_tech`, `family`, `bed_model`, `bed_texture`,
|
||||
`hotend_model`, `default_materials`, `not_support_bed_type`, `image_bed_type`,
|
||||
`bottom_texture_end_name`, `bottom_texture_rect`, `bottom_texture_rect_longer`, `middle_texture_rect`,
|
||||
`use_double_extruder_default_texture`.
|
||||
|
||||
**Everything else is silently dropped.** Only `name` and `nozzle_diameter` are required. Dead keys ship
|
||||
on real models today — `url`, `default_bed_type`, even a `desciption` typo — so a neighbour carrying a
|
||||
key is no evidence it does anything. Printer config options belong on the `machine` preset, never here.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "machine_model",
|
||||
"name": "Phrozen Arco",
|
||||
"machine_tech": "FFF",
|
||||
"family": "Phrozen",
|
||||
"model_id": "Phrozen Arco",
|
||||
"nozzle_diameter": "0.4",
|
||||
"bed_model": "Phrozen Arco_buildplate_model.stl",
|
||||
"bed_texture": "Phrozen Arco_buildplate_texture.svg",
|
||||
"hotend_model": "",
|
||||
"default_materials": "Generic PLA @Phrozen Arco 0.4 nozzle"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Notes |
|
||||
| --- | --- |
|
||||
| identity | **the `name` of the `machine_model_list` entry**, which is what a variant's `printer_model` must equal. `check_name_consistency` forces it to equal the file's `name`, so they coincide. |
|
||||
| `model_id` | a *separate* cloud/device printer type. Optional, and not required to be unique. Not the model's identity. Changing it changes device matching. |
|
||||
| `machine_tech` | only `starts_with("SL")` means SLA; everything else is FFF. Write `FFF`; a few models write `FGF`, which is a label with no effect. |
|
||||
| `nozzle_diameter` | `;`-separated string, one token per available size. Order is free (Qidi writes `0.4;0.2;0.6;0.8` to put the default first). This list is the authoritative set of legal `printer_variant` values. |
|
||||
| `default_materials` | `;`-separated filament **preset names**. Used to preselect in the wizard *and* by `PresetBundle::load_installed_filaments` to auto-install a printer's filaments on first run, so a dangling entry costs a real user a filament. Not `,`; case-sensitive (`@System`). `check` fails on a dangling name here or in `default_filament_profile`. |
|
||||
| `family` | a wizard grouping label only; give every model one. |
|
||||
|
||||
### Assets
|
||||
|
||||
`bed_model`, `bed_texture` and `hotend_model` are paths relative to the **vendor folder** (by id).
|
||||
Majority convention: `<Model>_buildplate_model.stl` and `<Model>_buildplate_texture.svg`. An empty string
|
||||
is the legal "none", and is the norm for `hotend_model`.
|
||||
|
||||
**Nothing checks that the file exists.** A missing `hotend_model` falls back to
|
||||
`resources/profiles/hotend.stl`; a missing `bed_model`/`bed_texture` just renders nothing. Broken
|
||||
references already ship. Verify by hand.
|
||||
|
||||
Every model also has a `<Model>_cover.png` in the vendor folder — treat it as required, not optional.
|
||||
240×240 is the cap `scripts/optimize_cover_images.py` enforces and the size most covers already use.
|
||||
A missing cover degrades to a placeholder in both the wizard and the sidebar.
|
||||
|
||||
## The `machine` variant
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "machine",
|
||||
"name": "Phrozen Arco 0.4 nozzle",
|
||||
"inherits": "fdm_machine_common",
|
||||
"from": "system",
|
||||
"setting_id": "lvaYKTUZr5C9jSwk",
|
||||
"instantiation": "true",
|
||||
"printer_model": "Phrozen Arco",
|
||||
"printer_variant": "0.4",
|
||||
"nozzle_diameter": ["0.4"],
|
||||
"default_print_profile": "0.20mm Standard @Phrozen Arco 0.4 nozzle",
|
||||
"default_filament_profile": ["Generic PLA @Phrozen Arco 0.4 nozzle"],
|
||||
"printable_area": ["0x0", "300x0", "300x300", "0x300"],
|
||||
"printable_height": "300"
|
||||
}
|
||||
```
|
||||
|
||||
Minimum viable key set: `type`, `name`, `from`, `instantiation`, `setting_id`, `inherits`,
|
||||
`printer_model`, `printer_variant`, `nozzle_diameter`, `printable_area`, `printable_height`,
|
||||
`default_print_profile`. The four keys without which the preset will not load at all are `name`,
|
||||
`instantiation`, `printer_model` and `printer_variant`; `default_filament_profile` is an array
|
||||
(`["Generic PLA @System"]`) and the model's `default_materials` a `;`-separated string. Unlike a
|
||||
`machine_model`, a `machine` **is** config-loaded, so a key belonging to another preset type is a
|
||||
reported error (a misspelled key is still silent).
|
||||
|
||||
### `printer_variant` — three hard rules
|
||||
|
||||
1. Non-empty, and an exact member of the model's `;`-separated `nozzle_diameter` list.
|
||||
2. `printer_model` non-empty and naming a model of this vendor.
|
||||
3. In validation mode, for instantiated presets only: split `printer_variant` on `+`, each token must
|
||||
start with a number (a trailing non-numeric suffix such as `HF` is ignored), and the resulting **set**
|
||||
must equal `set(nozzle_diameter)`.
|
||||
|
||||
Rules 1 and 2 are loader-enforced — failing either drops the preset *and* the whole bundle. Rule 3 only
|
||||
raises a validation error: the preset still loads, but the validator exits non-zero.
|
||||
|
||||
`nozzle_diameter` lists one entry **per physical nozzle**; `printer_variant` lists the **distinct**
|
||||
diameters joined with `+`. Snapmaker U1 is the worked case: `["0.4","0.4","0.6","0.6"]` against
|
||||
`"0.4+0.6"` — it passes because the comparison is on sets.
|
||||
|
||||
The conventional values are `0.2`, `0.25`, `0.4`, `0.5`, `0.6`, `0.8` and `1.0`. Suffixed forms
|
||||
(`0.4HF`, `0.6HF`, `0.8HF`, `0.4HS`) are Flashforge-only and the `+` form is rare. A variant is **not**
|
||||
required to be unique within a model — Volumic ships `EXO42 IDRE`, `… COPY MODE` and `… MIRROR MODE` all
|
||||
at `0.4` under the one model `EXO42 IDRE`.
|
||||
|
||||
The converse is **unchecked**: a nozzle size in the model's list with no matching variant is offered in
|
||||
the wizard and resolves to nothing. `Wanhao France`'s `D12 500 PRO M2 DIRECT` ships that bug today.
|
||||
|
||||
### Other fields worth knowing
|
||||
|
||||
- `default_print_profile` is a **scalar**, matched by exact preset name. Not a `;` list. The named
|
||||
process must be compatible with this printer through its resolved list or condition.
|
||||
`validate_slice` attempts to select it and rejects generic Default fallbacks, but compatibility
|
||||
updates can choose another compatible preset. Check the exact default reference yourself.
|
||||
- `default_filament_profile` is an **array**, one name per element.
|
||||
- `printable_area` is an array of `"XxY"` strings — four points for a rectangle, one per segment for a
|
||||
delta or circular bed.
|
||||
- `gcode_flavor` is usually set once in the base; `klipper`, `marlin`, `marlin2` and `reprapfirmware`
|
||||
cover nearly every shipped printer.
|
||||
- `printer_settings_id` is junk — most files carrying it disagree with their own name. Do not copy it
|
||||
when cloning a bundle.
|
||||
- `min_layer_height` / `max_layer_height` are **machine** keys (per extruder), never process keys.
|
||||
|
||||
## Bases
|
||||
|
||||
Nearly every machine-bearing vendor registers a base literally named `fdm_machine_common`, and Klipper
|
||||
vendors add `fdm_klipper_common` on top of it. Two levels is the usual depth.
|
||||
|
||||
**There is no leading-underscore convention for bases.**
|
||||
|
||||
## Adding a printer to an existing bundle
|
||||
|
||||
1. Choose the names first — model, variant(s), process(es); everything else references them.
|
||||
2. Add the model (`machine_model_list`) and one `machine` variant per nozzle; the minimum key sets are
|
||||
above. Bed assets and `<Model>_cover.png` go directly in `<Vendor>/`.
|
||||
3. Add at least one process per variant naming it in `compatible_printers`
|
||||
([process-profiles.md](process-profiles.md#adding-a-quality-tier-or-a-nozzles-processes)).
|
||||
4. Register everything (or run `update-index`), bump the version, run the id tool, validate.
|
||||
|
||||
## Adding a nozzle variant
|
||||
|
||||
1. Extend the model's `nozzle_diameter` (`"0.4"` → `"0.4;0.6"`).
|
||||
2. Add the variant preset. Either inherit the shared base (the usual choice) or the 0.4 sibling (Elegoo,
|
||||
BBL, Prusa and Qidi do this — smaller diff, but the sibling's edits now reach this file too).
|
||||
3. Override what actually changes with nozzle: `nozzle_diameter`, `printer_variant`,
|
||||
`default_print_profile`, `default_filament_profile`, `min_layer_height`/`max_layer_height`, and
|
||||
retraction if the vendor tunes it.
|
||||
4. Add at least one process for the new nozzle — see [process-profiles.md](process-profiles.md).
|
||||
5. Register both, bump the version, run the id tool, validate.
|
||||
|
||||
## Multi-extruder, IDEX and tool-changers
|
||||
|
||||
Per-extruder vectors are **silently resized** to the nozzle count, with no error. Padding repeats the
|
||||
**first** value, not the last — `["0.4","0.6"]` on a 4-nozzle machine becomes `0.4, 0.6, 0.4, 0.4`.
|
||||
Longer vectors are truncated.
|
||||
|
||||
Note the two sizing families: the plain per-extruder keys (`extruder_offset`, `extruder_colour`,
|
||||
`extruder_printable_height`, `min_layer_height`, `max_layer_height`, `nozzle_diameter`) are sized to the
|
||||
extruder count, while `printer_options_with_variant_1` (`retraction_length`, `z_hop`, `wipe`,
|
||||
`nozzle_type`, the rest of the retraction family) is sized to `printer_extruder_variant` instead.
|
||||
|
||||
- Give **one entry per extruder** for ordinary per-extruder vectors such as `extruder_offset`,
|
||||
`extruder_colour`, `min_layer_height` and `max_layer_height`; size the variant-dependent family
|
||||
to `printer_extruder_variant` instead.
|
||||
A single `["0x0"]` `extruder_offset` on a dual or multi-tool machine — which already ships — pads every
|
||||
toolhead to the same offset, so the offset never applies.
|
||||
- Overriding `nozzle_diameter` to a different count without re-stating every per-extruder vector is the
|
||||
other half of the trap — `Snapmaker U1 (0.4+0.6 nozzle)` inherits 5-entry vectors against 4 nozzles.
|
||||
|
||||
Copy targets: `Custom/machine/fdm_toolchanger_common.json` + `Custom/machine/MyToolChanger 0.4
|
||||
nozzle.json` (a clean minimal variant on a base that gives every vector five entries), and
|
||||
`Ratrig/machine/RatRig V-Core 4 IDEX 300 0.4 nozzle.json` for IDEX. The BBL extruder-variant machinery
|
||||
(`extruder_variant_list`, `printer_extruder_id`, `default_nozzle_volume_type`) is used by a handful of
|
||||
vendors — do not copy it into a new bundle (`nozzle_volume_type` itself is not a machine-preset key).
|
||||
|
||||
## Custom G-code
|
||||
|
||||
The keys are `machine_start_gcode`, `machine_end_gcode`, `change_filament_gcode`,
|
||||
`machine_pause_gcode`, `before_layer_change_gcode` and `layer_change_gcode`. Both a single string with
|
||||
embedded `\n` and a JSON array of lines are legal and both are in use — do not convert one into the
|
||||
other. Conditionals are `{if …}` / `{elsif …}` / `{else}` / `{endif}`; `{elsif}` is rare but real (Qidi's
|
||||
`layer_change_gcode` uses it).
|
||||
|
||||
Placeholder errors only surface when the config is actually expanded, which means `validate_slice`:
|
||||
|
||||
```bash
|
||||
./scripts/check_profile.sh --vendor "<Vendor>" validate_slice
|
||||
# Windows: scripts\check_profile.bat -Vendor "<Vendor>" validate_slice
|
||||
```
|
||||
|
||||
What the sweep covers is in [validation.md](validation.md#validate_slice); no `CP TOOLCHANGE START` in
|
||||
the output means `change_filament_gcode` never expanded.
|
||||
@@ -1,145 +0,0 @@
|
||||
# Process profiles
|
||||
|
||||
Processes live in `resources/profiles/<Vendor>/process/` — selectable leaves and shared bases alike, and
|
||||
every one of them is registered in `process_list`. There are no global processes shared across vendors.
|
||||
|
||||
## Naming
|
||||
|
||||
`"<layer height>mm <quality> @<target>"` — near-universal, so match it.
|
||||
|
||||
Follow the bundle's existing quality vocabulary. BBL's common ladder relates the quality word to
|
||||
the layer-height / nozzle ratio; it is a naming convention, not a loader constraint:
|
||||
|
||||
| Quality | Ratio | 0.2 nozzle | 0.4 | 0.6 | 0.8 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Extra Fine | 0.2× | — | 0.08 | — | — |
|
||||
| Fine | 0.3× | 0.06 | 0.12 | 0.18 | 0.24 |
|
||||
| Optimal | 0.4× | 0.08 | 0.16 | 0.24 | 0.32 |
|
||||
| Standard | 0.5× | 0.10 | 0.20 | 0.30 | 0.40 |
|
||||
| Draft | 0.6× | 0.12 | 0.24 | 0.36 | 0.48 |
|
||||
| Extra Draft | 0.7× | 0.14 | 0.28 | 0.42 | 0.56 |
|
||||
|
||||
This is the `fdm_process_single_<lh>_nozzle_<n>` ladder; 0.4 is commonly the unsuffixed nozzle default.
|
||||
Match neighbouring names rather than renaming shipped tiers to fit the table.
|
||||
|
||||
The `@target` is a human label, not a reference: most do not equal any real printer variant name.
|
||||
Compatibility comes from the resolved list or condition, not this label.
|
||||
|
||||
## Shape
|
||||
|
||||
A selectable leaf's only truly universal keys are `type`, `setting_id`, `name` and `instantiation`;
|
||||
`inherits` and `from` are near-universal — plus compatibility. No slicing key is universal; even
|
||||
`layer_height` is more often inherited than restated. A base has `type`, `name`, `instantiation`, almost
|
||||
always `from`, and **no** `setting_id`.
|
||||
|
||||
**Target shape: a 7-key leaf.** `OrcaArena` is the cleanest model —
|
||||
`fdm_process_common` → `fdm_process_arena_common` → `fdm_process_arena_<lh>_nozzle_<n>` → leaf, where the
|
||||
leaf carries only `type`, `name`, `inherits`, `from`, `setting_id`, `instantiation`,
|
||||
`compatible_printers`, and the per-nozzle base holds the layer height and all eight line widths.
|
||||
|
||||
BBL, WonderMaker and Z-Bolt are uniform in *layering* — every leaf inherits a base, names its printers
|
||||
directly and holds no layer height of its own — but not in key count. Imitate BBL's layering, not its
|
||||
content: its leaves carry doubled `print_extruder_variant` arrays that no single-variant vendor needs.
|
||||
|
||||
Nearly every vendor ships its own `fdm_process_common` as the inherits-less root. Those files are not
|
||||
identical; copying another vendor's version into a new bundle is normal.
|
||||
|
||||
Beware leaf-inherits-leaf: Prusa chains several levels deep through sibling leaves, and Elegoo and
|
||||
Flashforge do it too, so editing one selectable process silently changes others. Check a leaf's children
|
||||
before editing it.
|
||||
|
||||
## Compatibility
|
||||
|
||||
Most leaves set `compatible_printers` directly; some inherit it from a base, and Prusa's fall through to
|
||||
`compatible_printers_condition`. After resolving `inherits`, **every selectable process has one or the
|
||||
other** — that is the invariant to review against. Unlike filaments, inheriting `compatible_printers` is
|
||||
legitimate for a process, and no check enforces its presence.
|
||||
|
||||
- A non-empty `compatible_printers` makes `compatible_printers_condition` **dead code**. Use one or
|
||||
the other.
|
||||
- A condition that fails to parse means *compatible with everything* — a warning, not an error. A typo
|
||||
widens compatibility instead of narrowing it.
|
||||
- Matching is `boost::regex` **`regex_match`** — a full-string match, which is why every shipped
|
||||
condition wraps its keyword in `.*`. Because it is boost rather than `std`, `.` also spans the newlines
|
||||
inside `printer_notes`.
|
||||
- A `printer_notes` keyword that prefixes another model's keyword matches both. Prusa guards it:
|
||||
|
||||
```
|
||||
printer_notes=~/.*PRINTER_MODEL_COREONE[^_a-zA-Z0-9].*/ and nozzle_diameter[0]==0.4 and printer_notes=~/.*HF_NOZZLE.*/
|
||||
```
|
||||
|
||||
The `[^_a-zA-Z0-9]` exists because `PRINTER_MODEL_COREONE_L` also contains `PRINTER_MODEL_COREONE`.
|
||||
|
||||
`compatible_printers` is almost always one element. A leaf listing a whole model family is where a newly
|
||||
added printer is usually forgotten.
|
||||
|
||||
## What to review per nozzle
|
||||
|
||||
| Key group | Review |
|
||||
| --- | --- |
|
||||
| `line_width` and per-region widths | resolved widths suit the nozzle and layer height |
|
||||
| `layer_height`, `initial_layer_print_height` | within the printer's limits |
|
||||
| print speeds | consistent with flow limits and hardware tuning |
|
||||
| shell layers, wall loops, accelerations, support Z distances | preserve the intended thickness, motion and support behavior |
|
||||
|
||||
**A common starting pattern is nozzle + 0.02 mm**: 0.22 / 0.42 / 0.62 / 0.82 / 1.02. In that pattern, at 0.4,
|
||||
`inner_wall_line_width`, `sparse_infill_line_width`, `skin_infill_line_width` and
|
||||
`skeleton_infill_line_width` widen to 0.45 and `initial_layer_line_width` to 0.5; at 0.2,
|
||||
`initial_layer_line_width` widens to 0.25. Also derived, and easily missed:
|
||||
`ironing_inset = line_width / 2` (0.11 / 0.21 / 0.31 / 0.41).
|
||||
These are examples, not required values; preserve intentional vendor tuning and percentage/automatic
|
||||
widths, and validate their resolved values.
|
||||
|
||||
`min_layer_height` and `max_layer_height` are machine keys — no process file sets them.
|
||||
|
||||
## Slice-time content checks
|
||||
|
||||
`Print::validate()` enforces four rules at slice time:
|
||||
|
||||
1. `initial_layer_print_height` ≤ min `nozzle_diameter`
|
||||
2. `layer_height` ≤ min `nozzle_diameter` — *"Layer height cannot exceed nozzle diameter."*
|
||||
3. `line_width` and the seven per-region widths (inner/outer wall, sparse infill, internal solid infill,
|
||||
top surface, skin, skeleton) > `layer_height` — *"Line width too small"*. `support_line_width` only
|
||||
when the object has support or a raft; `initial_layer_line_width` is never checked.
|
||||
4. every width ≤ 5 × max `nozzle_diameter` — *"Line width too large"*
|
||||
|
||||
Two further rules cover `bridge_line_width` (≤ nozzle diameter; > `layer_height` unless `thick_bridges`
|
||||
and `thick_internal_bridges` are both on). The sweep starts from printer defaults rather than
|
||||
enumerating every process. **A new non-default process gets no dedicated slice coverage in CI.**
|
||||
|
||||
## What CI checks on a process
|
||||
|
||||
Structure, not content: `process_list` name consistency **and** index coverage the other way, two files
|
||||
claiming one process name, the `extruder_clearance_radius` / `extruder_clearance_max_radius` conflict
|
||||
pair, duplicate JSON keys, a file `normalize` would rewrite, and the five `setting_id` rules (the fifth
|
||||
rejects the misspelled key `settings_id`). `compatible_printers` presence is checked for **filaments
|
||||
only**.
|
||||
|
||||
Note the C++ loader derives a missing `setting_id` on the fly, so the validator will not fail a process
|
||||
without one — only `orca_profile_tool.py check` catches it. Running the validator alone gives a false
|
||||
all-clear.
|
||||
|
||||
## Silent failures specific to processes
|
||||
|
||||
- **Unknown or misspelled keys are discarded with no error and no warning.** They ship all over the
|
||||
process tree, both plain typos (`inital_layer_height`, `tree_support_bramch_diameter_angle`,
|
||||
`sparse_infill_patter`) and keys copied from other slicers that Orca never defined.
|
||||
- Keys on the tool's `OBSOLETE_KEYS` list (`adaptive_layer_height`, `overhang_totally_speed`, …) are
|
||||
rejected by `check`'s normalization pass across preset types; `normalize` removes them.
|
||||
The additional per-key obsolete warnings read `filament/` only.
|
||||
- A dangling `compatible_printers` inside an `instantiation: "false"` base is invisible to
|
||||
`check_preset_references`: a base never becomes a `Preset` at all (its config goes into `config_maps`
|
||||
and the loader returns early), so it is in no collection for the check to walk.
|
||||
- Orphan bases that nothing inherits are scattered through the tree — usually the leftover of a
|
||||
half-finished nozzle addition.
|
||||
|
||||
## Adding a quality tier or a nozzle's processes
|
||||
|
||||
1. Choose the layer height and quality label using the vendor's existing ladder.
|
||||
2. If the vendor has per-nozzle bases, add one (`fdm_process_<vendor>_<lh>_nozzle_<n>`) with the layer
|
||||
height, nozzle-appropriate line widths, `initial_layer_print_height` and `ironing_inset`.
|
||||
3. Add the leaf: 7 keys, `compatible_printers` naming the exact printer variant(s).
|
||||
4. Register both in `process_list`, parent first. Bump the version, run the id tool, validate.
|
||||
5. Slice this process explicitly with its intended printer; the sweep gives non-default tiers no
|
||||
dedicated coverage. If it is a printer's `default_print_profile`, verify the exact name and
|
||||
resolved compatibility too — the sweep may fall back or select another compatible process.
|
||||
@@ -1,177 +0,0 @@
|
||||
# Reviewing a profile change
|
||||
|
||||
Start with delivery, identity and backward compatibility, then check the affected preset types.
|
||||
The table highlights gaps that need human review. What CI *does* run:
|
||||
[validation.md](validation.md).
|
||||
|
||||
| Not checked by CI | Consequence |
|
||||
| --- | --- |
|
||||
| The `version` bump | The change never reaches an upgrading user |
|
||||
| A misspelled setting key | Setting silently has no effect |
|
||||
| A filename Windows cannot check out, or one that differs from its `sub_path` only in case | Works on the author's machine, breaks the bundle on another platform |
|
||||
| `bed_model` / `bed_texture` / `hotend_model` pointing at a missing asset | Bed renders as Custom, hotend falls back to the generic model |
|
||||
| A nozzle size in a model's list with no matching variant | The size is offered and resolves to nothing |
|
||||
| A non-default process | `validate_slice` gives non-default quality tiers no dedicated coverage |
|
||||
| Whether the intended default survived compatibility selection | The sweep can select a different compatible preset |
|
||||
| A dangling `compatible_printers` inside an `instantiation: "false"` base | A base never becomes a `Preset`, so the reference check never sees it (a bad `inherits` in a base *is* caught) |
|
||||
| A `renamed_from` whose old name is still a live preset | The redirect is inert while a live preset carries that name |
|
||||
| Per-extruder vector length on a multi-nozzle printer | Silently padded (with the **first** value) or truncated |
|
||||
|
||||
## 1. Was the vendor `version` bumped?
|
||||
|
||||
For **every** bundle whose folder the diff touches, `resources/profiles/<Vendor>.json` must have its
|
||||
`version` incremented — last component, carrying `.99` into the third component. A library change
|
||||
means bumping `OrcaFilamentLibrary.json`.
|
||||
|
||||
*Why:* nothing in CI checks it, and `PresetUpdater` reinstalls only when `vendor_ver < resource_ver` —
|
||||
without a bump the change reaches neither an upgrading user nor the author's own running app.
|
||||
|
||||
## 2. Was the index rebuilt, and does the diff contain only this change?
|
||||
|
||||
`check` now fails on an unregistered file, on an index `update-index` would reorder, and on a file
|
||||
`normalize` would rewrite — so a PR that skipped them arrives red, and you do not have to spot the
|
||||
omission yourself. Three things are still yours:
|
||||
|
||||
- **The index diff belongs to this change.** `update-index` rewrites whole `*_list` sections. If the
|
||||
bundle had drifted, the author's PR now carries someone else's reordering; ask for it in a separate
|
||||
commit rather than reviewing it inline.
|
||||
- **A deleted selectable preset needs a successor** as in item 4. `update-index` removes its
|
||||
registration; `validate_custom` detects the break only for names covered by released fixtures.
|
||||
- **`normalize` edits content, not just layout.** It drops `version` and `is_custom_defined` from preset
|
||||
files, removes obsolete keys, deletes six print-speed keys from filament profiles, and resolves
|
||||
`extruder_clearance_radius` against `extruder_clearance_max_radius` by keeping the larger.
|
||||
Check that the keys it removed were meant to go.
|
||||
|
||||
Obsolete keys fail `check`'s normalization pass and should be removed with `normalize`.
|
||||
`check` also reports per-key obsolete warnings for filament profiles in the selected vendors.
|
||||
|
||||
*Why:* the index is the loader's only entry point. Out-of-order entries fail with `can not find inherits`
|
||||
and take the whole vendor bundle down; an unindexed file gets reviewed, merged and never loads.
|
||||
|
||||
## 3. Are ids generated, not written?
|
||||
|
||||
No hand-typed or copied `setting_id` / `filament_id`. Instantiated presets have a `setting_id`; bases do
|
||||
not. `check` enforces all of that; what it cannot tell you is whether the identity *should* have moved.
|
||||
|
||||
A rewritten or removed `filament_id` means a product's identity moved — a rename, or an edited
|
||||
`filament_vendor` / `filament_type` — and the old id is not forwarded anywhere. Confirm that was
|
||||
intended, and that a new id is not a rename in disguise.
|
||||
|
||||
*Why:* a duplicate `filament_id` on one printer makes AMS spool matching a coin toss; a copied
|
||||
`setting_id` breaks preset identity. See [ids.md](ids.md).
|
||||
|
||||
## 4. Does anything disappear for existing users?
|
||||
|
||||
A rename, a deletion, or a flip of `"instantiation": "true"` → `"false"` on a shipped preset removes the
|
||||
name from the preset collection. It needs `renamed_from` on a successor — and only one preset may claim a
|
||||
given old name. The claimed old name must **not** still be a live preset; the redirect is inert if it is.
|
||||
|
||||
*Why:* user presets inheriting it die with `can not find parent <name> for config <file>!`; 3MF-embedded
|
||||
presets are dropped with no error at all. Commit `33923464ae` reverted exactly this for Cubicon;
|
||||
`6943b6ddc3` redid it correctly. CI's `validate_custom` catches the shipped-name case — but not an inert
|
||||
`renamed_from`.
|
||||
|
||||
## 5. Is `compatible_printers` right?
|
||||
|
||||
Exact printer **variant** names, non-empty on every instantiated filament outside OrcaFilamentLibrary
|
||||
and written in the preset's own file — golden rule 6, with the flattened-vs-own-key trap in
|
||||
[filament-profiles.md](filament-profiles.md#compatible_printers). Watch for a nozzle-specific variant that
|
||||
inherited or copied the base's full printer list, and for two presets of one product with overlapping
|
||||
lists — duplicate combobox entries and an ambiguous AMS match.
|
||||
|
||||
*Why:* real shipped bugs twice (`b7b3418baf` "showing up everywhere", `ff83aa41ef` duplicate Flashforge
|
||||
entries).
|
||||
|
||||
## 6. Model ↔ variant ↔ process consistency
|
||||
|
||||
- New nozzle size → the model's `nozzle_diameter` list extended, a variant with a matching
|
||||
`printer_variant`, and at least one process listing that variant.
|
||||
- `default_print_profile` is one exact name (not a `;` list), and that process's resolved
|
||||
compatibility list or condition includes this printer.
|
||||
- `default_filament_profile` is an array of names that exist.
|
||||
|
||||
*Why:* an unlisted `printer_variant` is a hard bundle-load failure. Default process selection is
|
||||
weaker: the sweep attempts the named default, then updates compatibility and rejects generic Default
|
||||
fallbacks. Another compatible process can conceal a bad reference, so inspect it even after a pass.
|
||||
|
||||
## 7. Types and spellings
|
||||
|
||||
Every value a string or an array of strings; `filament_type` an array; `instantiation` the string
|
||||
`"true"`/`"false"` — golden rule 7. Check index metadata and model `nozzle_diameter` especially;
|
||||
wrong types there can abort loading for **every** vendor.
|
||||
|
||||
The part only a reviewer can do: check new setting keys against `src/libslic3r/PrintConfig.cpp`. A
|
||||
misspelled key is silently discarded (rule 8), the single most common way a profile edit does nothing
|
||||
while CI stays green.
|
||||
|
||||
## 8. Blast radius of a base edit
|
||||
|
||||
A change to `fdm_*_common.json` reaches every child at once. Ask which presets it touches — several
|
||||
reverts in this repo are exactly this (`41d1b0d3c8`, `dc491166a8`). Also check whether the edited leaf has
|
||||
children of its own: Prusa, Flashforge and Elegoo all chain leaf-inherits-leaf several levels deep.
|
||||
|
||||
## 9. Do the numbers make sense for the nozzle?
|
||||
|
||||
Check resolved widths and layer heights against the nozzle, and flow limits / pressure advance
|
||||
against the actual hardware and material. The patterns in [process-profiles.md](process-profiles.md)
|
||||
are examples, not mandatory values; [filament-profiles.md](filament-profiles.md) explains what to
|
||||
revisit for a nozzle change. A cloned preset's unchanged MVS needs particular scrutiny.
|
||||
|
||||
Settings tuned for real hardware cannot be verified by reading the diff. Say so rather than approving
|
||||
numbers nobody measured.
|
||||
|
||||
## 10. Asset references (not checked anywhere)
|
||||
|
||||
`bed_model`, `bed_texture`, `hotend_model` and `<Model>_cover.png` exist under
|
||||
`resources/profiles/<vendor folder>/`. Broken references already ship; nothing checks them.
|
||||
|
||||
## 11. `default_materials` (checked by CI)
|
||||
|
||||
`check` fails on a `default_materials` / `default_filament_profile` name that resolves to no system
|
||||
filament, so a dangling entry no longer reaches review. Scope the run while working on one vendor:
|
||||
|
||||
```bash
|
||||
python3 scripts/orca_profile_tool.py check --vendor "<Vendor>" # py -3 on Windows
|
||||
```
|
||||
|
||||
## 12. Per-extruder vector lengths (not checked)
|
||||
|
||||
One entry per extruder for the plain per-extruder vectors; the `printer_options_with_variant_1` keys are
|
||||
sized to `printer_extruder_variant` instead. A wrong length is silently padded — repeating the **first**
|
||||
value, not the last — or truncated. The two sizing families and the worked cases are in
|
||||
[machine-profiles.md](machine-profiles.md#multi-extruder-idex-and-tool-changers).
|
||||
|
||||
## 13. Non-default processes get no slice coverage
|
||||
|
||||
`validate_slice` starts from printer defaults; it does not enumerate every process. Slice a new or
|
||||
changed non-default tier explicitly with its intended printer.
|
||||
|
||||
## 14. Housekeeping worth a nit, not a block
|
||||
|
||||
`"from"` other than `"system"` (the preset-bundle loader ignores it, though the CLI's config-file loader
|
||||
rejects anything but `system`/`user`/`User`), `printer_settings_id` copied from another
|
||||
vendor, and a filename that disagrees with the preset's `name` (common; the loader keys off `name`).
|
||||
|
||||
## 15. Cross-platform filenames and paths (not checked)
|
||||
|
||||
Check for Windows-invalid characters, reserved device names, trailing path-component spaces/dots,
|
||||
and case mismatches in `sub_path` or asset paths. See [cross-platform paths](validation.md#cross-platform-paths).
|
||||
|
||||
---
|
||||
|
||||
## Reporting the review
|
||||
|
||||
A finding is: **one defect**, its file, what breaks at runtime or in CI, and the fix. Split independent
|
||||
defects into separate findings even when they live in one file — five id problems in one bullet get one
|
||||
fix and four survivors.
|
||||
|
||||
Severity discriminates only if it is earned:
|
||||
|
||||
| Severity | Means |
|
||||
| --- | --- |
|
||||
| blocker | the bundle fails to load, or a preset is unreachable at runtime |
|
||||
| major | CI fails, or existing users lose a preset |
|
||||
| minor | wrong-but-working: dead keys, `from`, naming, redundant overrides |
|
||||
|
||||
Compute every number and id (`orca_profile_tool.py`, a scripted count) or omit it — one invented count
|
||||
makes a reader stop trusting the right ones. Report a command's result only if you ran it.
|
||||
@@ -1,255 +0,0 @@
|
||||
# Validating profiles
|
||||
|
||||
```bash
|
||||
./scripts/check_profile.sh # everything CI runs
|
||||
./scripts/check_profile.sh --vendor "<Vendor>" # fast loop
|
||||
./scripts/check_profile.sh profile_tool validate_slice # named checks only
|
||||
```
|
||||
|
||||
```bat
|
||||
scripts\check_profile.bat :: the same three, on Windows
|
||||
scripts\check_profile.bat -Vendor "<Vendor>"
|
||||
scripts\check_profile.bat profile_tool validate_slice
|
||||
```
|
||||
|
||||
`check_profile.bat` is a shim around `check_profile.ps1` — same checks, same order, same logs;
|
||||
the flags take PowerShell spellings (`-Vendor`, `-ProfilesDir`, `-Validator`, `-Download`, `-Refresh`,
|
||||
`-WorkDir`, `-LogLevel`) and positional check names are unchanged. `-p`, `-v` and `-l` are aliases, so
|
||||
`-v Elegoo -l 2` reads the same on both platforms. It passes `-ExecutionPolicy Bypass` because a
|
||||
default Windows client refuses to run a checked-out `.ps1` at all. The `.ps1` finds Python itself,
|
||||
probing `py -3`, then `python`, then `python3`; run the tool by hand with `py -3` for the same reason.
|
||||
|
||||
Every check in the run happens even after an earlier one fails; the script exits non-zero if any did, and writes
|
||||
`.test/check_profiles/logs/<check>.log` plus, on failure, `.test/check_profiles/pr_comment.md` — the same
|
||||
report CI posts on the PR. A stale `.test/check_profiles/.lock` after a crash must be removed by hand.
|
||||
|
||||
## The five checks
|
||||
|
||||
| Check | Command it runs | Catches |
|
||||
| --- | --- | --- |
|
||||
| `profile_tool` | `python3 scripts/orca_profile_tool.py check` | index coverage **both ways**, preset-name collisions, files `normalize`/`update-index` would still rewrite, duplicate JSON keys, filament `compatible_printers`, `filament_type` array, conflict keys, id length, **all `setting_id` and `filament_id` rules** |
|
||||
| `validate_system` | `validator -p resources/profiles -l 2` | load errors, missing filament `compatible_printers`, dangling `inherits`/`compatible_*`, duplicate `filament_id` per printer |
|
||||
| `validate_slice` | `validator -p … -s -l 2` | custom G-code expansion, unresolvable printer defaults |
|
||||
| `validate_filament_subtypes` | `validator -p … -l 2 -f` | nothing extra — see below |
|
||||
| `validate_custom` | `validator -p <tree+fixture> -l 2` | a shipped preset name that a past release offered no longer resolving |
|
||||
|
||||
**`-f` is a no-op.** It is declared `po::bool_switch()->default_value(true)`, so the duplicate-`filament_id`
|
||||
check runs whether or not you pass it — `validate_system` already fails on duplicates. The binary's own
|
||||
`--help` ("Off unless this flag is present") does not reflect that default.
|
||||
|
||||
### `validate_custom` — the backward-compatibility gate
|
||||
|
||||
Downloads one fixture archive per past release (v1.9.0 onwards) of *generated mock* user presets —
|
||||
a `<vendor>_<preset>_orca_test` copy of every system preset that
|
||||
release shipped, cut with the validator's own `-g 1` mode — unpacks each over a copy of the current tree
|
||||
and loads it. Each entry holds only `inherits` plus a canned diff, so the one failure it adds over
|
||||
`validate_system` is a shipped preset name disappearing. (The whole current tree sits under each fixture,
|
||||
so every `validate_system` error fails it too.) This is what makes a rename or an
|
||||
`instantiation` flip a CI failure rather than just a user complaint, and the reason `renamed_from` is
|
||||
mandatory.
|
||||
|
||||
### `validate_slice`
|
||||
|
||||
Slices a two-colour cube on every instantiable printer in the tree, sequentially, forcing the prime tower.
|
||||
It selects `default_print_profile` and the first `default_filament_profile`, then updates compatibility;
|
||||
that update can select a different compatible preset. Confirm the intended defaults yourself rather
|
||||
than treating a passing sweep as proof that those exact presets were sliced.
|
||||
A printer fails if it cannot be selected, falls back to a Default preset, throws, produces no g-code, or
|
||||
emits no `CP TOOLCHANGE START`. It cannot be scoped to a filament-only vendor
|
||||
(`No instantiable printer presets found for vendor OrcaFilamentLibrary`); `check_profile.sh` records it
|
||||
as SKIP for a vendor with no `machine/` folder.
|
||||
|
||||
## `orca_profile_tool.py check`
|
||||
|
||||
`check` is one subcommand of the tool that also owns
|
||||
`generate-id`, `normalize`, `trim` and `update-index`; see [ids.md](ids.md) for the writing half.
|
||||
|
||||
| Per vendor | Catches |
|
||||
| --- | --- |
|
||||
| `check_preset_name_uniqueness` | two files in one bundle claiming one type + name — indexed or not |
|
||||
| `check_index_coverage` | a file on disk that no `*_list` references (**an error, not a warning**) |
|
||||
| `check_name_consistency` | an index entry whose `name` disagrees with the file, or whose `sub_path` is missing |
|
||||
| `check_normalized` | a file `normalize` would rewrite, and an index `update-index` would rebuild |
|
||||
| `check_filament_compatible_printers` | an instantiated non-library filament with no `compatible_printers` of its own |
|
||||
| `check_conflict_keys` | `extruder_clearance_radius` alongside `extruder_clearance_max_radius` |
|
||||
| `check_vector_type_keys` | a vector option written as a scalar (`"filament_type": "PLA"`) |
|
||||
| `check_filament_id_length` | a declared `filament_id` longer than 8 characters |
|
||||
| `check_machine_default_materials` | every `default_materials` / `default_filament_profile` name resolves |
|
||||
| `check_obsolete_keys` | per-key warnings for ignored options; **filament files only** |
|
||||
|
||||
Tree-wide, **ignoring `--vendor` entirely**: `check_setting_id_uniqueness` and `check_filament_ids`. So a
|
||||
vendor-scoped run can and does fail on another vendor's files — and it saves seconds, not minutes.
|
||||
|
||||
Unscoped, the per-vendor pass covers every bundle. The only exclusion is the stray `user/` directory
|
||||
(see below); `OrcaFilamentLibrary` is held to the same rules as any vendor, its sole exemption being
|
||||
that a library filament may leave `compatible_printers` empty — exactly what
|
||||
`check_filament_compatible_printers` allows. `check_normalized` covers every bundle with an index.
|
||||
|
||||
Notes that matter:
|
||||
|
||||
- Exit codes: **0** clean, **1** errors found, **2** argparse misuse. Warnings never change the exit code.
|
||||
- A nonexistent `--vendor` is a hard error — `[ERROR] unknown vendor "<V>" in <dir>`, exit 1.
|
||||
- `--vendor ""` means all vendors; `check_profile.sh` relies on that. `--vendor` is repeatable.
|
||||
- A **stray directory** under `resources/profiles/` still gets counted as a vendor by the per-vendor pass
|
||||
and warned about (`No profiles found for vendor: <dir> at …/<dir>.json`, and the "Checked vendors" count
|
||||
goes up by one). The one exception is `user/`, the validator's data dir, which an unscoped `check`
|
||||
skips by name; `--vendor user` still checks and warns about it. Warnings never change the exit code.
|
||||
`normalize`, `trim` and `update-index` ignore strays too — they define a bundle as *a directory with a
|
||||
matching index file*.
|
||||
- Each remedy is printed once for the whole run, not once per file, as a `[WARNING]` under the errors
|
||||
("2 unreferenced file(s) above: delete them, or run … update-index"). Read those lines: they name the
|
||||
command that fixes the batch.
|
||||
- The trailing summary always suggests `normalize`. That is right for the shape errors and misleading for
|
||||
everything else — an id error needs `generate-id`, a dangling `default_materials` needs a human.
|
||||
- `resources/profiles/check_unused_setting_id.py` is a legacy BBL-only diagnostic, not part of
|
||||
profile CI. Use `orca_profile_tool.py check` for current id validation.
|
||||
|
||||
### Obsolete-key diagnostics
|
||||
|
||||
`check` always reports per-key warnings for obsolete options in filament profiles.
|
||||
The normalization check also rejects obsolete keys across preset types; `normalize` removes them.
|
||||
|
||||
### Default-material references
|
||||
|
||||
The materials check finds `default_materials` / `default_filament_profile` entries naming a preset
|
||||
that does not exist. The three authoring errors it surfaces are `,` instead of `;`, wrong case
|
||||
(`@system`), and a whole `;`-joined string stuffed into one array element.
|
||||
|
||||
### `normalize` and `update-index` are part of the check
|
||||
|
||||
`check` fails when either command would still change something, so they are not optional polish — the
|
||||
file that gets reviewed has to be the file that ships. What `normalize` changes is narrow and fixed:
|
||||
adds a missing `type`, deletes a `version` or `is_custom_defined` key from a *preset* file, deletes six
|
||||
print-speed keys from filament profiles (`initial_layer_print_speed`, `outer_wall_speed`,
|
||||
`inner_wall_speed`, `infill_speed`, `top_surface_speed`, `travel_speed`), deletes the
|
||||
obsolete keys in `PrintConfigDef::handle_legacy`'s `ignore` set across preset types, resolves the
|
||||
`extruder_clearance_*` conflict pair by keeping the larger, arrayifies five filament options besides
|
||||
`filament_type`, and hoists `type`, `name`, `renamed_from`, `inherits`, `from`, `setting_id`,
|
||||
`filament_id`, `instantiation` to the front. A file it changes is then rewritten whole — tab-indented,
|
||||
LF, one trailing newline, keys reordered.
|
||||
|
||||
**Set `type` explicitly when authoring.** For a file in `machine/` without it, normalization guesses
|
||||
`machine` only if its name contains `nozzle`, otherwise `machine_model`. That heuristic cannot
|
||||
reliably classify shared machine bases or unusually named variants.
|
||||
|
||||
The Python obsolete-key set is checked against the C++ source by a unit test. Active options
|
||||
and legacy aliases that the loader migrates (such as `extruder_type` and
|
||||
`extruder_clearance_max_radius`) are preserved.
|
||||
|
||||
Two things it therefore does **not** enforce:
|
||||
|
||||
- **Formatting and key order on their own.** A file with none of those problems is skipped entirely, so
|
||||
4-space indent, a missing trailing newline, and a file that leads with `compatible_printers` all pass
|
||||
`check`. They stay latent until something else trips `normalize` and the whole file reformats inside an
|
||||
unrelated diff. (`normalize --force` rewrites every file, which is not something to run on a shipped
|
||||
bundle.)
|
||||
- **A misspelled setting key.** `inital_layer_height` and `sparse_infill_densiti` pass `check` cleanly.
|
||||
Verify new keys against `PrintConfig.cpp` and `PrintConfigDef::handle_legacy`.
|
||||
|
||||
## The validator binary
|
||||
|
||||
Built from `src/dev-utils/OrcaSlicer_profile_validator.cpp` (`-DORCA_TOOLS=ON`).
|
||||
Both scripts find a local build under `build*/` — `check_profile.sh` tries Release, RelWithDebInfo, then
|
||||
Debug, and `check_profile.ps1` adds MinSizeRel — else they download the nightly into
|
||||
`.test/check_profiles/validator`. Pass `--download` / `-Download` to match CI exactly, since a stale
|
||||
local build is used silently. Windows looks for `OrcaSlicer_profile_validator.exe`.
|
||||
|
||||
If your build lives somewhere else entirely, point at it with `--validator` / `-Validator`, or set
|
||||
`ORCA_PROFILE_VALIDATOR` (`$env:ORCA_PROFILE_VALIDATOR` in PowerShell).
|
||||
|
||||
| Flag | Meaning |
|
||||
| --- | --- |
|
||||
| `-p <dir>` | profile tree (also becomes the data dir) |
|
||||
| `-l <n>` | log level; CI uses 2 |
|
||||
| `-v <Vendor>` | load only that vendor **plus** OrcaFilamentLibrary |
|
||||
| `-s` | slice sweep |
|
||||
| `-f` | no-op (see above) |
|
||||
| `-g 1` | regenerate user-preset fixtures; takes a value, and wipes the user preset dir first |
|
||||
|
||||
On ARM64 Linux the nightly is x86-64 only — the script warns and downloads anyway, producing a binary
|
||||
that will not run. Build it locally instead.
|
||||
|
||||
Running the validator directly uses the profile tree as its data directory and can create `user/`
|
||||
there. Prefer the wrappers, which stash existing user presets and restore them afterward. After a
|
||||
direct run, inspect `user/` and remove only empty directories created by that run; fixtures or
|
||||
pre-existing user files may be present.
|
||||
|
||||
## Checking a copy of the tree
|
||||
|
||||
Use `--profiles DIR` on the Python tool and `-p DIR` on the validator. The wrappers' `--profiles` /
|
||||
`-ProfilesDir` passes the tree to both, so one run validates a copy fully:
|
||||
|
||||
```bash
|
||||
./scripts/check_profile.sh --profiles "<tree>"
|
||||
```
|
||||
|
||||
On Windows use `scripts\check_profile.bat -ProfilesDir "<tree>"`.
|
||||
|
||||
## Testing in the app
|
||||
|
||||
Editing this checkout's `resources/profiles` does not update a separately installed application.
|
||||
Test with a build using the edited resources and a bumped bundle version; the updater installs newer
|
||||
bundles under `<data_dir>/system/`, and the preset cache also depends on the bundle version.
|
||||
Use Help ▸ Show Configuration Folder to locate the active data directory:
|
||||
|
||||
| Platform | Default data directory |
|
||||
| --- | --- |
|
||||
| macOS | `~/Library/Application Support/OrcaSlicer` |
|
||||
| Linux | `$XDG_CONFIG_HOME/OrcaSlicer`, or `~/.config/OrcaSlicer` when unset |
|
||||
| Windows | `%APPDATA%\OrcaSlicer` |
|
||||
|
||||
A portable `data_dir` next to the executable takes precedence. Use a separate test configuration
|
||||
for a clean-install check; preserve the normal configuration and user presets.
|
||||
|
||||
## Cross-platform paths
|
||||
|
||||
Match the exact case of each `sub_path` and asset filename; Linux filesystems commonly distinguish
|
||||
case even when a macOS or Windows checkout does not. Preset-name references are case-sensitive
|
||||
on every platform. Avoid Windows-invalid characters (`< > : " | ? *`), reserved device names
|
||||
such as `CON` / `NUL` (including with extensions), and trailing spaces or dots in path components.
|
||||
Keep stems tidy too, but a space immediately before `.json` is not a trailing path-component space.
|
||||
|
||||
## Error → remedy
|
||||
|
||||
| Message | Fix |
|
||||
| --- | --- |
|
||||
| `can not find inherits <parent> for <preset>` | parent missing, unregistered, or listed **after** the child |
|
||||
| `can not find filament_id for <name>` | nothing in the chain declares one — run `generate-id` |
|
||||
| `can not find parent <name> for config <user preset>!` | a shipped name disappeared — add `renamed_from` |
|
||||
| `Missing instantiation attribute for <name>` | key absent **or** not the string `"true"`/`"false"` |
|
||||
| `contains incorrect keys: <keys>, which were removed` | a key valid for a different preset type |
|
||||
| `defines invalid printer variant "<v>"` | not in the model's `nozzle_diameter` list |
|
||||
| `has printer_variant "<v>" that does not match its nozzle_diameter` | the set comparison in [machine-profiles.md](machine-profiles.md) |
|
||||
| `references unknown compatible_printers "<p>"` | the printer was renamed or deleted; fix the reference |
|
||||
| `references renamed compatible_printers "<old>" (now "<new>")` | in-tree references must name the current preset; `renamed_from` does not excuse them |
|
||||
| `Filament preset "<f>" is missing compatible_printers setting` | non-library filaments need a non-empty list in their **own** file — the flattened-vs-own-key trap is in [filament-profiles.md](filament-profiles.md#compatible_printers) |
|
||||
| `Ambiguous AMS filament match: N presets share filament_id "X" … printer "Y"` | make the lists disjoint, or fix an `inherits` pointing at another material's `@base` |
|
||||
| `Layer height cannot exceed nozzle diameter.` / `Line width too small` | `Print::validate()` flow rules |
|
||||
| `[ERROR] … no <V>.json list references it, so it never loads` | `update-index`, or delete the file |
|
||||
| `[ERROR] … references it and it declares no profile type` | set the correct `type` explicitly, then `normalize` and `update-index` |
|
||||
| `[ERROR] … normalize would <change>` / `<V>.json: update-index would rebuild <lists>` | run that command and commit the result |
|
||||
| `[ERROR] <V> has N <type> profiles named "<name>"` | identify the intended preset and remove or rename the duplicate; use `trim --dry-run` only for deliberate unindexed-file cleanup |
|
||||
| `[ERROR] … must not have a setting_id` / `is missing a setting_id` | `generate-id --setting-id` |
|
||||
| `inherits filament_id "X" but its own triple … mints "Y"` | `generate-id` will **not** fix this — see [ids.md](ids.md) |
|
||||
| `vendor <V>'s config version: <s> invalid` | the `version` string is not Semver-parseable |
|
||||
| `[json.exception.type_error.302] type must be string` | locate the non-string value in the index or model; see [failure scopes](vendor-bundle.md#failure-modes-ranked-by-blast-radius) |
|
||||
| `Printer "<p>" fell back to a default preset` | final process or filament selection is a generic Default preset; check named defaults, visibility and available compatible presets. An incompatible default may instead be replaced without this error |
|
||||
| `Printer "<p>" sliced but the filament change never fired` | `change_filament_gcode` never expanded |
|
||||
|
||||
## CI
|
||||
|
||||
`.github/workflows/check_profiles.yml`, job **"Check profiles"**, on `pull_request` into `main` or
|
||||
`release/*`, paths `resources/profiles/**`, `resources/printers/**`, `scripts/**` and the workflow itself.
|
||||
There is no push trigger — a direct push to main runs no profile validation.
|
||||
|
||||
The job opens with `python3 -m unittest discover -s scripts/tests -t scripts`, the tool's own unit
|
||||
tests. That step is deliberately **not** `continue-on-error`: a broken tool makes everything it then says
|
||||
about the profiles worthless. Every check after it is `continue-on-error` with a final gate, so one run
|
||||
reports all five results. On failure a second workflow posts or replaces a single PR comment marked
|
||||
`<!-- profile-validation-comment -->`, with each failing log truncated to 30 KB; it deletes the comment
|
||||
once the run is green.
|
||||
|
||||
The job name is also the required check for the delegated-merge bot, which lets a vendor maintainer
|
||||
self-merge a `resources/profiles/<Their vendor>/` PR with no human review — so whatever CI does not check
|
||||
is what ships unreviewed. Its denied patterns refuse `^scripts/` and any `.py`, so a PR that touches the
|
||||
tooling always needs a maintainer.
|
||||
@@ -1,175 +0,0 @@
|
||||
# The vendor bundle and the loader
|
||||
|
||||
A bundle is `resources/profiles/<Vendor>.json` (the index) plus `resources/profiles/<Vendor>/`.
|
||||
The **vendor id is the filename stem**, not the `name` inside — several differ (`BBL.json` is named
|
||||
"Bambulab"). Asset paths and the `setting_id` formula use the id; the `validate_custom` fixture prefix
|
||||
uses the `name`.
|
||||
|
||||
## The index
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Phrozen",
|
||||
"version": "02.04.00.03",
|
||||
"force_update": "0",
|
||||
"description": "Phrozen configurations",
|
||||
"machine_model_list": [ { "name": "...", "sub_path": "machine/....json" } ],
|
||||
"machine_list": [ ... ],
|
||||
"process_list": [ ... ],
|
||||
"filament_list": [ ... ]
|
||||
}
|
||||
```
|
||||
|
||||
The loader reads `name`, `version`, `url` and the four `*_list` arrays.
|
||||
`description` is only logged. `force_update` is read by `PresetUpdater`, never by the loader.
|
||||
`sub_path` is relative to the **vendor folder**.
|
||||
|
||||
| List | Holds |
|
||||
| --- | --- |
|
||||
| `machine_model_list` | `machine_model` records (the printer product) |
|
||||
| `machine_list` | printer variants **and** shared machine bases |
|
||||
| `process_list` | selectable processes **and** shared process bases |
|
||||
| `filament_list` | selectable filaments **and** shared filament bases |
|
||||
|
||||
### Three registration rules
|
||||
|
||||
1. **Everything is registered, bases included.** Every preset file on disk has exactly one entry in the
|
||||
matching list, and no unindexed preset file is left in the tree.
|
||||
2. **Parents before children.** `inherits` resolves against a per-kind map filled as the list is walked
|
||||
(`configs.clear()` then process, filaments, printers). A parent listed after its child produces
|
||||
`can not find inherits <parent> for <child>` and the bundle is discarded.
|
||||
3. **The index entry's `name` must equal the `name` inside the sub_path file.** `check_name_consistency`
|
||||
walks the index looking for the files; `check_index_coverage` walks the files looking for them in the
|
||||
index. The `renamed_from` escape hatch `check_name_consistency`'s docstring promises is commented out.
|
||||
|
||||
All three are `check` errors now, and `update-index` writes an index that satisfies all three from the
|
||||
files on disk — including the parents-first ordering, by topological sort. Hand-editing the index is
|
||||
fine for a one-line addition, but the committed result must equal what `update-index` writes, because
|
||||
`check` compares them.
|
||||
|
||||
The loader itself reports none of this: an unregistered file, or an entry with a typo'd key
|
||||
(`"subpath"`), is silently dropped. (A typo'd `sub_path` is a `check` error naming the entry.)
|
||||
|
||||
`BBL/cli_config.json` and `BBL/filament/filaments_color_codes.json` are auxiliary data loaded by path,
|
||||
not presets. The tool's `NON_PROFILE_FILES` excludes these basenames from preset maintenance.
|
||||
|
||||
## `version`
|
||||
|
||||
Parsed by a four-component Semver where the 4th is folded in as `patch = patch*100 + value`. Write it
|
||||
zero-padded, `MM.mm.pp.bb`; a couple of bundles drop a component or the padding, but do not imitate them.
|
||||
|
||||
- **Bump the version for every bundle the PR touches.** `PresetUpdater` installs bundled resources
|
||||
only when their version is newer than the installed version; the `.opc` preset cache is also
|
||||
versioned. Nothing in profile CI checks the bump.
|
||||
- **Keep the last component ≤ 99.** `02.04.00.100` and `02.04.01.00` both parse to `2.4.100`. A bundle
|
||||
that reaches `.99` carries into the third component (`02.03.02.99` → `02.03.03.00`).
|
||||
- An **absent** version is worse than a stale one: the validator still passes, but `Semver::valid()`
|
||||
excludes `0.0.0`, so the vendor is dropped from the configuration wizard entirely and the preset cache
|
||||
is disabled for it. An *unparseable* version is not silent — it throws and discards the whole bundle
|
||||
(see the failure table below).
|
||||
|
||||
## Common preset keys
|
||||
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| `type` | `machine_model` / `machine` / `process` / `filament` |
|
||||
| `name` | the preset name; the filename is *not* authoritative |
|
||||
| `inherits` | the parent's exact `name` — no path, no `.json` |
|
||||
| `instantiation` | the **string** `"true"` (selectable) or `"false"` (base) |
|
||||
| `from` | `"system"` by convention; the vendor loader never reads it |
|
||||
| `setting_id` | required on instantiated presets, forbidden on bases — generated |
|
||||
| `renamed_from` | `;`-separated list of old names this preset supersedes |
|
||||
|
||||
These are config-preset keys; `machine_model` records have their own
|
||||
[schema](machine-profiles.md#a-machine_model-is-not-a-config-preset). Keep `from` as `"system"`
|
||||
for shipped presets. The vendor loader ignores it, but the CLI config-file loader accepts only
|
||||
`system`, `user` or `User` and handles their inheritance differently.
|
||||
|
||||
`instantiation` is the one metadata key that is hard-gated: a missing key or any value other than the
|
||||
strings `"true"`/`"false"` is an error (`Missing instantiation attribute for <name>`). A JSON boolean
|
||||
`true` fails harder — it throws inside `load_from_json` and takes the **whole vendor bundle** down.
|
||||
|
||||
### `inherits`
|
||||
|
||||
Resolution is an exact-name lookup **within the same bundle**, plus one exception: filaments may inherit
|
||||
from `OrcaFilamentLibrary`, which is loaded first and becomes the base bundle. Vendor-to-vendor
|
||||
inheritance always fails. You can inherit from an instantiated preset as well as from a base; it is
|
||||
common.
|
||||
|
||||
### `renamed_from`
|
||||
|
||||
One JSON string, `;`-separated for several old names.
|
||||
|
||||
- Write `"A;B"`, never `"A ; B"` — an unquoted item keeps its trailing space and can never match.
|
||||
- When `renamed_from` is **absent** and the name contains `@`, the loader auto-adds the `@`-removed form
|
||||
(`X @Y` → `X Y`) as a rename alias. Declaring an explicit `renamed_from` **suppresses** that, so a
|
||||
preset that needs both the `@`-removed form and a real old name must list both. No shipped profile
|
||||
currently does, which means any preset that gained a `renamed_from` quietly lost its `X Y` alias.
|
||||
- It rescues names stored **outside** the tree: user presets and 3MF projects. It does **not** rescue
|
||||
in-tree `inherits` (exact lookup), it does **not** satisfy `check_name_consistency`, the validator
|
||||
reports an in-tree reference that only resolves through it (`references renamed compatible_printers
|
||||
"OLD" (now "NEW")`), and `machine_model` records never read it at all.
|
||||
- Only one preset may claim a given old name — two that do is a counted error
|
||||
(`… was marked as renamed from "Y" … as well`). But the redirect is **inert while a live preset still
|
||||
carries that name**, and nothing checks *that*; Z-Bolt ships a folder of such dead entries.
|
||||
|
||||
## Failure modes, ranked by blast radius
|
||||
|
||||
| Scope | Cause |
|
||||
| --- | --- |
|
||||
| **All vendors, zero system profiles** | a non-string `version`, `name` or `url` at the top level of a vendor index (`"version": 2`), or non-string `nozzle_diameter` on a model — `nlohmann::type_error` escapes the per-vendor `std::runtime_error` catch |
|
||||
| **The whole vendor bundle** | unparseable `version`; index JSON parse error; a `sub_path` file missing or unparseable; unresolvable `inherits`; duplicate preset name within the vendor; empty/unknown `printer_model` or `printer_variant`; a filament resolving no `filament_id` |
|
||||
| **One preset** | `instantiation` missing or a wrong string; keys belonging to another preset type (`contains incorrect keys: …, which were removed`); a non-string inside a `*_list` entry (`invalid value type for <key>`) |
|
||||
| **Logged, not counted** | a raw JSON number in a preset — `invalid json type for <key>`, the value is dropped and the exit code stays 0 |
|
||||
| **Nothing reported by the loader** | unregistered file; misspelled setting key; missing bed/hotend asset. Only the first of those is a `check` error; the other two reach users |
|
||||
|
||||
Deleting a file the index still lists surfaces as a *parse error* on line 1, not "file not found" — the
|
||||
loader `ifstream`s the missing path and nlohmann reports `unexpected end of input`.
|
||||
|
||||
Preset names are a **single global namespace across every vendor**: a duplicate within one vendor is a
|
||||
hard bundle failure, a duplicate across vendors is reported as `Found duplicated preset: <name> in
|
||||
vendor: <vendor>` and still counts as an error. `check_preset_name_uniqueness` catches the within-bundle
|
||||
case earlier and more precisely — including an *unindexed* twin, which is one `sub_path` edit away from
|
||||
silently becoming the parent every child resolves to (`std::map::emplace` keeps the first insertion, so
|
||||
index order decides). Base names, by contrast, repeat across bundles by design: `fdm_process_common`
|
||||
exists in nearly all of them.
|
||||
|
||||
## Starting a whole new vendor bundle
|
||||
|
||||
Nothing generates one; copy the smallest bundle that resembles the hardware. **`Voxelab` or `M3D`** are
|
||||
the minimal shape — a shared machine base, the model, one variant, a shared process base, two
|
||||
processes, and an empty `filament_list` that takes the library generics. Do *not* start from `Phrozen`:
|
||||
it carries local `fdm_filament_*` copies that have drifted from the library, and a filament preset that
|
||||
restates most of its parent — the style this skill advises against.
|
||||
|
||||
Write the machine files **last**, so you only visit them once:
|
||||
|
||||
1. **Choose the names first** — model, variant(s), process(es). Everything else references them.
|
||||
2. `resources/profiles/<Vendor>.json`: `name`, `version` (`01.00.00.00`), `force_update: "0"`,
|
||||
`description`, and all four `*_list` arrays (an empty `filament_list` is fine).
|
||||
3. The shared bases — `<Vendor>/machine/fdm_machine_common.json` and
|
||||
`<Vendor>/process/fdm_process_common.json`, both `"instantiation": "false"` with no `setting_id`.
|
||||
For a Klipper printer add your own `<Vendor>/machine/fdm_klipper_common.json` inheriting the machine
|
||||
base; there is no shared one, because a `machine` preset can only inherit inside its own bundle.
|
||||
4. One selectable process per variant, each naming its variant in `compatible_printers`.
|
||||
5. Bed assets and `<Model>_cover.png`, all directly in `<Vendor>/`. None of them is needed for the
|
||||
bundle to load, and nothing in CI checks them — but the bed files are inert unless the `machine_model`
|
||||
names them in `bed_model` / `bed_texture`, and the cover is found by convention as
|
||||
`<the name you gave the model in machine_model_list>_cover.png`.
|
||||
6. The `machine_model` record and the `machine` variants, now that every value they reference exists —
|
||||
the minimum key sets and the `default_*` shapes are in
|
||||
[machine-profiles.md](machine-profiles.md#the-machine-variant).
|
||||
7. Run the tool and validate — follow
|
||||
[Creating or modifying a profile](../SKILL.md#creating-or-modifying-a-profile). `generate-id` is not
|
||||
optional for a new bundle: the validator loads presets that have no `setting_id`, but `check` fails
|
||||
every one of them. `update-index` will fill the four `*_list` arrays for you once the files exist, so
|
||||
step 2 only needs the bundle metadata to be right.
|
||||
|
||||
## `resources/profiles_template/`
|
||||
|
||||
A separate tree (`Template.json` + `Template/`) holding filament and process templates. It is **not** a
|
||||
scaffold for shipped profiles — `CreatePresetsDialog.cpp` reads it for the in-app "create a custom
|
||||
printer/filament" wizard, so editing it changes what users get when they create a custom preset.
|
||||
`check_profile.sh`'s validator checks default to `resources/profiles` (redirectable with `-p`), and so
|
||||
does `orca_profile_tool.py` (redirectable with `--profiles`);
|
||||
neither covers this tree.
|
||||
@@ -1,12 +1,2 @@
|
||||
# Set the default behavior, in case people don't have core.autocrlf set.
|
||||
* text=auto
|
||||
|
||||
# Shell scripts are run by Git Bash on Windows CI, which cannot read a script
|
||||
# with CRLF line endings: it fails on the first line. Windows checkouts default
|
||||
# to core.autocrlf=true, so keep these LF whatever the platform.
|
||||
*.sh text eol=lf
|
||||
|
||||
# Batch files are read by cmd.exe, which tracks a byte offset into the file to
|
||||
# resume after `call :label`. With LF endings that offset can land wrong and the
|
||||
# label lookup fails, so keep these CRLF whatever the platform.
|
||||
*.bat text eol=crlf
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: 🐞 Bug Report
|
||||
description: Something behaves incorrectly while Orca Slicer keeps running
|
||||
description: File a bug report
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
@@ -10,8 +10,6 @@ body:
|
||||
Please note that this is not the place to make feature requests or ask for help.
|
||||
For this, please use the [Feature request](https://github.com/OrcaSlicer/OrcaSlicer/issues/new?assignees=&labels=&projects=&template=feature_request.yml) issue type or you can discuss your idea on our [Discord server](https://discord.gg/P4VE9UY9gJ) with others.
|
||||
|
||||
If Orca Slicer closes on its own, freezes or stops responding, please use the [Crash report](https://github.com/OrcaSlicer/OrcaSlicer/issues/new?assignees=&labels=&projects=&template=crash_report.yml) form instead. It asks for the logs a crash needs.
|
||||
|
||||
Before filing, please check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
@@ -34,22 +32,14 @@ body:
|
||||
attributes:
|
||||
label: OrcaSlicer Version
|
||||
description: Which version of Orca Slicer are you running? You can see the full version in `Help` -> `About Orca Slicer`.
|
||||
placeholder: e.g. 2.5.0
|
||||
placeholder: e.g. 1.9.0
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: working_version
|
||||
attributes:
|
||||
label: Regression compared to a previous version
|
||||
description: Did it work in a previous version?
|
||||
placeholder: e.g. 2.3.2
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: os_type
|
||||
attributes:
|
||||
label: "Operating System (OS)"
|
||||
description: "What OSes are you experiencing issues on?"
|
||||
description: "What OSes are you are experiencing issues on?"
|
||||
multiple: true
|
||||
options:
|
||||
- Linux
|
||||
@@ -88,7 +78,7 @@ body:
|
||||
id: reproduce_steps
|
||||
attributes:
|
||||
label: How to reproduce
|
||||
description: Please describe the detailed steps to reproduce this issue
|
||||
description: Please described the detailed steps to reproduce this issue
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
@@ -110,23 +100,28 @@ body:
|
||||
description: What should happen after the above steps?
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
id: file_required
|
||||
attributes:
|
||||
value: |
|
||||
Please be sure to add the following files:
|
||||
* Please upload a ZIP archive containing the **project file** used when the problem arise. Please export it just before or after the problem occurs. Even if you did nothing and/or there is no object, export it! (We need the configurations in project file).
|
||||
You can export the project file from the application menu in `File`->`Save project as...`, then zip it
|
||||
* A **log file** for crashes and similar issues.
|
||||
You can find your log file here:
|
||||
Windows: `%APPDATA%\OrcaSlicer\log` or usually `C:\Users\<your username>\AppData\Roaming\OrcaSlicer\log`
|
||||
MacOS: `$HOME/Library/Application Support/OrcaSlicer/log`
|
||||
Linux: `$HOME/.config/OrcaSlicer/log`
|
||||
If Orca Slicer still starts, you can also reach this directory from the application menu in `Help` -> `Show Configuration Folder`
|
||||
You can zip the log directory, or just select the newest logs when this issue happens, and zip them
|
||||
- type: textarea
|
||||
id: file_uploads
|
||||
attributes:
|
||||
label: Project file & Debug log uploads
|
||||
description: |
|
||||
Attach the files with the **Paste, drop, or click to add files** control directly underneath this box. Zip anything that is not a `.log`, `.txt` or image, since GitHub rejects other file types, and keep each file under 25 MB.
|
||||
|
||||
* The **project file** used when the problem happened, zipped. Export it just before or after the problem occurs. Even if you did nothing and there is no object on the plate, export it, since we need the configuration it carries. `File` -> `Save project as...`
|
||||
* The **log folder**, zipped. `Help` -> `Show Configuration Folder` opens it, or find it at:
|
||||
* Windows: `%APPDATA%\OrcaSlicer\log`, usually `C:\Users\<you>\AppData\Roaming\OrcaSlicer\log`
|
||||
* macOS: `$HOME/Library/Application Support/OrcaSlicer/log`
|
||||
* Linux: `$HOME/.config/OrcaSlicer/log`
|
||||
* Flatpak: `$HOME/.var/app/com.orcaslicer.OrcaSlicer/config/OrcaSlicer/log`
|
||||
* If the zip comes out over 25 MB, attach the newest logs from that folder on their own instead.
|
||||
description: Drop the project file and debug log here
|
||||
placeholder: |
|
||||
Zipped project file
|
||||
Zipped log folder
|
||||
Project File: `File` -> `Save project as...` then zip it & drop it here
|
||||
Log File: `Help` -> `Show Configuration Folder`, then zip the log directory, or just select the newest logs in `log` when this issue happens and zip them, then drop the zip file here
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
@@ -141,5 +136,7 @@ body:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Screenshots? References? Anything that will give us more context about the issue you are encountering!
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
name: 💥 Crash Report
|
||||
description: Orca Slicer closes on its own, freezes or stops responding
|
||||
labels: ["crash"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Thank you for taking the time to report a crash.**
|
||||
|
||||
Use this form when Orca Slicer closes on its own, freezes, or stops responding.
|
||||
If the application stays open and only produces a wrong result, please use the [Bug report](https://github.com/OrcaSlicer/OrcaSlicer/issues/new?assignees=&labels=&projects=&template=bug_report.yml) form instead.
|
||||
A printer whose toolhead collides with the print is also a bug report rather than a crash, since the application itself did not stop.
|
||||
|
||||
Before filing, please check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is this crash reproducible in the latest nightly build?
|
||||
description: >
|
||||
Please verify this crash still happens in the latest nightly build first. It may already be fixed there:
|
||||
[Nightly builds](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds).
|
||||
options:
|
||||
- label: I have checked the latest nightly build and the crash is still reproducible
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this crash?
|
||||
description: Please search to see if an issue already exists for the crash you encountered.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: OrcaSlicer Version
|
||||
description: Which version of Orca Slicer are you running? You can see the full version in `Help` -> `About Orca Slicer`.
|
||||
placeholder: e.g. 2.5.0
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: working_version
|
||||
attributes:
|
||||
label: Regression compared to a previous version
|
||||
description: Did it work in a previous version?
|
||||
placeholder: e.g. 2.3.2
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: os_type
|
||||
attributes:
|
||||
label: "Operating System (OS)"
|
||||
description: "What OSes are you seeing the crash on?"
|
||||
multiple: true
|
||||
options:
|
||||
- Linux
|
||||
- macOS
|
||||
- Windows
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: os_version
|
||||
attributes:
|
||||
label: "OS Version"
|
||||
description: "What OS version does this relate to?"
|
||||
placeholder: "i.e. OS: Windows 7/8/10/11 ..., Ubuntu 22.04/Fedora 36 ..., macOS 10.15/11.1/12.3 ..."
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: printer
|
||||
attributes:
|
||||
label: Printer
|
||||
description: Which printer was selected
|
||||
placeholder: Voron 2.4/VzBot/Prusa MK4/Bambu Lab X1 series/Bambu Lab P1P/...
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: crash_moment
|
||||
attributes:
|
||||
label: When does the crash happen?
|
||||
description: Pick the point where Orca Slicer stops working.
|
||||
options:
|
||||
- Not sure
|
||||
- On startup, before the main window appears
|
||||
- When opening or importing a project or model
|
||||
- While changing printer, filament or process settings
|
||||
- While slicing
|
||||
- In the 3D view, Preview or Assembly view
|
||||
- When exporting G-code or sending a print to the printer
|
||||
- On the Device tab, or connecting to a printer (camera, sync, login)
|
||||
- While using a specific tool, dialog or calibration
|
||||
- After resuming from sleep or changing monitors
|
||||
- When closing the application
|
||||
- No clear pattern
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: crash_frequency
|
||||
attributes:
|
||||
label: How often does it happen?
|
||||
options:
|
||||
- Not sure
|
||||
- Every time
|
||||
- Often, but not every time
|
||||
- Rarely
|
||||
- It only happened once
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: fresh_config
|
||||
attributes:
|
||||
label: Does it still crash with a fresh configuration?
|
||||
description: >
|
||||
Close Orca Slicer and rename your configuration folder (`%APPDATA%\OrcaSlicer` on Windows,
|
||||
`$HOME/Library/Application Support/OrcaSlicer` on macOS, `$HOME/.config/OrcaSlicer` on Linux),
|
||||
then start it again. Renaming keeps your settings, so you can put the folder back afterwards.
|
||||
options:
|
||||
- I have not tried this
|
||||
- Yes, it still crashes
|
||||
- No, the crash goes away
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduce_steps
|
||||
attributes:
|
||||
label: How to reproduce
|
||||
description: Please describe the detailed steps that lead to the crash.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. Scroll down to '...'
|
||||
4. Orca Slicer closes
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: system_info
|
||||
attributes:
|
||||
label: Additional system information
|
||||
description: >
|
||||
Display card and driver version are worth adding for crashes on startup or in the 3D view.
|
||||
CPU and memory are worth adding for crashes while slicing.
|
||||
placeholder: |
|
||||
CPU: 11th gen Intel r core tm i7-1185g7/AMD Ryzen 7 6800h/...
|
||||
Memory: 32/16 GB...
|
||||
Display Card: NVIDIA Quadro P400/...
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: file_uploads
|
||||
attributes:
|
||||
label: Project file, logs and crash report uploads
|
||||
description: |
|
||||
A crash report without logs usually cannot be acted on. Attach the files with the **Paste, drop, or click to add files** control directly underneath this box. Zip anything that is not a `.log`, `.txt` or image, since GitHub rejects other file types, and keep each file under 25 MB.
|
||||
|
||||
* The **project file** used when the crash happened, zipped. Export it just before or after the crash, even if the plate is empty, since we need the configuration it carries. `File` -> `Save project as...`
|
||||
* The whole **log folder**, zipped rather than single files picked out of it. `Help` -> `Show Configuration Folder` opens it, or find it at:
|
||||
* Windows: `%APPDATA%\OrcaSlicer\log`, usually `C:\Users\<you>\AppData\Roaming\OrcaSlicer\log`
|
||||
* macOS: `$HOME/Library/Application Support/OrcaSlicer/log`
|
||||
* Linux: `$HOME/.config/OrcaSlicer/log`
|
||||
* Flatpak: `$HOME/.var/app/com.orcaslicer.OrcaSlicer/config/OrcaSlicer/log`
|
||||
* On Windows the crash itself is written to a separate `crash_*.log` in there, and that is the file we need most. If the zip comes out over 25 MB GitHub will refuse it, so attach the newest log and any `crash_*.log` on their own instead.
|
||||
* The **operating system crash report**, on macOS and Linux, where Orca Slicer cannot write its own crash log. It is often the only record of where it died:
|
||||
* macOS: Console.app -> Crash Reports, or `$HOME/Library/Logs/DiagnosticReports/`. The file starts with `OrcaSlicer` and ends in `.ips`. Zip it before attaching, GitHub does not accept `.ips` files.
|
||||
* Linux: run `orca-slicer` from a terminal (Flatpak: `flatpak run com.orcaslicer.OrcaSlicer`) and paste everything it prints when it dies. On systemd systems `coredumpctl info orca-slicer` gives a backtrace.
|
||||
placeholder: |
|
||||
Zipped project file
|
||||
Zipped log folder
|
||||
Zipped macOS .ips crash report, or the terminal output on Linux
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: file_checklist
|
||||
attributes:
|
||||
label: Checklist of files to include
|
||||
options:
|
||||
- label: Log folder
|
||||
- label: Project file
|
||||
- label: Operating system crash report (macOS and Linux)
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Screenshots? References? Anything that will give us more context about the crash you are encountering!
|
||||
validations:
|
||||
required: false
|
||||
@@ -14,10 +14,6 @@ on:
|
||||
- 'localization/**'
|
||||
- 'resources/**'
|
||||
- ".github/workflows/build_*.yml"
|
||||
- ".github/workflows/unit_tests*.yml"
|
||||
- 'build_win.bat'
|
||||
- 'scripts/test_build_win.ps1'
|
||||
- 'scripts/build_preset_cache.*'
|
||||
- 'scripts/flatpak/**'
|
||||
- 'scripts/msix/**'
|
||||
- 'tests/**'
|
||||
@@ -33,12 +29,10 @@ on:
|
||||
- '**/CMakeLists.txt'
|
||||
- 'version.inc'
|
||||
- ".github/workflows/build_*.yml"
|
||||
- ".github/workflows/unit_tests*.yml"
|
||||
- 'build_linux.sh'
|
||||
- 'build_win.bat'
|
||||
- 'scripts/test_build_win.ps1'
|
||||
- 'build_release_vs.bat'
|
||||
- 'build_release_vs2022.bat'
|
||||
- 'build_release_macos.sh'
|
||||
- 'scripts/build_preset_cache.*'
|
||||
- 'scripts/flatpak/**'
|
||||
- 'scripts/msix/**'
|
||||
- 'tests/**'
|
||||
@@ -60,22 +54,6 @@ concurrency:
|
||||
|
||||
|
||||
jobs:
|
||||
# build_win.bat ships a test suite. Run it before the Windows builds.
|
||||
check_build_script:
|
||||
name: Windows build script tests
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
lfs: 'false'
|
||||
|
||||
# Windows PowerShell rather than pwsh: the suite drives build_win.bat
|
||||
# through cmd, and the two differ in how they quote native arguments.
|
||||
- name: Run the build script test suite
|
||||
shell: powershell
|
||||
run: .\scripts\test_build_win.ps1
|
||||
|
||||
build_linux:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -101,16 +79,14 @@ jobs:
|
||||
# SELF_HOSTED skips arm64 (the self-hosted Windows server is x64-only).
|
||||
matrix:
|
||||
include: ${{ fromJSON(vars.SELF_HOSTED
|
||||
&& '[{"arch":"x64","os":"orca-win-server","compiler":"clang"}]'
|
||||
|| '[{"arch":"x64","os":"windows-latest","compiler":"clang"},{"arch":"arm64","os":"windows-11-vs2026-arm","compiler":"clang"}]') }}
|
||||
needs: check_build_script
|
||||
&& '[{"arch":"x64","os":"orca-win-server"}]'
|
||||
|| '[{"arch":"x64","os":"windows-latest"},{"arch":"arm64","os":"windows-11-arm"}]') }}
|
||||
# Don't run scheduled builds on forks:
|
||||
if: ${{ !cancelled() && needs.check_build_script.result == 'success' && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }}
|
||||
if: ${{ !cancelled() && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }}
|
||||
uses: ./.github/workflows/build_check_cache.yml
|
||||
with:
|
||||
os: ${{ matrix.os }}
|
||||
arch: ${{ matrix.arch }}
|
||||
compiler: ${{ matrix.compiler }}
|
||||
build-deps-only: ${{ inputs.build-deps-only || false }}
|
||||
force-build: ${{ github.event_name == 'schedule' }}
|
||||
secrets: inherit
|
||||
@@ -171,7 +147,7 @@ jobs:
|
||||
if: ${{ !cancelled() && success() && !vars.SELF_HOSTED }}
|
||||
uses: ./.github/workflows/unit_tests.yml
|
||||
with:
|
||||
os: windows-11-vs2026-arm
|
||||
os: windows-11-arm
|
||||
artifact: ${{ github.sha }}-tests-windows-arm64
|
||||
test-dir: build-arm64/tests
|
||||
unit_tests_macos_arm64:
|
||||
@@ -209,7 +185,7 @@ jobs:
|
||||
./validator-bin/OrcaSlicer_profile_validator -p "${{ github.workspace }}/resources/profiles" -s -l 2
|
||||
publish_test_results:
|
||||
name: Publish Test Results
|
||||
needs: [unit_tests_linux_x86_64, unit_tests_linux_aarch64, unit_tests_windows_x64, unit_tests_windows_arm64, unit_tests_macos_arm64, unit_tests_flatpak_x86_64, unit_tests_flatpak_aarch64]
|
||||
needs: [unit_tests_linux_x86_64, unit_tests_linux_aarch64, unit_tests_windows_x64, unit_tests_windows_arm64, unit_tests_macos_arm64]
|
||||
if: ${{ !cancelled() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -281,148 +257,38 @@ jobs:
|
||||
echo "date=$(date +'%Y%m%d')" >> $GITHUB_ENV
|
||||
echo "git_commit_hash=$git_commit_hash" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
- name: Compute the flatpak-builder cache key
|
||||
id: fp_cache_key
|
||||
run: echo "key=flatpak-builder-${{ matrix.variant.arch }}-${{ hashFiles('deps/**', 'scripts/flatpak/com.orcaslicer.OrcaSlicer.yml', 'scripts/flatpak/make_deps_tar.sh') }}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash
|
||||
# Manage flatpak-builder cache externally so PRs restore but never upload.
|
||||
# The compiler cache under it is keyed per run below, so it is left out.
|
||||
# Manage flatpak-builder cache externally so PRs restore but never upload
|
||||
- name: Restore flatpak-builder cache
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: |
|
||||
.flatpak-builder/*
|
||||
!.flatpak-builder/ccache
|
||||
key: ${{ steps.fp_cache_key.outputs.key }}
|
||||
path: .flatpak-builder
|
||||
key: flatpak-builder-${{ matrix.variant.arch }}-${{ github.event.pull_request.base.sha }}
|
||||
restore-keys: flatpak-builder-${{ matrix.variant.arch }}-
|
||||
- name: Save/restore flatpak-builder cache
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
.flatpak-builder/*
|
||||
!.flatpak-builder/ccache
|
||||
key: ${{ steps.fp_cache_key.outputs.key }}
|
||||
path: .flatpak-builder
|
||||
key: flatpak-builder-${{ matrix.variant.arch }}-${{ github.sha }}
|
||||
restore-keys: flatpak-builder-${{ matrix.variant.arch }}-
|
||||
# Compiler cache for the OrcaSlicer module, as in build_orca.yml. Pull
|
||||
# requests only restore it; every other run (main, release branches, the
|
||||
# nightly, a dispatch) saves it. orca_deps stays on the state cache above.
|
||||
- name: Name the compiler cache leg
|
||||
run: |
|
||||
leg="Flatpak-${{ matrix.variant.arch }}"
|
||||
echo "CCACHE_LEG=$leg" >> "$GITHUB_ENV"
|
||||
echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV"
|
||||
shell: bash
|
||||
- name: Restore compiler cache
|
||||
id: ccache_restore
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: .flatpak-builder/ccache
|
||||
key: ${{ env.CCACHE_ENTRY }}
|
||||
restore-keys: ccache-${{ env.CCACHE_LEG }}-
|
||||
- name: Disable debug info for faster CI builds
|
||||
run: |
|
||||
sed -i '/^build-options:/a\ no-debuginfo: true\n strip: true' \
|
||||
scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
|
||||
shell: bash
|
||||
# flatpak-builder reuses a module from its cache when the definition and
|
||||
# sources are unchanged, so a re-run of the same commit would skip the
|
||||
# OrcaSlicer module and ship no test asset. A per-run value in that module's
|
||||
# env keeps it rebuilding; orca_deps stays cached, and the compiler cache
|
||||
# still serves the rebuild.
|
||||
- name: Inject commit hash and flatpak-builder cache buster into Flatpak manifest
|
||||
env:
|
||||
flatpak_builder_cache_buster: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||
- name: Inject git commit hash into Flatpak manifest
|
||||
run: |
|
||||
sed -i "/name: OrcaSlicer/{n;s|buildsystem: simple|buildsystem: simple\n build-options:\n env:\n flatpak_builder_cache_buster: \"$flatpak_builder_cache_buster\"\n git_commit_hash: \"$git_commit_hash\"|}" \
|
||||
sed -i "/name: OrcaSlicer/{n;s|buildsystem: simple|buildsystem: simple\n build-options:\n env:\n git_commit_hash: \"$git_commit_hash\"|}" \
|
||||
scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
|
||||
shell: bash
|
||||
# flatpak-builder's --ccache only wraps cc and gcc, and the manifest builds
|
||||
# with clang, so CMake's launcher runs ccache instead; --ccache is still what
|
||||
# mounts the cache directory into the sandbox. The settings go into that
|
||||
# directory's own config file, which the sandbox reads too.
|
||||
- name: Enable compiler cache
|
||||
run: |
|
||||
printf ' %s\n' \
|
||||
'CMAKE_C_COMPILER_LAUNCHER: ccache' \
|
||||
'CMAKE_CXX_COMPILER_LAUNCHER: ccache' > "$RUNNER_TEMP/ccache-env.yml"
|
||||
sed -i "/^ git_commit_hash: /r $RUNNER_TEMP/ccache-env.yml" \
|
||||
scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
|
||||
grep -q '^ CMAKE_CXX_COMPILER_LAUNCHER: ccache$' scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
|
||||
mkdir -p .flatpak-builder/ccache
|
||||
export CCACHE_DIR=$PWD/.flatpak-builder/ccache
|
||||
ccache --set-config=max_size=3G
|
||||
# The compiler is reinstalled every run, so its mtime means nothing.
|
||||
ccache --set-config=compiler_check=content
|
||||
# Headers a fresh checkout has just written, the few files that use
|
||||
# __DATE__ or __TIME__, and the precompiled header, whose macros ccache
|
||||
# cannot see.
|
||||
ccache --set-config=sloppiness=pch_defines,time_macros,include_file_mtime,include_file_ctime
|
||||
# Hash the includes the compiler reports instead of preprocessing every
|
||||
# miss before compiling it.
|
||||
ccache --set-config=depend_mode=true
|
||||
# The restored directory carries the previous run's counters.
|
||||
ccache -z
|
||||
shell: bash
|
||||
- name: Check the manifest keeps orca_deps cacheable
|
||||
run: ./scripts/flatpak/check_manifest_cacheable.sh
|
||||
shell: bash
|
||||
- name: Pack deps/ for the Flatpak manifest
|
||||
run: ./scripts/flatpak/make_deps_tar.sh
|
||||
shell: bash
|
||||
- uses: flatpak/flatpak-github-actions/flatpak-builder@master
|
||||
with:
|
||||
bundle: OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
|
||||
manifest-path: scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
|
||||
# cache only turns on flatpak-builder --ccache; the caching itself is above.
|
||||
cache: true
|
||||
restore-cache: false
|
||||
save-cache: false
|
||||
cache: false
|
||||
arch: ${{ matrix.variant.arch }}
|
||||
upload-artifact: false
|
||||
# run-tests fires the module's build-only test-commands; keep-build-dirs
|
||||
# retains the binaries for the packaging step below.
|
||||
run-tests: true
|
||||
keep-build-dirs: true
|
||||
# The build has just touched everything it can use, so an object untouched
|
||||
# for a week is dead, usually orphaned by a flag change.
|
||||
- name: Compiler cache statistics
|
||||
if: always()
|
||||
run: |
|
||||
export CCACHE_DIR=$PWD/.flatpak-builder/ccache
|
||||
ccache --evict-older-than 7d
|
||||
ccache -s -v || ccache -s
|
||||
shell: bash
|
||||
# Save the new entry first, then drop the older ones for this leg on this
|
||||
# ref, so a failed save leaves the previous entry in place. A cancelled or
|
||||
# failed build saves too, since what it compiled is still valid; a restore
|
||||
# that did not finish does not, since the directory may be a truncated copy.
|
||||
- name: Save compiler cache
|
||||
id: ccache_save
|
||||
if: ${{ always() && steps.ccache_restore.outcome == 'success' && github.event_name != 'pull_request' }}
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
path: .flatpak-builder/ccache
|
||||
key: ${{ env.CCACHE_ENTRY }}
|
||||
- name: Drop older compiler cache entries
|
||||
if: ${{ always() && steps.ccache_save.outcome == 'success' }}
|
||||
# The container has no gh, so this is the list and delete over the REST API.
|
||||
# Older means a lower run id, so two runs finishing close together keep
|
||||
# the newer entry whichever of them cleans up last.
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
api="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/caches"
|
||||
curl -sSf -H "Authorization: Bearer $GH_TOKEN" \
|
||||
"$api?ref=$GITHUB_REF&key=ccache-$CCACHE_LEG-&per_page=100" \
|
||||
| jq -r --arg prefix "ccache-$CCACHE_LEG-" --argjson run "$GITHUB_RUN_ID" \
|
||||
'.actions_caches[] | select((.key | ltrimstr($prefix) | split("-")[0] | tonumber?) < $run) | .id' \
|
||||
| while read -r id; do
|
||||
curl -sSf -X DELETE -H "Authorization: Bearer $GH_TOKEN" "$api/$id"
|
||||
done
|
||||
shell: bash
|
||||
- name: Upload artifacts Flatpak
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
@@ -438,46 +304,3 @@ jobs:
|
||||
asset_name: OrcaSlicer-Linux-flatpak_nightly${{ env.nightly_suffix }}_${{ matrix.variant.arch }}.flatpak
|
||||
asset_content_type: application/octet-stream
|
||||
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
|
||||
# The asset is /app (the exes link it at runtime) plus the build tree
|
||||
# slimmed to what ctest needs.
|
||||
- name: Package flatpak test asset
|
||||
shell: bash
|
||||
run: |
|
||||
d=$(ls -d .flatpak-builder/build/OrcaSlicer-* | tail -1)
|
||||
find "$d/build_flatpak" -mindepth 1 -maxdepth 1 ! -name tests -exec rm -rf {} +
|
||||
# Strip debug info (the SDK builds with -g, only the app gets stripped);
|
||||
# the bounds checks are compiled in, so a stripped exe still catches them.
|
||||
find "$d/build_flatpak/tests" -type f -perm -u+x -exec strip --strip-unneeded {} + 2>/dev/null || true
|
||||
# At runtime the tests read tests/ (TEST_DATA_DIR), scripts/, and under
|
||||
# resources/ the shipped profiles (PROFILES_DIR) and the printers/ maps.
|
||||
find "$d" -mindepth 1 -maxdepth 1 -type d \
|
||||
! -name tests ! -name build_flatpak ! -name scripts ! -name resources -exec rm -rf {} +
|
||||
find "$d/resources" -mindepth 1 -maxdepth 1 ! -name profiles ! -name printers -exec rm -rf {} +
|
||||
tar -cf flatpak-test-asset.tar flatpak_app "$d"
|
||||
- name: Upload flatpak test asset
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ github.sha }}-flatpak-tests-${{ matrix.variant.arch }}
|
||||
path: flatpak-test-asset.tar
|
||||
retention-days: 1
|
||||
# keep-build-dirs would otherwise land in the flatpak-builder cache saved post-job.
|
||||
- name: Drop the kept build dirs before the flatpak-builder cache saves
|
||||
if: always()
|
||||
shell: bash
|
||||
run: rm -rf .flatpak-builder/build
|
||||
unit_tests_flatpak_x86_64:
|
||||
name: Flatpak x86_64
|
||||
needs: flatpak
|
||||
if: ${{ !cancelled() && success() }}
|
||||
uses: ./.github/workflows/unit_tests_flatpak.yml
|
||||
with:
|
||||
os: ubuntu-24.04
|
||||
artifact: ${{ github.sha }}-flatpak-tests-x86_64
|
||||
unit_tests_flatpak_aarch64:
|
||||
name: Flatpak aarch64
|
||||
needs: flatpak
|
||||
if: ${{ !cancelled() && success() }}
|
||||
uses: ./.github/workflows/unit_tests_flatpak.yml
|
||||
with:
|
||||
os: ubuntu-24.04-arm
|
||||
artifact: ${{ github.sha }}-flatpak-tests-aarch64
|
||||
|
||||
@@ -9,10 +9,6 @@ on:
|
||||
arch:
|
||||
required: false
|
||||
type: string
|
||||
compiler:
|
||||
required: false
|
||||
type: string
|
||||
default: msvc
|
||||
build-deps-only:
|
||||
required: false
|
||||
type: boolean
|
||||
@@ -37,12 +33,11 @@ jobs:
|
||||
- name: set outputs
|
||||
id: set_outputs
|
||||
env:
|
||||
# Anything that changes how the tree is built belongs in the key, or a job
|
||||
# restores one it cannot use. Linux amd64 passes no arch deliberately, so
|
||||
# 'linux-clang' keeps the cache it already has.
|
||||
cache-os: ${{ runner.os == 'macOS' && format('macos-{0}', inputs.arch) || (runner.os == 'Windows' && format('windows-{0}-{1}', inputs.arch, inputs.compiler) || format('linux-clang{0}', inputs.arch && format('-{0}', inputs.arch) || '')) }}
|
||||
# The Windows ARM64 deps build in build-arm64, all others under build;
|
||||
# build_deps.yml and build_orca.yml pass the Windows directory to build_win.bat.
|
||||
# Keep macOS/Windows cache keys architecture-specific. amd64 Linux passes
|
||||
# no arch (key stays 'linux-clang', preserving the existing cache);
|
||||
# aarch64 gets its own 'linux-clang-aarch64' key.
|
||||
cache-os: ${{ runner.os == 'macOS' && format('macos-{0}', inputs.arch) || (runner.os == 'Windows' && format('windows-{0}', inputs.arch) || format('linux-clang{0}', inputs.arch && format('-{0}', inputs.arch) || '')) }}
|
||||
# ARM64 builds use the build-arm64 tree (see build_release_vs.bat); x64/other use build.
|
||||
dep-folder-name: ${{ runner.os == 'macOS' && format('/{0}', inputs.arch) || (runner.os == 'Windows' && inputs.arch == 'arm64') && '-arm64/OrcaSlicer_dep' || '/OrcaSlicer_dep' }}
|
||||
output-cmd: ${{ runner.os == 'Windows' && '$env:GITHUB_OUTPUT' || '"$GITHUB_OUTPUT"'}}
|
||||
run: |
|
||||
@@ -67,7 +62,6 @@ jobs:
|
||||
valid-cache: ${{ needs.check_cache.outputs.valid-cache == 'true' }}
|
||||
os: ${{ inputs.os }}
|
||||
arch: ${{ inputs.arch }}
|
||||
compiler: ${{ inputs.compiler }}
|
||||
build-deps-only: ${{ inputs.build-deps-only }}
|
||||
force-build: ${{ inputs.force-build }}
|
||||
secrets: inherit
|
||||
|
||||
@@ -16,10 +16,6 @@ on:
|
||||
arch:
|
||||
required: false
|
||||
type: string
|
||||
compiler:
|
||||
required: false
|
||||
type: string
|
||||
default: msvc
|
||||
build-deps-only:
|
||||
required: false
|
||||
type: boolean
|
||||
@@ -138,11 +134,14 @@ jobs:
|
||||
if (-not "${{ vars.SELF_HOSTED }}") {
|
||||
choco install strawberryperl
|
||||
}
|
||||
# cache-path is the install directory inside the deps build directory.
|
||||
$deps = (Split-Path "${{ inputs.cache-path }}").Replace('\', '/')
|
||||
# -l compiles with Visual Studio's clang-cl and -x builds with Ninja; --msvc --msbuild is cl under the Visual Studio generator.
|
||||
$flags = if ("${{ inputs.compiler }}" -eq "clang") { "-l", "-x" } else { "--msvc", "--msbuild" }
|
||||
.\build_win.bat -d --arch ${{ inputs.arch }} --deps-dir $deps @flags
|
||||
$arch = "${{ inputs.arch }}"
|
||||
if ($arch -eq "arm64") {
|
||||
.\build_release_vs.bat deps arm64
|
||||
.\build_release_vs.bat pack arm64
|
||||
} else {
|
||||
.\build_release_vs.bat deps
|
||||
.\build_release_vs.bat pack
|
||||
}
|
||||
shell: pwsh
|
||||
|
||||
- name: Build on Mac ${{ inputs.arch }}
|
||||
@@ -150,9 +149,9 @@ jobs:
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
if [ -z "${{ vars.SELF_HOSTED }}" ]; then
|
||||
brew install automake texinfo libtool pkgconf yasm nasm
|
||||
brew install automake texinfo libtool
|
||||
fi
|
||||
./build_release_macos.sh -dx ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -a ${{ inputs.arch }} -t 10.15
|
||||
./build_release_macos.sh -dx ${{ !vars.SELF_HOSTED && '-1' || '' }} -a ${{ inputs.arch }} -t 10.15
|
||||
(cd "${{ github.workspace }}/deps/build/${{ inputs.arch }}" && \
|
||||
find . -mindepth 1 -maxdepth 1 ! -name 'OrcaSlicer_dep' -exec rm -rf {} +)
|
||||
|
||||
@@ -205,5 +204,4 @@ jobs:
|
||||
cache-path: ${{ inputs.cache-path }}
|
||||
os: ${{ inputs.os }}
|
||||
arch: ${{ inputs.arch }}
|
||||
compiler: ${{ inputs.compiler }}
|
||||
secrets: inherit
|
||||
|
||||
@@ -13,10 +13,6 @@ on:
|
||||
arch:
|
||||
required: false
|
||||
type: string
|
||||
compiler:
|
||||
required: false
|
||||
type: string
|
||||
default: msvc
|
||||
macos-combine-only:
|
||||
required: false
|
||||
type: boolean
|
||||
@@ -76,69 +72,6 @@ jobs:
|
||||
if (-not (Test-Path "$cmakeBin\cmake.exe")) { throw "cmake.exe not found at $cmakeBin" }
|
||||
Add-Content -Path $env:GITHUB_PATH -Value $cmakeBin
|
||||
|
||||
# Compiler cache. Pushes save it, so main keeps it warm; pull requests
|
||||
# restore it and discard what they compiled. Objects are keyed on the
|
||||
# preprocessed source, the compiler and the flags, so a leg only ever
|
||||
# hits its own entries. A failed install costs the caching, not the build.
|
||||
- name: Name the compiler cache leg
|
||||
if: ${{ !inputs.macos-combine-only }}
|
||||
shell: bash
|
||||
run: |
|
||||
leg="${{ runner.os }}-${{ inputs.arch || 'amd64' }}${{ runner.os == 'Windows' && format('-{0}', inputs.compiler) || '' }}"
|
||||
# clang-cl refuses a precompiled header from another cl.exe build and ccache
|
||||
# does not hash that build, so each one gets its own cache. The build number
|
||||
# is read from cl.exe itself; the toolset directory keeps its name across patches.
|
||||
if [ "${{ runner.os }}" = Windows ]; then
|
||||
vswhere='/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe'
|
||||
toolset=$(tr -d '\r\n' < "$("$vswhere" -latest -products '*' -find 'VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt' | tr -d '\r')")
|
||||
cl=$("$vswhere" -latest -products '*' -find 'VC\Tools\MSVC\'"$toolset"'\**\cl.exe' | tr -d '\r' | head -1)
|
||||
leg="$leg-vc$("$cl" 2>&1 | grep -o -E 'Version [0-9.]+' | cut -d' ' -f2)"
|
||||
fi
|
||||
echo "CCACHE_LEG=$leg" >> "$GITHUB_ENV"
|
||||
echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV"
|
||||
|
||||
# The action only installs and configures ccache. Restore and save go
|
||||
# through actions/cache with one path string, since the cache service
|
||||
# only matches entries saved under the identical path and the action
|
||||
# spells it differently on Windows.
|
||||
- name: Compiler cache
|
||||
id: ccache
|
||||
if: ${{ !inputs.macos-combine-only }}
|
||||
continue-on-error: true
|
||||
uses: hendrikmuhs/ccache-action@v1.2.24
|
||||
with:
|
||||
key: ${{ env.CCACHE_LEG }}
|
||||
max-size: 3G
|
||||
restore: false
|
||||
save: false
|
||||
# ccache -s runs as its own step; no summary table per job.
|
||||
job-summary: ''
|
||||
|
||||
- name: Restore compiler cache
|
||||
id: ccache_restore
|
||||
if: ${{ steps.ccache.outcome == 'success' }}
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: ${{ github.workspace }}/.ccache
|
||||
key: ${{ env.CCACHE_ENTRY }}
|
||||
restore-keys: ccache-${{ env.CCACHE_LEG }}-
|
||||
|
||||
- name: Enable compiler cache
|
||||
if: ${{ steps.ccache.outcome == 'success' }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "CMAKE_C_COMPILER_LAUNCHER=ccache" >> "$GITHUB_ENV"
|
||||
echo "CMAKE_CXX_COMPILER_LAUNCHER=ccache" >> "$GITHUB_ENV"
|
||||
# Headers a fresh checkout has just written, the few files that
|
||||
# use __DATE__ or __TIME__, and the precompiled header, whose
|
||||
# macros ccache cannot see.
|
||||
echo "CCACHE_SLOPPINESS=pch_defines,time_macros,include_file_mtime,include_file_ctime" >> "$GITHUB_ENV"
|
||||
# Hash the includes the compiler reports instead of preprocessing
|
||||
# every miss before compiling it.
|
||||
echo "CCACHE_DEPEND=1" >> "$GITHUB_ENV"
|
||||
# The restored directory carries the previous run's counters.
|
||||
ccache -z
|
||||
|
||||
- name: Get the version and date on Ubuntu and macOS
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
@@ -212,7 +145,7 @@ jobs:
|
||||
env:
|
||||
ORCA_TESTS_BUILD_ONLY: ${{ inputs.arch == 'arm64' && '1' || '' }}
|
||||
run: |
|
||||
./build_release_macos.sh -s -n -x ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -a ${{ inputs.arch }} -t 10.15 ${{ inputs.arch == 'arm64' && '-T' || '' }}
|
||||
./build_release_macos.sh -s -n -x ${{ !vars.SELF_HOSTED && '-1' || '' }} -a ${{ inputs.arch }} -t 10.15 ${{ inputs.arch == 'arm64' && '-T' || '' }}
|
||||
|
||||
- name: Pack unit tests mac
|
||||
if: runner.os == 'macOS' && !inputs.macos-combine-only && inputs.arch == 'arm64'
|
||||
@@ -229,14 +162,6 @@ jobs:
|
||||
retention-days: 5
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Build system preset cache (macOS)
|
||||
if: runner.os == 'macOS' && !inputs.macos-combine-only
|
||||
working-directory: ${{ github.workspace }}
|
||||
shell: bash
|
||||
# The bundle was already packed from resources/, so the caches have to be
|
||||
# installed into it here; the source tree keeps its JSONs for later jobs.
|
||||
run: ./scripts/build_preset_cache.sh -b build/${{ inputs.arch }} build/${{ inputs.arch }}/OrcaSlicer/OrcaSlicer.app/Contents/Resources/profiles
|
||||
|
||||
- name: Pack macOS app bundle ${{ inputs.arch }}
|
||||
if: runner.os == 'macOS' && !inputs.macos-combine-only
|
||||
working-directory: ${{ github.workspace }}
|
||||
@@ -271,7 +196,7 @@ jobs:
|
||||
if: runner.os == 'macOS' && inputs.macos-combine-only
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
./build_release_macos.sh -u -x ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -a universal -t 10.15
|
||||
./build_release_macos.sh -u -x ${{ !vars.SELF_HOSTED && '-1' || '' }} -a universal -t 10.15
|
||||
|
||||
# Thanks to RaySajuuk, it's working now
|
||||
- name: Sign app and notary
|
||||
@@ -459,22 +384,12 @@ jobs:
|
||||
# env:
|
||||
# WindowsSdkDir: 'C:\Program Files (x86)\Windows Kits\10\'
|
||||
# WindowsSDKVersion: '10.0.26100.0\'
|
||||
# --tests builds the unit tests too; the unit_tests_windows_* jobs run them.
|
||||
# "tests" builds the unit tests too; the unit_tests_windows_* jobs run them.
|
||||
run: |
|
||||
# cache-path is the install directory inside the deps build directory.
|
||||
$deps = (Split-Path "${{ inputs.cache-path }}").Replace('\', '/')
|
||||
# -l compiles with Visual Studio's clang-cl and -x builds with Ninja; --msvc --msbuild is cl under the Visual Studio generator.
|
||||
$flags = if ("${{ inputs.compiler }}" -eq "clang") { "-l", "-x" } else { "--msvc", "--msbuild" }
|
||||
.\build_win.bat -s --tests -i --arch ${{ inputs.arch }} --build-dir $env:BUILD_DIR --deps-dir $deps @flags
|
||||
$arch = "${{ inputs.arch }}"
|
||||
if ($arch -eq "arm64") { .\build_release_vs.bat slicer arm64 tests } else { .\build_release_vs.bat slicer tests }
|
||||
shell: pwsh
|
||||
|
||||
- name: Build system preset cache (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: cmd
|
||||
# Shipped into both the already-installed tree (portable zip, MSIX) and
|
||||
# the checkout cpack re-installs from when it builds the NSIS installer.
|
||||
run: scripts\build_preset_cache.bat --prune-source "%BUILD_DIR%" "resources\profiles" "%BUILD_DIR%\OrcaSlicer\resources\profiles"
|
||||
|
||||
- name: Pack unit tests Win
|
||||
if: runner.os == 'Windows'
|
||||
working-directory: ${{ github.workspace }}
|
||||
@@ -624,20 +539,6 @@ jobs:
|
||||
retention-days: 5
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Build system preset cache (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
# Both were packed from resources/ before the caches existed, so the
|
||||
# AppImage is unpacked first and the caches shipped into it and into
|
||||
# the package tree; the source tree keeps its JSONs for later steps.
|
||||
appimage=$(find build -maxdepth 1 -name "OrcaSlicer_Linux_AppImage*.AppImage" | head -1)
|
||||
chmod +x "$appimage"
|
||||
"$appimage" --appimage-extract
|
||||
./scripts/build_preset_cache.sh -b build build/package/resources/profiles squashfs-root/resources/profiles
|
||||
appimagetool=$(find build -name "appimagetool.AppImage" | head -1)
|
||||
ARCH=$(uname -m) "$appimagetool" --appimage-extract-and-run squashfs-root "$appimage"
|
||||
rm -rf squashfs-root
|
||||
# Ship the freshly-built validator so slice_check_linux (build_all.yml)
|
||||
# can slice-sweep the shipped profiles with this PR's engine. Taken from
|
||||
# the aarch64 leg so the sweep also exercises the arm build; x86_64 on
|
||||
@@ -725,41 +626,3 @@ jobs:
|
||||
asset_name: orca_custom_preset_tests.zip
|
||||
asset_content_type: application/octet-stream
|
||||
max_releases: 1
|
||||
|
||||
# The build has just touched everything it can use, so an object
|
||||
# untouched for a week is dead, usually orphaned by a flag change.
|
||||
- name: Compiler cache statistics
|
||||
if: ${{ always() && steps.ccache.outcome == 'success' }}
|
||||
shell: bash
|
||||
run: |
|
||||
ccache --evict-older-than 7d
|
||||
ccache -s -v || ccache -s
|
||||
|
||||
# Entries are immutable, so the new one is saved first and the older
|
||||
# ones for this leg on this ref are dropped afterwards: a failed save
|
||||
# leaves the previous entry in place. A cancelled or failed build saves
|
||||
# too, since what it compiled is still valid; a restore that did not
|
||||
# finish does not, since the directory may be a truncated copy.
|
||||
- name: Save compiler cache
|
||||
id: ccache_save
|
||||
if: ${{ always() && steps.ccache_restore.outcome == 'success' && github.event_name != 'pull_request' }}
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
path: ${{ github.workspace }}/.ccache
|
||||
key: ${{ env.CCACHE_ENTRY }}
|
||||
|
||||
- name: Drop older compiler cache entries
|
||||
if: ${{ always() && steps.ccache_save.outcome == 'success' }}
|
||||
# A read-only token (fork PRs) cannot delete; that only costs storage.
|
||||
# Older means a lower run id, so two runs finishing close together keep
|
||||
# the newer entry whichever of them cleans up last.
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh cache list --ref "$GITHUB_REF" --key "ccache-$CCACHE_LEG-" --limit 100 --json id,key \
|
||||
| jq -r --arg prefix "ccache-$CCACHE_LEG-" --argjson run "$GITHUB_RUN_ID" \
|
||||
'.[] | select((.key | ltrimstr($prefix) | split("-")[0] | tonumber?) < $run) | .id' \
|
||||
| tr -d '\r' \
|
||||
| while read -r id; do gh cache delete "$id"; done
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
name: Check profiles
|
||||
on:
|
||||
pull_request:
|
||||
# release/* is included because pr-merge-bot.yml lets delegates merge into
|
||||
# it, and it gates on this workflow's result. Without it a delegated merge
|
||||
# into a release branch would run no profile validation at all.
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
paths:
|
||||
- 'resources/profiles/**'
|
||||
# orca_profile_tool.py also validates resources/printers/bambu_filament_ids.json, and
|
||||
# both it and its tests live in scripts/, so a PR touching only those must still run
|
||||
# this workflow.
|
||||
- 'resources/printers/**'
|
||||
- 'scripts/**'
|
||||
- ".github/workflows/check_profiles.yml"
|
||||
|
||||
workflow_dispatch:
|
||||
@@ -29,31 +20,18 @@ permissions:
|
||||
|
||||
jobs:
|
||||
check_profiles:
|
||||
# This job name is the check-run name pr-merge-bot.yml requires before a
|
||||
# delegated merge. Renaming it silently disables that gate.
|
||||
name: Check profiles
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
# Deliberately not continue-on-error, unlike every check below: if the tool itself is
|
||||
# broken, nothing it then reports about the profiles is worth reading.
|
||||
- name: Run the profile tool's own unit tests
|
||||
run: python3 -m unittest discover -s scripts/tests -t scripts
|
||||
|
||||
# What the validator below cannot see. It loads the tree the way the slicer does, so
|
||||
# it never notices a profile no <vendor>.json indexes, a preset name two files claim,
|
||||
# an id that is not the mint of its own triple, or a file that normalize and
|
||||
# update-index would still rewrite.
|
||||
# The step id is the handle the PR comment and the failure gate below use; renaming it
|
||||
# silently disables them.
|
||||
- name: Check profiles (orca_profile_tool.py)
|
||||
id: profile_tool
|
||||
- name: Run extra JSON check
|
||||
id: extra_json_check
|
||||
continue-on-error: true
|
||||
run: |
|
||||
set +e
|
||||
python3 ./scripts/orca_profile_tool.py check 2>&1 | tee ${{ runner.temp }}/profile_tool.log
|
||||
python3 ./scripts/orca_extra_profile_check.py 2>&1 | tee ${{ runner.temp }}/extra_json_check.log
|
||||
exit ${PIPESTATUS[0]}
|
||||
|
||||
# download
|
||||
@@ -80,14 +58,13 @@ jobs:
|
||||
set +e
|
||||
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -s -l 2 2>&1 | tee ${{ runner.temp }}/validate_slice.log
|
||||
exit ${PIPESTATUS[0]}
|
||||
# All vendors' filament_id collisions were fixed, so the duplicate-filament-subtype
|
||||
# check runs tree-wide.
|
||||
- name: validate filament subtype check
|
||||
# For now run filament subtype check only for BBL profiles until we fix other vendors' profiles.
|
||||
- name: validate filament subtype check for BBL profiles
|
||||
id: validate_filament_subtypes
|
||||
continue-on-error: true
|
||||
run: |
|
||||
set +e
|
||||
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 -f 2>&1 | tee ${{ runner.temp }}/validate_filament_subtypes.log
|
||||
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 -v BBL -f 2>&1 | tee ${{ runner.temp }}/validate_filament_subtypes.log
|
||||
exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: validate custom presets
|
||||
@@ -198,7 +175,7 @@ jobs:
|
||||
echo "${{ github.event.pull_request.number }}" > ${{ runner.temp }}/profile-check-results/pr_number.txt
|
||||
|
||||
- name: Prepare comment artifact
|
||||
if: ${{ always() && github.event_name == 'pull_request' && (steps.profile_tool.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
|
||||
if: ${{ always() && github.event_name == 'pull_request' && (steps.extra_json_check.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
|
||||
run: |
|
||||
{
|
||||
# Marker matched by check_profiles_comment.yml to delete prior comments.
|
||||
@@ -206,11 +183,11 @@ jobs:
|
||||
echo "## :x: Profile Validation Errors"
|
||||
echo ""
|
||||
|
||||
if [ "${{ steps.profile_tool.outcome }}" = "failure" ]; then
|
||||
echo "### Profile Check Failed (orca_profile_tool.py)"
|
||||
if [ "${{ steps.extra_json_check.outcome }}" = "failure" ]; then
|
||||
echo "### Extra JSON Check Failed"
|
||||
echo ""
|
||||
echo '```'
|
||||
head -c 30000 ${{ runner.temp }}/profile_tool.log || echo "No output captured"
|
||||
head -c 30000 ${{ runner.temp }}/extra_json_check.log || echo "No output captured"
|
||||
echo '```'
|
||||
echo ""
|
||||
fi
|
||||
@@ -234,7 +211,7 @@ jobs:
|
||||
fi
|
||||
|
||||
if [ "${{ steps.validate_filament_subtypes.outcome }}" = "failure" ]; then
|
||||
echo "### Filament Subtype Validation Failed"
|
||||
echo "### BBL Filament Subtype Validation Failed"
|
||||
echo ""
|
||||
echo '```'
|
||||
head -c 30000 ${{ runner.temp }}/validate_filament_subtypes.log || echo "No output captured"
|
||||
@@ -252,7 +229,7 @@ jobs:
|
||||
fi
|
||||
|
||||
echo "---"
|
||||
echo '*Fix the errors above and push a new commit. To reproduce this run locally: `scripts/check_profile.sh`, or `scripts\check_profile.bat` on Windows.*'
|
||||
echo "*Please fix the above errors and push a new commit.*"
|
||||
} > ${{ runner.temp }}/profile-check-results/pr_comment.md
|
||||
|
||||
- name: Upload comment artifact
|
||||
@@ -264,8 +241,7 @@ jobs:
|
||||
retention-days: 1
|
||||
|
||||
- name: Fail if any check failed
|
||||
if: ${{ always() && (steps.profile_tool.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
|
||||
if: ${{ always() && (steps.extra_json_check.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
|
||||
run: |
|
||||
echo "One or more profile checks failed; see the step logs above."
|
||||
echo 'Reproduce the whole run locally with scripts/check_profile.sh (scripts\check_profile.bat on Windows).'
|
||||
echo "One or more profile checks failed. See above for details."
|
||||
exit 1
|
||||
|
||||
@@ -19,18 +19,11 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
# Doxygen with call graphs over all of src/ outgrows the runner's RAM;
|
||||
# replace the runner's swapfile with an 8 GB one.
|
||||
- name: Grow swap space
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo swapoff -a
|
||||
sudo rm -f /swapfile
|
||||
sudo fallocate -l 8G /swapfile
|
||||
sudo chmod 600 /swapfile
|
||||
sudo mkswap /swapfile
|
||||
sudo swapon /swapfile
|
||||
free -h
|
||||
- uses: thejerrybao/setup-swap-space@v1
|
||||
with:
|
||||
swap-space-path: /swapfile
|
||||
swap-size-gb: 8
|
||||
remove-existing-swap-files: true
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
# Nightly parity checks from OrcaSlicer/orca-test-repo, kept out of the
|
||||
# per-build "Run external slicer regression tests" step because they take far
|
||||
# longer than that step's budget:
|
||||
# effect - the CLI override sweep's full effect stage: every landed option
|
||||
# re-sliced on its own to see whether it changes the G-code
|
||||
# harness - the GUI-vs-CLI parity harness (metrics only, never fails)
|
||||
# Both test the latest successful build_all.yml Linux AppImage from main, with
|
||||
# sources checked out at the commit that build was made from. Nothing here
|
||||
# gates a build or a PR.
|
||||
name: Parity Nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# build_all.yml starts at 17:00 UTC and has finished by ~20:00
|
||||
- cron: "0 21 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test_repo_ref:
|
||||
description: "orca-test-repo ref to run"
|
||||
required: false
|
||||
default: "main"
|
||||
build_branch:
|
||||
description: "branch whose latest successful build_all artifact to test"
|
||||
required: false
|
||||
default: "main"
|
||||
fixtures:
|
||||
description: "harness fixture ids, space-separated (empty = all)"
|
||||
required: false
|
||||
default: ""
|
||||
cli_presets:
|
||||
description: "harness lane C presets: flat = flatten inherits first, raw = leaf profile as-is"
|
||||
required: false
|
||||
default: "flat"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Find the build to test
|
||||
# Don't run scheduled checks on forks
|
||||
if: github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
run_id: ${{ steps.find.outputs.run_id }}
|
||||
head_sha: ${{ steps.find.outputs.head_sha }}
|
||||
steps:
|
||||
- id: find
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh run list --workflow build_all.yml \
|
||||
--branch "${{ inputs.build_branch || 'main' }}" \
|
||||
--status success --limit 1 --json databaseId,headSha \
|
||||
--jq '"run_id=\(.[0].databaseId)\nhead_sha=\(.[0].headSha)"' \
|
||||
>> "$GITHUB_OUTPUT"
|
||||
cat "$GITHUB_OUTPUT"
|
||||
|
||||
effect:
|
||||
name: Override sweep effect stage (shard ${{ matrix.shard }})
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# orca-test-repo's parity/effect_routing.json holds a 2-way split,
|
||||
# ~12.5 min a shard on this runner
|
||||
shard: [0, 1]
|
||||
steps:
|
||||
- &checkout-suite
|
||||
name: Check out the test suite
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: OrcaSlicer/orca-test-repo
|
||||
ref: ${{ inputs.test_repo_ref || 'main' }}
|
||||
path: orca-test-repo
|
||||
|
||||
# The AppImage ships only packed preset caches, so profiles and the CLI
|
||||
# option surface come from the sources the build was made from
|
||||
- &checkout-slicer
|
||||
name: Check out OrcaSlicer at the build's commit
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.build.outputs.head_sha }}
|
||||
path: slicer
|
||||
lfs: 'false'
|
||||
|
||||
- &extract-appimage
|
||||
name: Download and extract the Linux AppImage
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh run download "${{ needs.build.outputs.run_id }}" --dir appimage \
|
||||
--pattern "OrcaSlicer_Linux_ubuntu_2404*"
|
||||
appimage=$(find appimage -name "*.AppImage" ! -name "*aarch64*" | head -1)
|
||||
[ -n "$appimage" ] || { echo "no x86_64 AppImage in run ${{ needs.build.outputs.run_id }}"; exit 1; }
|
||||
chmod +x "$appimage"
|
||||
"$appimage" --appimage-extract > /dev/null
|
||||
# The bare binary cannot find the AppImage's bundled libraries; AppRun
|
||||
# sets them up and execs it, so exit codes and signals pass through
|
||||
[ -x squashfs-root/AppRun ] || { echo "no AppRun in the AppImage"; exit 1; }
|
||||
echo "ORCA_BIN=$PWD/squashfs-root/AppRun" >> "$GITHUB_ENV"
|
||||
echo "ORCA_SOURCE=$PWD/slicer" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install the AppImage's host runtime dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install suite dependencies
|
||||
run: pip install -r orca-test-repo/requirements.txt
|
||||
|
||||
- name: Run the override sweep with the full effect stage
|
||||
id: run
|
||||
continue-on-error: true
|
||||
working-directory: orca-test-repo
|
||||
run: |
|
||||
set -o pipefail
|
||||
# -rA keeps the per-stage summaries, which pytest otherwise swallows
|
||||
# for passing tests
|
||||
python -m pytest test_cli_overrides.py -c pytest.ini -v -rA \
|
||||
--effect-full --effect-shard ${{ matrix.shard }}/2 \
|
||||
--orca-bin "$ORCA_BIN" --orca-source "$ORCA_SOURCE" \
|
||||
2>&1 | tee ../sweep.log
|
||||
|
||||
- name: Publish job summary
|
||||
if: always()
|
||||
run: |
|
||||
{
|
||||
echo "## Override sweep effect stage, shard ${{ matrix.shard }}/2"
|
||||
echo "Build ${{ needs.build.outputs.head_sha }} (run ${{ needs.build.outputs.run_id }})"
|
||||
echo '```'
|
||||
grep -E "\[override sweep" sweep.log || echo "no stage summaries, see the log"
|
||||
grep -E "^=+ .*(passed|failed)" sweep.log | tail -1 || true
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload the override report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: override-report-shard${{ matrix.shard }}
|
||||
path: |
|
||||
orca-test-repo/.pytest_cache/override_report.json
|
||||
sweep.log
|
||||
if-no-files-found: warn
|
||||
retention-days: 30
|
||||
|
||||
# The sweep step continues on error so the summary and report still get
|
||||
# published; this puts the failure back on the job
|
||||
- name: Fail the job if the sweep failed
|
||||
if: steps.run.outcome == 'failure'
|
||||
run: |
|
||||
echo "the override sweep failed, see the job summary and the uploaded report" >&2
|
||||
exit 1
|
||||
|
||||
harness:
|
||||
name: GUI-vs-CLI parity harness
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- *checkout-suite
|
||||
- *checkout-slicer
|
||||
- *extract-appimage
|
||||
|
||||
- name: Install display tooling and the AppImage's host runtime
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
xvfb xdotool imagemagick openbox mesa-utils \
|
||||
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0
|
||||
|
||||
- name: Run the parity harness
|
||||
run: |
|
||||
set -euo pipefail
|
||||
fixtures=()
|
||||
for f in ${{ inputs.fixtures || '' }}; do
|
||||
fixtures+=(--fixture "$f")
|
||||
done
|
||||
# 2 GUI displays: ~1.5 cores peak / ~1.9 GB on this 4-vCPU runner,
|
||||
# and each fixture is fully isolated, so results match a serial run
|
||||
python3 orca-test-repo/parity/run_parity.py \
|
||||
--slicer-root "$ORCA_SOURCE" --bin "$ORCA_BIN" \
|
||||
--cli-presets "${{ inputs.cli_presets || 'flat' }}" \
|
||||
--gui-workers 2 --out "$PWD/parity-out" "${fixtures[@]}"
|
||||
|
||||
- name: Publish job summary
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f parity-out/report.md ]; then
|
||||
cat parity-out/report.md >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "the harness produced no report, see the log" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
- name: Drop per-lane datadirs before upload
|
||||
if: always()
|
||||
run: rm -rf parity-out/*/seed parity-out/*/datadir-* || true
|
||||
|
||||
- name: Upload the scorecard and evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: parity-scorecard
|
||||
path: parity-out/
|
||||
if-no-files-found: warn
|
||||
retention-days: 30
|
||||
@@ -32,21 +32,13 @@ jobs:
|
||||
}
|
||||
|
||||
const allowedLabels = [
|
||||
// kind of change
|
||||
'crash',
|
||||
'bug-fix',
|
||||
'enhancement',
|
||||
'QoL',
|
||||
'optimization',
|
||||
// area
|
||||
'UI/UX',
|
||||
'profile',
|
||||
'Localization',
|
||||
// infrastructure
|
||||
'build',
|
||||
'test',
|
||||
'dependencies',
|
||||
'documentation'
|
||||
'profile',
|
||||
'QoL',
|
||||
'UI/UX',
|
||||
'dependencies'
|
||||
];
|
||||
const pr = context.payload.pull_request;
|
||||
const labelsList = `${allowedLabels
|
||||
@@ -190,21 +182,13 @@ jobs:
|
||||
}
|
||||
|
||||
const allowedLabels = [
|
||||
// kind of change
|
||||
'crash',
|
||||
'bug-fix',
|
||||
'enhancement',
|
||||
'QoL',
|
||||
'optimization',
|
||||
// area
|
||||
'UI/UX',
|
||||
'profile',
|
||||
'Localization',
|
||||
// infrastructure
|
||||
'build',
|
||||
'test',
|
||||
'dependencies',
|
||||
'documentation'
|
||||
'profile',
|
||||
'QoL',
|
||||
'UI/UX',
|
||||
'dependencies'
|
||||
];
|
||||
|
||||
const issue = context.payload.issue;
|
||||
|
||||
@@ -1,911 +0,0 @@
|
||||
name: PR Merge Bot
|
||||
|
||||
# Merges a pull request on request from a delegated vendor profile maintainer.
|
||||
# The merge is performed by this workflow's GITHUB_TOKEN, so a delegate needs no
|
||||
# repository access.
|
||||
#
|
||||
# Commands, posted as a comment on the PR:
|
||||
# /bot merge squash-merge the PR
|
||||
# /bot merge --dry-run report the verdict without merging
|
||||
#
|
||||
# Merges only when the commenter holds a grant covering every changed path, the
|
||||
# PR targets main or release/*, and CI is green on the head commit. Otherwise it
|
||||
# comments naming the files that fell outside the grant.
|
||||
#
|
||||
# When a PR touching resources/profiles/** is opened, two labels are applied
|
||||
# independently of the merge command:
|
||||
# profile every changed path is inside resources/profiles/
|
||||
# orca profile partner the PR author holds a grant covering every changed
|
||||
# path, plus a one-time comment explaining /bot merge
|
||||
# Neither label changes what the merge command checks.
|
||||
#
|
||||
# Grants come from the FOLDER_MERGERS variable in the `merge-delegation`
|
||||
# environment: one per line, `account: path`, `#` comments and blank lines
|
||||
# allowed. Paths may contain spaces. A vendor takes two grants, the folder and
|
||||
# its sibling bundle JSON:
|
||||
#
|
||||
# # Acme profiles
|
||||
# vendor-maintainer: resources/profiles/Acme/
|
||||
# vendor-maintainer: resources/profiles/Acme.json
|
||||
#
|
||||
# Edit the grant list (environment scope, so admin only):
|
||||
# gh variable set FOLDER_MERGERS --env merge-delegation --body "$(cat folder-mergers.txt)"
|
||||
# gh variable get FOLDER_MERGERS --env merge-delegation
|
||||
#
|
||||
# Stop all merging without touching this file:
|
||||
# gh variable set MERGE_BOT_DRY_RUN --body true
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types:
|
||||
- created
|
||||
# Labels profile PRs on open, without waiting for a /bot merge command.
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
paths:
|
||||
- 'resources/profiles/**'
|
||||
|
||||
# One merge attempt per PR at a time, so two quick comments cannot race.
|
||||
# Labels run under their own group, so a queued label run is not replaced by
|
||||
# a merge run for the same PR.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.issue.number || github.event.pull_request.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
merge:
|
||||
# Skips the job unless a PR comment mentions the command.
|
||||
if: >-
|
||||
github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||
&& github.event_name == 'issue_comment'
|
||||
&& github.event.issue.pull_request != null
|
||||
&& contains(github.event.comment.body, '/bot merge')
|
||||
permissions:
|
||||
contents: write # pulls.merge
|
||||
pull-requests: write # pulls.merge
|
||||
issues: write # feedback comment + reactions
|
||||
actions: write # re-dispatch build_all.yml after the merge
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
# Supplies FOLDER_MERGERS. Must carry no protection rules, or every
|
||||
# delegated merge and partner label run would wait for a human reviewer.
|
||||
environment: merge-delegation
|
||||
steps:
|
||||
- name: Merge PR on behalf of a folder delegate
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
# Read as env vars, never interpolated into the script body.
|
||||
FOLDER_MERGERS: ${{ vars.FOLDER_MERGERS }}
|
||||
MERGE_BOT_DRY_RUN: ${{ vars.MERGE_BOT_DRY_RUN }}
|
||||
with:
|
||||
script: |
|
||||
function isPermissionDenied(error) {
|
||||
return error && error.status === 403 && /Resource not accessible by integration/i.test(error.message || '');
|
||||
}
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const MARKER = '<!-- pr-merge-bot -->';
|
||||
// No grant may reach outside this root.
|
||||
const DELEGATABLE_ROOT = 'resources/profiles/';
|
||||
const ALLOWED_BASE_BRANCH = /^(?:main|release\/.+)$/;
|
||||
const MERGE_METHOD = 'squash';
|
||||
const REQUIRED_CHECK = 'Check profiles'; // job name in check_profiles.yml
|
||||
const LISTFILES_CAP = 3000;
|
||||
const MAX_REPORTED_FILES = 12;
|
||||
const MERGEABLE_ATTEMPTS = 5;
|
||||
const MERGEABLE_DELAY_MS = 2000;
|
||||
const OK_CONCLUSIONS = new Set(['success', 'neutral', 'skipped']);
|
||||
const REGULAR_FILE_MODES = new Set(['100644', '100755']);
|
||||
|
||||
// Paths refused whatever the grants say. Checked before grants, so
|
||||
// delegating a new root means removing it from this list too.
|
||||
const DENIED_PATTERNS = [
|
||||
/^\.github\//,
|
||||
/(^|\/)\.git(attributes|modules|ignore|config)$/,
|
||||
/^(?:src|deps|deps_src|tests|tools|cmake|sandboxes|scripts|docs?|localization|bbl)\//,
|
||||
/(^|\/)cmakelists\.txt$/,
|
||||
/\.cmake$/,
|
||||
/^build_[^/]*\.(?:sh|bat)$/,
|
||||
/^version\.inc$/,
|
||||
// Executables, including those inside the delegatable root.
|
||||
/\.(?:sh|bash|bat|cmd|ps1|py|js|mjs|cjs|ts|rb|pl|php)$/
|
||||
];
|
||||
|
||||
function parseGrants(raw) {
|
||||
// GitHub login: 1-39 chars, alphanumerics with single interior hyphens.
|
||||
const loginPattern = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
|
||||
const grantsByLogin = new Map();
|
||||
const problems = [];
|
||||
|
||||
(raw || '').split(/\r?\n/).forEach((rawLine, index) => {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith('#')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Splits on the first colon only, so paths may contain ':' and spaces.
|
||||
const separator = line.indexOf(':');
|
||||
if (separator === -1) {
|
||||
problems.push(`line ${index + 1}: expected \`account: path\``);
|
||||
return;
|
||||
}
|
||||
|
||||
const login = line.slice(0, separator).trim().replace(/^@/, '');
|
||||
const path = line.slice(separator + 1).trim().replace(/\/+$/, '');
|
||||
|
||||
if (!loginPattern.test(login)) {
|
||||
problems.push(`line ${index + 1}: \`${login}\` is not a valid GitHub account name`);
|
||||
return;
|
||||
}
|
||||
if (/[\\*?\u0000-\u001f\u007f]/.test(path) || path.split('/').includes('..') || path.includes('//')) {
|
||||
problems.push(`line ${index + 1}: invalid path (no globs, \`..\`, \`//\`, backslashes or control characters)`);
|
||||
return;
|
||||
}
|
||||
// Rejects anything outside the root, and the bare root itself.
|
||||
if (!path.startsWith(DELEGATABLE_ROOT) || path.length <= DELEGATABLE_ROOT.length) {
|
||||
problems.push(`line ${index + 1}: \`${path}\` is not inside \`${DELEGATABLE_ROOT}\``);
|
||||
return;
|
||||
}
|
||||
|
||||
const key = login.toLowerCase();
|
||||
grantsByLogin.set(key, (grantsByLogin.get(key) || []).concat(path));
|
||||
});
|
||||
|
||||
return { grantsByLogin, problems };
|
||||
}
|
||||
|
||||
function isDenied(path) {
|
||||
if (/[\\\u0000-\u001f\u007f]/.test(path) || path.startsWith('/') || path.split('/').includes('..')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const normalized = path.normalize('NFKC').toLowerCase();
|
||||
return DENIED_PATTERNS.some((pattern) => pattern.test(normalized));
|
||||
}
|
||||
|
||||
// Byte-exact match on directory boundaries, so a grant of
|
||||
// `.../Acme` covers neither `.../Acme Labs/x.json` nor `.../Acme.json`.
|
||||
function isGranted(path, grants) {
|
||||
return grants.some((grant) => path === grant || path.startsWith(`${grant}/`));
|
||||
}
|
||||
|
||||
// Both endpoints of a rename; both must satisfy the grant.
|
||||
function pathsFor(file) {
|
||||
return [file.filename, file.previous_filename].filter(Boolean);
|
||||
}
|
||||
|
||||
function formatList(items) {
|
||||
const unique = [...new Set(items)];
|
||||
const shown = unique.slice(0, MAX_REPORTED_FILES).map((item) => `- \`${item}\``);
|
||||
if (unique.length > MAX_REPORTED_FILES) {
|
||||
shown.push(`- …and ${unique.length - MAX_REPORTED_FILES} more`);
|
||||
}
|
||||
return shown.join('\n');
|
||||
}
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const issue = context.payload.issue;
|
||||
const comment = context.payload.comment;
|
||||
|
||||
if (!issue.pull_request) {
|
||||
core.info('Ignoring comment that is not on a pull request.');
|
||||
return;
|
||||
}
|
||||
// Ignores a comment whose sender is not its author.
|
||||
if (context.payload.action !== 'created' || context.payload.sender.login !== comment.user.login) {
|
||||
core.warning('Ignoring comment whose sender does not match its author.');
|
||||
return;
|
||||
}
|
||||
if (comment.user.type !== 'User') {
|
||||
core.info('Ignoring bot-authored command.');
|
||||
return;
|
||||
}
|
||||
|
||||
const commandLine = (comment.body || '')
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.find((line) => /^\/bot\s+merge\b/i.test(line));
|
||||
|
||||
if (!commandLine) {
|
||||
core.info('No /bot merge command found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const commenter = comment.user.login;
|
||||
const { grantsByLogin, problems } = parseGrants(process.env.FOLDER_MERGERS);
|
||||
const grants = grantsByLogin.get(commenter.toLowerCase()) || [];
|
||||
|
||||
for (const problem of problems) {
|
||||
core.warning(`FOLDER_MERGERS ${problem}`);
|
||||
}
|
||||
|
||||
// Says nothing to accounts with no grant, so it cannot be used to spam.
|
||||
if (!grants.length) {
|
||||
core.info(`Ignoring /bot merge from @${commenter}: not listed in FOLDER_MERGERS.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Warns instead of failing when the token cannot post feedback.
|
||||
async function bestEffort(call, warning) {
|
||||
try {
|
||||
await call();
|
||||
} catch (error) {
|
||||
if (isPermissionDenied(error)) {
|
||||
core.warning(warning);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const react = (content) => bestEffort(
|
||||
() => github.rest.reactions.createForIssueComment({ owner, repo, comment_id: comment.id, content }),
|
||||
`Cannot add the "${content}" reaction because the token cannot write.`);
|
||||
|
||||
const say = (body) => bestEffort(
|
||||
() => github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body: `${MARKER}\n${body}` }),
|
||||
'Cannot post a comment because the token cannot write comments.');
|
||||
|
||||
// Declines the command: warns in the log, reacts, explains on the PR.
|
||||
async function refuse(reason) {
|
||||
const configNote = problems.length
|
||||
? `\n\n\`FOLDER_MERGERS\` also has problems a maintainer needs to fix:\n${problems.map((problem) => `- ${problem}`).join('\n')}`
|
||||
: '';
|
||||
const grantsNote = `\n\n<details><summary>Your current grants</summary>\n\n${formatList(grants)}\n\n</details>`;
|
||||
|
||||
core.warning(`Refused /bot merge from @${commenter}: ${reason}`);
|
||||
await react('-1');
|
||||
await say(`@${commenter} I can't merge this PR: ${reason}${configNote}${grantsNote}`);
|
||||
}
|
||||
|
||||
await react('eyes');
|
||||
|
||||
const args = (commandLine.match(/^\/bot\s+merge\s*(.*)$/i)[1] || '').trim().split(/\s+/).filter(Boolean);
|
||||
const unknownArgs = args.filter((arg) => arg.toLowerCase() !== '--dry-run');
|
||||
const dryRun = String(process.env.MERGE_BOT_DRY_RUN || '').toLowerCase() === 'true'
|
||||
|| unknownArgs.length !== args.length;
|
||||
|
||||
if (unknownArgs.length) {
|
||||
return refuse(
|
||||
`I don't understand ${unknownArgs.map((arg) => `\`${arg}\``).join(', ')}. ` +
|
||||
'Usage: `/bot merge` or `/bot merge --dry-run`.'
|
||||
);
|
||||
}
|
||||
|
||||
// Refuses everything while the grant list is malformed.
|
||||
if (problems.length) {
|
||||
return refuse(
|
||||
'the `FOLDER_MERGERS` grant list has malformed lines, so I refuse every merge until it is fixed.'
|
||||
);
|
||||
}
|
||||
|
||||
let { data: pr } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: issue.number
|
||||
});
|
||||
|
||||
if (pr.merged) {
|
||||
return refuse('it is already merged.');
|
||||
}
|
||||
if (pr.state !== 'open') {
|
||||
return refuse(`its state is \`${pr.state}\`, not \`open\`.`);
|
||||
}
|
||||
if (pr.draft) {
|
||||
return refuse('it is still a draft. Mark it ready for review first.');
|
||||
}
|
||||
if (!ALLOWED_BASE_BRANCH.test(pr.base.ref)) {
|
||||
return refuse(`it targets \`${pr.base.ref}\`. Delegated merges are only allowed into \`main\` and \`release/*\`.`);
|
||||
}
|
||||
|
||||
// ---- folder scope ----
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number,
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
if (!files.length) {
|
||||
return refuse('it changes no files, so there is nothing to verify or merge.');
|
||||
}
|
||||
// Refuses when the file list is truncated or disagrees with the PR.
|
||||
if (files.length >= LISTFILES_CAP || files.length !== pr.changed_files) {
|
||||
return refuse(
|
||||
`it reports ${pr.changed_files} changed files but the API listed ${files.length}, ` +
|
||||
'so the file list is truncated and I cannot verify the folder scope. A maintainer must merge this one.'
|
||||
);
|
||||
}
|
||||
|
||||
const deniedFiles = [];
|
||||
const outsideFiles = [];
|
||||
|
||||
for (const file of files) {
|
||||
for (const path of pathsFor(file)) {
|
||||
if (isDenied(path)) {
|
||||
deniedFiles.push(path);
|
||||
} else if (!isGranted(path, grants)) {
|
||||
outsideFiles.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deniedFiles.length) {
|
||||
core.error(`@${commenter} attempted a delegated merge touching protected paths: ${deniedFiles.join(', ')}`);
|
||||
return refuse(
|
||||
'it touches paths that are never delegatable, whatever the grants say ' +
|
||||
`(CI, build, scripts or executable files):\n\n${formatList(deniedFiles)}\n\nA maintainer should look at this before it goes any further.`
|
||||
);
|
||||
}
|
||||
if (outsideFiles.length) {
|
||||
return refuse(
|
||||
`${outsideFiles.length} changed path(s) fall outside your grants:\n\n${formatList(outsideFiles)}\n\n` +
|
||||
'A vendor needs both grants: `resources/profiles/<Vendor>/` **and** `resources/profiles/<Vendor>.json`.'
|
||||
);
|
||||
}
|
||||
|
||||
// ---- file modes: rejects symlinks and submodules ----
|
||||
// Fetches the delegatable subtree only; listFiles does not report modes.
|
||||
const headSha = pr.head.sha;
|
||||
const { data: tree } = await github.rest.git.getTree({
|
||||
owner,
|
||||
repo,
|
||||
tree_sha: `${headSha}:${DELEGATABLE_ROOT.replace(/\/$/, '')}`,
|
||||
recursive: 'true'
|
||||
});
|
||||
|
||||
if (tree.truncated) {
|
||||
return refuse('the git tree is too large to verify file modes. A maintainer must merge this one.');
|
||||
}
|
||||
|
||||
// Entry paths are subtree-relative.
|
||||
const modesByPath = new Map(tree.tree.map((entry) => [`${DELEGATABLE_ROOT}${entry.path}`, entry.mode]));
|
||||
const irregularFiles = files
|
||||
.filter((file) => file.status !== 'removed')
|
||||
.map((file) => [file.filename, modesByPath.get(file.filename)])
|
||||
.filter(([, mode]) => !REGULAR_FILE_MODES.has(mode))
|
||||
.map(([path, mode]) => `${path} (mode ${mode || 'missing'})`);
|
||||
|
||||
if (irregularFiles.length) {
|
||||
core.error(`@${commenter} attempted a delegated merge with non-regular files: ${irregularFiles.join(', ')}`);
|
||||
return refuse(
|
||||
`it adds symlinks, submodules or files I cannot verify:\n\n${formatList(irregularFiles)}\n\nA maintainer should look at this before it goes any further.`
|
||||
);
|
||||
}
|
||||
|
||||
// ---- mergeability: waits for GitHub to compute it ----
|
||||
for (let attempt = 0; pr.mergeable === null && attempt < MERGEABLE_ATTEMPTS; attempt += 1) {
|
||||
core.info(`Mergeability not computed yet; retrying in ${MERGEABLE_DELAY_MS}ms.`);
|
||||
await sleep(MERGEABLE_DELAY_MS);
|
||||
({ data: pr } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number
|
||||
}));
|
||||
}
|
||||
|
||||
if (pr.mergeable === null) {
|
||||
return refuse('GitHub is still working out whether it can be merged. Try `/bot merge` again in a minute.');
|
||||
}
|
||||
if (!pr.mergeable) {
|
||||
return refuse(`it is not mergeable (\`${pr.mergeable_state}\`) - most likely a conflict with \`${pr.base.ref}\`.`);
|
||||
}
|
||||
|
||||
// ---- CI on the head commit ----
|
||||
const checkRuns = await github.paginate(github.rest.checks.listForRef, {
|
||||
owner,
|
||||
repo,
|
||||
ref: headSha,
|
||||
filter: 'latest',
|
||||
per_page: 100
|
||||
});
|
||||
const pendingChecks = checkRuns.filter((run) => run.status !== 'completed');
|
||||
const failedChecks = checkRuns.filter((run) => run.status === 'completed' && !OK_CONCLUSIONS.has(run.conclusion));
|
||||
|
||||
if (pendingChecks.length) {
|
||||
return refuse(
|
||||
`${pendingChecks.length} check(s) are still running on \`${headSha.slice(0, 7)}\`:\n\n` +
|
||||
`${formatList(pendingChecks.map((run) => run.name))}\n\nRe-run \`/bot merge\` once they finish.`
|
||||
);
|
||||
}
|
||||
if (failedChecks.length) {
|
||||
return refuse(
|
||||
`${failedChecks.length} check(s) are not green on \`${headSha.slice(0, 7)}\`:\n\n` +
|
||||
formatList(failedChecks.map((run) => `${run.name} (${run.conclusion})`))
|
||||
);
|
||||
}
|
||||
|
||||
const { data: combined } = await github.rest.repos.getCombinedStatusForRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: headSha
|
||||
});
|
||||
// total_count 0 only means there are no legacy statuses.
|
||||
if (combined.total_count > 0 && combined.state !== 'success') {
|
||||
return refuse(
|
||||
`the combined commit status on \`${headSha.slice(0, 7)}\` is \`${combined.state}\`:\n\n` +
|
||||
formatList(combined.statuses.filter((status) => status.state !== 'success')
|
||||
.map((status) => `${status.context} (${status.state})`))
|
||||
);
|
||||
}
|
||||
|
||||
// Requires the check to have actually run, not merely to have not failed.
|
||||
const requiredCheck = checkRuns.find((run) =>
|
||||
run.name === REQUIRED_CHECK &&
|
||||
run.app && run.app.slug === 'github-actions' &&
|
||||
run.status === 'completed' && OK_CONCLUSIONS.has(run.conclusion));
|
||||
|
||||
if (!requiredCheck) {
|
||||
return refuse(
|
||||
`the \`${REQUIRED_CHECK}\` check has not succeeded on \`${headSha.slice(0, 7)}\`. ` +
|
||||
'If it never ran, a maintainer needs to approve the workflow run first.'
|
||||
);
|
||||
}
|
||||
|
||||
const scopeSummary = `${files.length} file(s), all within:\n${formatList(grants)}`;
|
||||
|
||||
if (dryRun) {
|
||||
core.info('Dry run: every gate passed, not merging.');
|
||||
await react('+1');
|
||||
await say(
|
||||
`@${commenter} **dry run** - this PR passes every gate and I *would* squash-merge it ` +
|
||||
`at \`${headSha.slice(0, 7)}\`.\n\nVerified scope: ${scopeSummary}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- re-validate, then merge ----
|
||||
// An unchanged head SHA means the verified file list still holds.
|
||||
const { data: fresh } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number
|
||||
});
|
||||
|
||||
if (fresh.head.sha !== headSha || fresh.base.ref !== pr.base.ref || fresh.state !== 'open' || fresh.merged || fresh.draft) {
|
||||
return refuse('it changed while I was checking it. Nothing was merged - re-run `/bot merge`.');
|
||||
}
|
||||
|
||||
let merged;
|
||||
try {
|
||||
// Pinned to the verified head: a moved head fails with 409.
|
||||
({ data: merged } = await github.rest.pulls.merge({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number,
|
||||
sha: headSha,
|
||||
merge_method: MERGE_METHOD,
|
||||
commit_title: `${pr.title} (#${pr.number})`,
|
||||
commit_message:
|
||||
`Merged by /bot merge on behalf of @${commenter} (id ${comment.user.id}).\n` +
|
||||
`Grants: ${grants.join(', ')}\nHead: ${headSha}\n`
|
||||
}));
|
||||
} catch (error) {
|
||||
const hint = {
|
||||
403: 'the workflow token cannot write to the repository.',
|
||||
405: 'GitHub refused the merge - branch protection, a required review or check, a newly added CODEOWNERS file, or squash merging being disabled.',
|
||||
409: `the head commit moved after I verified it (was \`${headSha.slice(0, 7)}\`).`,
|
||||
422: 'GitHub rejected the merge as invalid.'
|
||||
}[error.status];
|
||||
|
||||
if (!hint) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await refuse(`${hint}\n\n> ${error.message}\n\nNothing was merged.`);
|
||||
core.setFailed(`Delegated merge failed: ${error.status} ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
core.info(`Merged #${pr.number} as ${merged.sha}.`);
|
||||
await react('rocket');
|
||||
await say(
|
||||
`@${commenter} squash-merged into \`${pr.base.ref}\` as ${merged.sha}.\n\nVerified scope: ${scopeSummary}`
|
||||
);
|
||||
|
||||
// ---- re-kick the build ----
|
||||
// A GITHUB_TOKEN merge fires no push event, so build_all.yml would
|
||||
// otherwise never see these files.
|
||||
try {
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner,
|
||||
repo,
|
||||
workflow_id: 'build_all.yml',
|
||||
ref: pr.base.ref
|
||||
});
|
||||
core.info(`Dispatched build_all.yml on ${pr.base.ref}.`);
|
||||
} catch (error) {
|
||||
core.warning(`Merged successfully, but dispatching build_all.yml failed: ${error.message}`);
|
||||
}
|
||||
|
||||
label-profile:
|
||||
# Independent of the merge rules: any PR that changes only files inside
|
||||
# resources/profiles/ is labeled `profile`.
|
||||
if: >-
|
||||
github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||
&& github.event_name == 'pull_request_target'
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: write
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Label profile-only PRs
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
function isPermissionDenied(error) {
|
||||
return error && error.status === 403 && /Resource not accessible by integration/i.test(error.message || '');
|
||||
}
|
||||
|
||||
const PROFILE_ROOT = 'resources/profiles/';
|
||||
const LABEL = 'profile';
|
||||
const LISTFILES_CAP = 3000;
|
||||
const ATTEMPTS = 3;
|
||||
|
||||
function profileOnlyProblem(pr, files) {
|
||||
if (!files.length) {
|
||||
return 'PR changes no files; not labeling.';
|
||||
}
|
||||
// A truncated list, or a count that disagrees with the PR, cannot
|
||||
// prove "only profile files".
|
||||
if (files.length >= LISTFILES_CAP || files.length !== pr.changed_files) {
|
||||
return `PR reports ${pr.changed_files} changed files but the API listed ${files.length}; not labeling.`;
|
||||
}
|
||||
|
||||
// Both endpoints of a rename count, so a move out of the profile
|
||||
// root is not mistaken for a profile-only change.
|
||||
const paths = files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean));
|
||||
const outside = paths.filter((path) => !path.startsWith(PROFILE_ROOT));
|
||||
|
||||
if (outside.length) {
|
||||
return `${outside.length} changed path(s) fall outside ${PROFILE_ROOT}; not labeling.`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const number = context.payload.pull_request.number;
|
||||
|
||||
// The event payload is frozen at `opened`; listFiles is not. Read
|
||||
// fresh PR metadata and retry if either side of the diff changes.
|
||||
for (let attempt = 0; attempt < ATTEMPTS; attempt += 1) {
|
||||
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: number });
|
||||
|
||||
if (pr.state !== 'open') {
|
||||
core.info(`PR is ${pr.state}; not labeling.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number,
|
||||
per_page: 100
|
||||
});
|
||||
const problem = profileOnlyProblem(pr, files);
|
||||
|
||||
const { data: after } = await github.rest.pulls.get({ owner, repo, pull_number: number });
|
||||
if (
|
||||
after.state !== 'open' ||
|
||||
after.head.sha !== pr.head.sha ||
|
||||
after.base.ref !== pr.base.ref ||
|
||||
after.base.sha !== pr.base.sha
|
||||
) {
|
||||
core.info('PR changed while listing files; retrying.');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (problem) {
|
||||
core.info(problem);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
labels: [LABEL]
|
||||
});
|
||||
core.info(`Applied the "${LABEL}" label.`);
|
||||
} catch (error) {
|
||||
if (isPermissionDenied(error)) {
|
||||
core.warning(`Cannot add the "${LABEL}" label because the token cannot write.`);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
core.warning('PR kept changing during verification; not labeling.');
|
||||
|
||||
label-profile-partner:
|
||||
# Labels a profile PR whose author holds a grant covering every changed
|
||||
# path, and explains the /bot merge command to them once.
|
||||
if: >-
|
||||
github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||
&& github.event_name == 'pull_request_target'
|
||||
permissions:
|
||||
contents: read # delegatable subtree, for file modes
|
||||
pull-requests: read
|
||||
issues: write # label + comment
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
# Supplies FOLDER_MERGERS. Must carry no protection rules, or every
|
||||
# qualifying PR open would wait for a human reviewer.
|
||||
environment: merge-delegation
|
||||
steps:
|
||||
- name: Label profile PRs from delegated maintainers
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
# Read as an env var, never interpolated into the script body.
|
||||
FOLDER_MERGERS: ${{ vars.FOLDER_MERGERS }}
|
||||
with:
|
||||
script: |
|
||||
function isPermissionDenied(error) {
|
||||
return error && error.status === 403 && /Resource not accessible by integration/i.test(error.message || '');
|
||||
}
|
||||
|
||||
// Never prints the grant list: this job posts public comments and
|
||||
// its logs are public too.
|
||||
async function bestEffort(call, warning) {
|
||||
try {
|
||||
await call();
|
||||
} catch (error) {
|
||||
if (isPermissionDenied(error)) {
|
||||
core.warning(warning);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const MARKER = '<!-- profile-partner-bot -->';
|
||||
const LABEL = 'orca profile partner';
|
||||
const ATTEMPTS = 3;
|
||||
|
||||
// ---- scope rules, mirrored from the merge job above ----
|
||||
// Change both together: these decide whether a delegate could merge.
|
||||
const DELEGATABLE_ROOT = 'resources/profiles/';
|
||||
const ALLOWED_BASE_BRANCH = /^(?:main|release\/.+)$/;
|
||||
const LISTFILES_CAP = 3000;
|
||||
const REGULAR_FILE_MODES = new Set(['100644', '100755']);
|
||||
|
||||
const DENIED_PATTERNS = [
|
||||
/^\.github\//,
|
||||
/(^|\/)\.git(attributes|modules|ignore|config)$/,
|
||||
/^(?:src|deps|deps_src|tests|tools|cmake|sandboxes|scripts|docs?|localization|bbl)\//,
|
||||
/(^|\/)cmakelists\.txt$/,
|
||||
/\.cmake$/,
|
||||
/^build_[^/]*\.(?:sh|bat)$/,
|
||||
/^version\.inc$/,
|
||||
// Executables, including those inside the delegatable root.
|
||||
/\.(?:sh|bash|bat|cmd|ps1|py|js|mjs|cjs|ts|rb|pl|php)$/
|
||||
];
|
||||
|
||||
function parseGrants(raw) {
|
||||
// GitHub login: 1-39 chars, alphanumerics with single interior hyphens.
|
||||
const loginPattern = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
|
||||
const grantsByLogin = new Map();
|
||||
const problems = [];
|
||||
|
||||
(raw || '').split(/\r?\n/).forEach((rawLine, index) => {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith('#')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Splits on the first colon only, so paths may contain ':' and spaces.
|
||||
const separator = line.indexOf(':');
|
||||
if (separator === -1) {
|
||||
problems.push(`line ${index + 1}: expected \`account: path\``);
|
||||
return;
|
||||
}
|
||||
|
||||
const login = line.slice(0, separator).trim().replace(/^@/, '');
|
||||
const path = line.slice(separator + 1).trim().replace(/\/+$/, '');
|
||||
|
||||
if (!loginPattern.test(login)) {
|
||||
problems.push(`line ${index + 1}: \`${login}\` is not a valid GitHub account name`);
|
||||
return;
|
||||
}
|
||||
if (/[\\*?\u0000-\u001f\u007f]/.test(path) || path.split('/').includes('..') || path.includes('//')) {
|
||||
problems.push(`line ${index + 1}: invalid path (no globs, \`..\`, \`//\`, backslashes or control characters)`);
|
||||
return;
|
||||
}
|
||||
// Rejects anything outside the root, and the bare root itself.
|
||||
if (!path.startsWith(DELEGATABLE_ROOT) || path.length <= DELEGATABLE_ROOT.length) {
|
||||
problems.push(`line ${index + 1}: \`${path}\` is not inside \`${DELEGATABLE_ROOT}\``);
|
||||
return;
|
||||
}
|
||||
|
||||
const key = login.toLowerCase();
|
||||
grantsByLogin.set(key, (grantsByLogin.get(key) || []).concat(path));
|
||||
});
|
||||
|
||||
return { grantsByLogin, problems };
|
||||
}
|
||||
|
||||
function isDenied(path) {
|
||||
if (/[\\\u0000-\u001f\u007f]/.test(path) || path.startsWith('/') || path.split('/').includes('..')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const normalized = path.normalize('NFKC').toLowerCase();
|
||||
return DENIED_PATTERNS.some((pattern) => pattern.test(normalized));
|
||||
}
|
||||
|
||||
// Byte-exact match on directory boundaries, so a grant of
|
||||
// `.../Acme` covers neither `.../Acme Labs/x.json` nor `.../Acme.json`.
|
||||
function isGranted(path, grants) {
|
||||
return grants.some((grant) => path === grant || path.startsWith(`${grant}/`));
|
||||
}
|
||||
|
||||
// Both endpoints of a rename; both must satisfy the grant.
|
||||
function pathsFor(file) {
|
||||
return [file.filename, file.previous_filename].filter(Boolean);
|
||||
}
|
||||
// ---- end mirrored rules ----
|
||||
|
||||
function scopeProblem(pr, files, grants) {
|
||||
if (!files.length) {
|
||||
return 'PR changes no files; not labeling.';
|
||||
}
|
||||
if (files.length >= LISTFILES_CAP || files.length !== pr.changed_files) {
|
||||
return `PR reports ${pr.changed_files} changed files but the API listed ${files.length}; not labeling.`;
|
||||
}
|
||||
|
||||
let outsideCount = 0;
|
||||
for (const file of files) {
|
||||
for (const path of pathsFor(file)) {
|
||||
if (isDenied(path) || !isGranted(path, grants)) {
|
||||
outsideCount += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (outsideCount) {
|
||||
return `PR has ${outsideCount} path(s) outside @${author}'s grants; not labeling.`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---- file modes: rejects symlinks and submodules ----
|
||||
function modeProblem(files, tree) {
|
||||
if (tree.truncated) {
|
||||
return 'The profile tree is too large to verify file modes; not labeling.';
|
||||
}
|
||||
|
||||
const modesByPath = new Map(tree.tree.map((entry) => [`${DELEGATABLE_ROOT}${entry.path}`, entry.mode]));
|
||||
const hasIrregularFile = files.some((file) =>
|
||||
file.status !== 'removed' && !REGULAR_FILE_MODES.has(modesByPath.get(file.filename)));
|
||||
|
||||
if (hasIrregularFile) {
|
||||
return 'PR adds symlinks, submodules or files whose modes cannot be verified; not labeling.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const number = context.payload.pull_request.number;
|
||||
const author = context.payload.pull_request.user.login;
|
||||
|
||||
const { grantsByLogin, problems } = parseGrants(process.env.FOLDER_MERGERS);
|
||||
|
||||
// Only the count: the malformed lines may name grant holders.
|
||||
if (problems.length) {
|
||||
core.warning(`FOLDER_MERGERS has ${problems.length} malformed line(s); not labeling.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const grants = grantsByLogin.get(author.toLowerCase()) || [];
|
||||
// Says nothing to accounts with no grant, so it cannot be used to spam.
|
||||
if (!grants.length) {
|
||||
core.info(`Ignoring PR from @${author}: not listed in FOLDER_MERGERS.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Read current PR metadata for the file list and head tree. Retry
|
||||
// if either side of the diff changes during verification.
|
||||
for (let attempt = 0; attempt < ATTEMPTS; attempt += 1) {
|
||||
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: number });
|
||||
|
||||
if (pr.state !== 'open') {
|
||||
core.info(`PR is ${pr.state}; not labeling.`);
|
||||
return;
|
||||
}
|
||||
if (!ALLOWED_BASE_BRANCH.test(pr.base.ref)) {
|
||||
core.info(`PR targets "${pr.base.ref}", not main or release/*; not labeling.`);
|
||||
return;
|
||||
}
|
||||
// Checked before listing files, so a PR too large to list is
|
||||
// rejected in one call.
|
||||
if (pr.changed_files >= LISTFILES_CAP) {
|
||||
core.info(`PR changes ${pr.changed_files} files, more than the API can list; not labeling.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number,
|
||||
per_page: 100
|
||||
});
|
||||
const scopeIssue = scopeProblem(pr, files, grants);
|
||||
|
||||
let modeIssue = null;
|
||||
if (!scopeIssue) {
|
||||
const { data: tree } = await github.rest.git.getTree({
|
||||
owner,
|
||||
repo,
|
||||
tree_sha: `${pr.head.sha}:${DELEGATABLE_ROOT.replace(/\/$/, '')}`,
|
||||
recursive: 'true'
|
||||
});
|
||||
modeIssue = modeProblem(files, tree);
|
||||
}
|
||||
|
||||
const { data: after } = await github.rest.pulls.get({ owner, repo, pull_number: number });
|
||||
if (
|
||||
after.state !== 'open' ||
|
||||
after.head.sha !== pr.head.sha ||
|
||||
after.base.ref !== pr.base.ref ||
|
||||
after.base.sha !== pr.base.sha
|
||||
) {
|
||||
core.info('PR changed while verifying; retrying.');
|
||||
continue;
|
||||
}
|
||||
|
||||
const problem = scopeIssue || modeIssue;
|
||||
if (problem) {
|
||||
core.info(problem);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- label + one-time comment ----
|
||||
await bestEffort(
|
||||
() => github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: [LABEL] }),
|
||||
`Cannot add the "${LABEL}" label because the token cannot write.`);
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
per_page: 100
|
||||
});
|
||||
if (comments.some((comment) => (comment.body || '').includes(MARKER))) {
|
||||
core.info('Partner notice already present; skipping comment.');
|
||||
return;
|
||||
}
|
||||
|
||||
await bestEffort(
|
||||
() => github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
body:
|
||||
`${MARKER}\n` +
|
||||
`Hi @${author}, this profile PR is covered by your delegated merge grant.\n\n` +
|
||||
`Once it is ready for review and CI is green, you can merge it yourself:\n\n` +
|
||||
`- \`/bot merge\` - squash-merge into \`main\` or \`release/*\`\n` +
|
||||
`- \`/bot merge --dry-run\` - report the verdict without merging\n\n` +
|
||||
`The bot re-checks the scope, the file modes and the \`Check profiles\` check at merge time.`
|
||||
}),
|
||||
'Cannot post the partner notice because the token cannot write comments.');
|
||||
|
||||
core.info(`Applied the "${LABEL}" label and posted the /bot merge notice.`);
|
||||
return;
|
||||
}
|
||||
|
||||
core.warning('PR kept changing during verification; not labeling.');
|
||||
@@ -54,10 +54,8 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
tar -xvf build_tests.tar
|
||||
# Every platform builds with a multi-config generator (build_linux.sh uses Ninja
|
||||
# Multi-Config), so ctest needs the config: without it, plain add_test() tests
|
||||
# lose their labels and report "Not Run".
|
||||
scripts/run_unit_tests.sh "${{ inputs.test-dir }}" Release
|
||||
# Multi-config generators (Windows/macOS) need a config; Linux is single-config.
|
||||
scripts/run_unit_tests.sh "${{ inputs.test-dir }}" "${{ runner.os != 'Linux' && 'Release' || '' }}"
|
||||
- name: Upload Test Logs
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
name: Flatpak Unit Tests
|
||||
|
||||
# Run the flatpak build's test asset inside the sandbox, once per arch. The
|
||||
# GNOME SDK's _GLIBCXX_ASSERTIONS gives a bounds-checked STL that catches
|
||||
# out-of-bounds reads no other test leg does.
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
os:
|
||||
required: true
|
||||
type: string
|
||||
artifact:
|
||||
description: Test asset uploaded by the flatpak build leg
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
unit_tests_flatpak:
|
||||
name: Flatpak Unit Tests
|
||||
runs-on: ${{ inputs.os }}
|
||||
container:
|
||||
image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-50
|
||||
options: --privileged
|
||||
steps:
|
||||
- name: Restore test asset
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: ${{ inputs.artifact }}
|
||||
- name: Run unit tests (bounds-checked sandbox)
|
||||
timeout-minutes: 20
|
||||
shell: bash
|
||||
run: |
|
||||
tar -xf flatpak-test-asset.tar
|
||||
# Recreate the stable module symlink so /run/build/OrcaSlicer resolves.
|
||||
d=$(ls -d .flatpak-builder/build/OrcaSlicer-* | tail -1)
|
||||
ln -sfn "$(basename "$d")" .flatpak-builder/build/OrcaSlicer
|
||||
# The runtime + SDK + the llvm extension the app metadata references,
|
||||
# which `flatpak build` mounts; best-effort, the image may have them.
|
||||
flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
flatpak install --user -y --noninteractive flathub \
|
||||
org.gnome.Platform//50 org.gnome.Sdk//50 org.freedesktop.Sdk.Extension.llvm21//25.08 || true
|
||||
# `flatpak build` uses bwrap (no rofiles-fuse, which this container
|
||||
# rejects); bind-mount the build tree so the baked TEST_DATA_DIR resolves.
|
||||
flatpak build --die-with-parent --share=network \
|
||||
--bind-mount=/run/build="$PWD/.flatpak-builder/build" \
|
||||
flatpak_app \
|
||||
bash -c 'cd /run/build/OrcaSlicer && scripts/run_unit_tests.sh build_flatpak/tests'
|
||||
- name: Collect test results
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
d=$(ls -d .flatpak-builder/build/OrcaSlicer-* 2>/dev/null | tail -1 || true)
|
||||
[ -n "$d" ] && [ -f "$d/ctest_results.xml" ] && cp "$d/ctest_results.xml" ctest_results.xml || true
|
||||
- name: Upload Test Results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: test-results-${{ inputs.artifact }}
|
||||
path: ctest_results.xml
|
||||
retention-days: 5
|
||||
if-no-files-found: warn
|
||||
- name: Delete Test Asset
|
||||
if: success()
|
||||
uses: geekyeggo/delete-artifact@v6
|
||||
with:
|
||||
name: ${{ inputs.artifact }}
|
||||
failOnError: false
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
Build
|
||||
Build.bat
|
||||
/build*/
|
||||
/out/
|
||||
CMakeLists.txt.user
|
||||
CMakeUserPresets.json
|
||||
**/CMakeLists.txt.autosave
|
||||
deps/build*
|
||||
MYMETA.json
|
||||
@@ -51,6 +49,3 @@ internal_docs/
|
||||
# Python bytecode
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.opc
|
||||
/.test/
|
||||
docs/superpowers/
|
||||
@@ -20,18 +20,11 @@ cmake --build . --config %build_type% --target ALL_BUILD -- -m
|
||||
Catch2 framework. Tests in `tests/`; see [tests/AGENTS.md](tests/AGENTS.md) for where a new test belongs and the conventions to follow.
|
||||
|
||||
```bash
|
||||
cd build && ctest -C Release --output-on-failure # all tests
|
||||
ctest --test-dir ./tests/libslic3r -C Release # individual suite
|
||||
ctest --test-dir ./tests/fff_print -C Release
|
||||
cd build && ctest --output-on-failure # all tests
|
||||
ctest --test-dir ./tests/libslic3r # individual suite
|
||||
ctest --test-dir ./tests/fff_print
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- Docs live in `docs/`; the high-level design of a subsystem goes in `docs/HLSD/<subsystem>.md`.
|
||||
- Describe the design as it stands — what the subsystem does, why it exists, and the constraints that shape it. Not the route that got there: no phases, task lists, status markers, or "before/after this PR" framing.
|
||||
- Planning and investigation output (brainstorms, superpowers design and plan docs) stays in `docs/superpowers/`, which is gitignored. Never commit it.
|
||||
- Write a doc only when the design is not evident from the code, and when a change invalidates an existing one, update it in the same PR.
|
||||
|
||||
## Code Style
|
||||
|
||||
- C++17, selective C++20. PascalCase classes, snake_case functions/variables
|
||||
@@ -63,17 +56,16 @@ ctest --test-dir ./tests/fff_print -C Release
|
||||
- Add helper functions or utilities only when existing code cannot reasonably be reused. Avoid duplication.
|
||||
- Keep code concise and clear. Manually simplify AI generated bloated codes before review.
|
||||
- Include targeted tests or documented verification for behavior changes, especially in slicing logic, profiles, formats, and GUI defaults.
|
||||
- For profile changes (`resources/profiles/<Vendor>/**`), check that `version` in the sibling `resources/profiles/<Vendor>.json` was bumped.
|
||||
- For translation changes (`localization/i18n/**/*.po`), check that recurring terms match the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md) for that language.
|
||||
- For translation changes (`localization/i18n/**/*.po`), check that recurring terms match the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/guides/localization_glossary.md) for that language.
|
||||
|
||||
## Localization & translations
|
||||
|
||||
Catalogs live in `localization/i18n/<lang>/OrcaSlicer_<lang>.po`; the template is `OrcaSlicer.pot`.
|
||||
See the [Localization guide](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_guide.md) for the human-facing version of these principles.
|
||||
See the [Localization guide](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/guides/localization_guide.md) for the human-facing version of these principles.
|
||||
|
||||
### Terminology
|
||||
|
||||
- Use the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md) as the source of truth for recurring terms, so the same English term is always rendered the same way within a language, and terms that must stay in English (brand/product names, acronyms, materials, file formats, G-code tokens, macros/variables/identifiers) are not translated.
|
||||
- Use the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/guides/localization_glossary.md) as the source of truth for recurring terms, so the same English term is always rendered the same way within a language, and terms that must stay in English (brand/product names, acronyms, materials, file formats, G-code tokens, macros/variables/identifiers) are not translated.
|
||||
- If a term's established translation changes, update both the affected `.po` files and the glossary (`localization_glossary.tsv`, then regenerate) so they stay in sync.
|
||||
- Translate the *meaning*, not the words. Check what the string actually controls before translating it — English reuses one word for different things. `Flow ratio` (multiplier), `Flow Rate` (throughput) and `Flow Dynamics` (pressure compensation) are three different terms; `extruder` may mean the toolhead, the feeder motor, or the nozzle depending on the string.
|
||||
- Reuse one template per recurring message shape (`Failed to connect to …`, `Are you sure you want to …?`), even where the English wording varies.
|
||||
|
||||
@@ -4,10 +4,6 @@ endif()
|
||||
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
if(POLICY CMP0177)
|
||||
cmake_policy(SET CMP0177 NEW)
|
||||
endif()
|
||||
|
||||
|
||||
# The following line used to be in tests/CMakeLists.txt
|
||||
# Having it there causes rebuilds of all targets on any CMakeLists.txt change under tests/
|
||||
@@ -63,13 +59,6 @@ if (APPLE)
|
||||
message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
|
||||
endif ()
|
||||
|
||||
# Keep MSVC's default /W3 out of CMAKE_<LANG>_FLAGS so it can be applied to our own
|
||||
# targets only. Silencing a bundled target would otherwise override a warning level,
|
||||
# which cl reports as D9025 for every file it compiles.
|
||||
if (POLICY CMP0092)
|
||||
cmake_policy(SET CMP0092 NEW)
|
||||
endif ()
|
||||
|
||||
project(OrcaSlicer)
|
||||
|
||||
# Backward compatibility for old CMake versions
|
||||
@@ -91,14 +80,37 @@ endif()
|
||||
|
||||
if (DEFINED BBL_RELEASE_TO_PUBLIC)
|
||||
add_compile_definitions("BBL_RELEASE_TO_PUBLIC=${BBL_RELEASE_TO_PUBLIC}")
|
||||
if (BBL_RELEASE_TO_PUBLIC)
|
||||
add_compile_definitions(WXINSPECTOR_DISABLE)
|
||||
endif ()
|
||||
else ()
|
||||
add_compile_definitions("BBL_RELEASE_TO_PUBLIC=$<CONFIG:Release>")
|
||||
add_compile_definitions("$<$<CONFIG:Release>:WXINSPECTOR_DISABLE>")
|
||||
endif ()
|
||||
|
||||
find_package(Git)
|
||||
if(DEFINED ENV{git_commit_hash} AND NOT "$ENV{git_commit_hash}" STREQUAL "")
|
||||
message(STATUS "Specified git commit hash: $ENV{git_commit_hash}")
|
||||
if(GIT_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git")
|
||||
# Convert the given hash to short hash
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} rev-parse --short "$ENV{git_commit_hash}"
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE GIT_COMMIT_HASH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
else()
|
||||
# No .git directory (e.g., Flatpak sandbox) — truncate directly
|
||||
string(SUBSTRING "$ENV{git_commit_hash}" 0 7 GIT_COMMIT_HASH)
|
||||
endif()
|
||||
add_definitions("-DGIT_COMMIT_HASH=\"${GIT_COMMIT_HASH}\"")
|
||||
elseif(GIT_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git")
|
||||
# Check current Git commit hash
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} log -1 --format=%h
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE GIT_COMMIT_HASH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
add_definitions("-DGIT_COMMIT_HASH=\"${GIT_COMMIT_HASH}\"")
|
||||
endif()
|
||||
|
||||
if(DEFINED ENV{SLIC3R_STATIC})
|
||||
set(SLIC3R_STATIC_INITIAL $ENV{SLIC3R_STATIC})
|
||||
else()
|
||||
@@ -107,15 +119,11 @@ endif()
|
||||
|
||||
option(SLIC3R_STATIC "Compile OrcaSlicer with static libraries (Boost, TBB)" ${SLIC3R_STATIC_INITIAL})
|
||||
option(SLIC3R_GUI "Compile OrcaSlicer with GUI components (OpenGL, wxWidgets)" 1)
|
||||
option(SLIC3R_CAD "Compile OrcaSlicer with the parametric Design/CAD tab (needs OCCT ModelingAlgorithms)" 1)
|
||||
option(SLIC3R_FHS "Assume OrcaSlicer is to be installed in a FHS directory structure" 0)
|
||||
option(SLIC3R_PROFILE "Compile OrcaSlicer with an invasive Shiny profiler" 0)
|
||||
option(SLIC3R_PCH "Use precompiled headers" 1)
|
||||
option(SLIC3R_WARNINGS "Emit compiler warnings for OrcaSlicer sources" 1)
|
||||
option(SLIC3R_BUNDLED_WARNINGS "Emit compiler warnings for bundled third-party sources" 0)
|
||||
option(SLIC3R_MSVC_COMPILE_PARALLEL "Compile on Visual Studio in parallel" 1)
|
||||
option(SLIC3R_MSVC_PDB "Generate PDB files on MSVC in Release mode" 1)
|
||||
option(SLIC3R_RELATIVE_DEBUG_PATHS "Record a relative compilation directory in debug info (clang-cl)" 0)
|
||||
option(SLIC3R_ASAN "Enable ASan on Clang and GCC" 0)
|
||||
|
||||
# Python stubgen module
|
||||
@@ -277,8 +285,6 @@ if (APPLE)
|
||||
SET(CMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.orcaslicer.OrcaSlicer")
|
||||
|
||||
message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}")
|
||||
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
set(CMAKE_INSTALL_RPATH "$ORIGIN")
|
||||
endif ()
|
||||
|
||||
# Proposal for C++ unit tests and sandboxes
|
||||
@@ -309,10 +315,6 @@ if (SLIC3R_GUI)
|
||||
add_definitions(-DSLIC3R_GUI)
|
||||
endif ()
|
||||
|
||||
if (SLIC3R_CAD)
|
||||
add_definitions(-DSLIC3R_CAD)
|
||||
endif ()
|
||||
|
||||
if(SLIC3R_DESKTOP_INTEGRATION)
|
||||
add_definitions(-DSLIC3R_DESKTOP_INTEGRATION)
|
||||
endif ()
|
||||
@@ -329,47 +331,23 @@ if (MSVC AND CMAKE_CXX_COMPILER_ID STREQUAL Clang)
|
||||
|
||||
# clang-cl can interpret SYSTEM header paths if -imsvc is used
|
||||
set(CMAKE_INCLUDE_SYSTEM_FLAG_CXX "-imsvc")
|
||||
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall \
|
||||
-Wno-old-style-cast -Wno-reserved-id-macro -Wno-c++98-compat-pedantic")
|
||||
else ()
|
||||
set(IS_CLANG_CL FALSE)
|
||||
endif ()
|
||||
|
||||
if (MSVC)
|
||||
# CMP0092 only applies when the cache is created; an existing tree keeps its /W3,
|
||||
# which a silenced bundled target would then override (D9025, once per file).
|
||||
string(REGEX REPLACE "/W[0-4]" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}")
|
||||
string(REGEX REPLACE "/W[0-4]" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
|
||||
|
||||
# /MP only matters for the VS generators, where CMake turns it into the
|
||||
# MultiProcessorCompilation property. Ninja parallelises on its own, and
|
||||
# clang-cl warns "argument unused" if the flag reaches it.
|
||||
if (SLIC3R_MSVC_COMPILE_PARALLEL AND CMAKE_GENERATOR MATCHES "Visual Studio")
|
||||
if (SLIC3R_MSVC_COMPILE_PARALLEL AND NOT IS_CLANG_CL)
|
||||
add_compile_options(/MP)
|
||||
endif ()
|
||||
|
||||
# Parse lambdas the way the standard says, as clang and GCC already do. Without it
|
||||
# MSVC keeps its legacy lambda processor under /std:c++17 and rejects reading a
|
||||
# constexpr constant inside a lambda that does not capture it (C3493), which no
|
||||
# other compiler requires. Implied by /std:c++20 and /permissive-, so it is only
|
||||
# needed while we are on C++17. clang-cl is conforming already and does not take
|
||||
# the flag. Requires VS2019 16.8 or newer.
|
||||
if (NOT IS_CLANG_CL)
|
||||
# cl.exe only warns (D9002) about an unknown /Zc: option, so without this the
|
||||
# flag would be dropped and the first lambda reading a constexpr constant would
|
||||
# fail with C3493 far from the cause.
|
||||
if (MSVC_VERSION LESS 1928)
|
||||
message(FATAL_ERROR "Visual Studio 2019 16.8 (MSVC 19.28) or newer is required; detected MSVC ${MSVC_VERSION}.")
|
||||
endif ()
|
||||
add_compile_options(/Zc:lambda)
|
||||
endif ()
|
||||
# /bigobj (Increase Number of Sections in .Obj file)
|
||||
add_compile_options(-bigobj)
|
||||
# error C3859: virtual memory range for PCH exceeded; please recompile with a command line option of '-Zm90' or greater
|
||||
# Generate symbols at every build target, even for the release.
|
||||
# -Zm520 fixes error C3859 but forces the compiler to pre-allocate that memory for every translation unit regardless
|
||||
# combining /Zi with /FS frees up a significant amount of memory pressure across all parallel compile jobs and makes /MP faster overall.
|
||||
if (SLIC3R_MSVC_PDB)
|
||||
add_compile_options(/Zi /FS)
|
||||
endif ()
|
||||
add_compile_options(-bigobj /Zi /FS)
|
||||
# Disable STL4007: Many result_type typedefs and all argument_type, first_argument_type, and second_argument_type typedefs are deprecated in C++17.
|
||||
#FIXME Remove this line after eigen library adapts to the new C++17 adaptor rules.
|
||||
add_compile_options(-D_SILENCE_CXX17_ADAPTOR_TYPEDEFS_DEPRECATION_WARNING)
|
||||
@@ -387,16 +365,6 @@ if (MSVC)
|
||||
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /LTCG")
|
||||
endif ()
|
||||
|
||||
# Without this every object names its build directory and two worktrees never
|
||||
# share cache entries. The linker still writes absolute paths into the PDB.
|
||||
if (SLIC3R_RELATIVE_DEBUG_PATHS)
|
||||
if (IS_CLANG_CL)
|
||||
add_compile_options(-ffile-compilation-dir=.)
|
||||
else ()
|
||||
message(WARNING "SLIC3R_RELATIVE_DEBUG_PATHS is only implemented for clang-cl")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if (${CMAKE_CXX_COMPILER_ID} STREQUAL "AppleClang" AND ${CMAKE_CXX_COMPILER_VERSION} VERSION_GREATER 15)
|
||||
add_compile_definitions(BOOST_NO_CXX98_FUNCTION_BASE _HAS_AUTO_PTR_ETC=0)
|
||||
endif()
|
||||
@@ -551,103 +519,63 @@ if (CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUXX)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fext-numeric-literals" )
|
||||
endif()
|
||||
|
||||
if ((NOT MSVC OR IS_CLANG_CL) AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang"))
|
||||
if (IS_CLANG_CL)
|
||||
# clang-cl reads -Wall as MSVC /Wall, which clang maps to -Weverything. /W4 is
|
||||
# its -Wall -Wextra and, unlike /clang:-Wall, is ordered with the -Wno-* below
|
||||
# instead of after them. The -Wextra-only warnings are dropped again so the set
|
||||
# matches what -Wall gives the GNU/Clang builds.
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4" )
|
||||
add_compile_options(-Wno-unused-parameter -Wno-ignored-qualifiers -Wno-missing-field-initializers)
|
||||
elseif (NOT MINGW)
|
||||
if (NOT MSVC AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang"))
|
||||
if (NOT MINGW)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall" )
|
||||
endif ()
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-reorder" )
|
||||
|
||||
# Every warning is an error unless it appears in one of the two lists below.
|
||||
# disabled - never wanted. Off everywhere, so it never warns or errors.
|
||||
# demoted - wanted, not cleared yet. Still warns, does not error.
|
||||
|
||||
# Disabled.
|
||||
set(warnings_disabled
|
||||
reorder # members initialised in an order we chose
|
||||
sign-compare # signed/unsigned comparisons throughout
|
||||
misleading-indentation # false positives on mixed tabs and spaces
|
||||
switch # unhandled enum value in a switch
|
||||
unused-function # commented-out or conditionally compiled code
|
||||
unused-variable # commented-out or conditionally compiled code
|
||||
unused-but-set-variable # commented-out or conditionally compiled code
|
||||
unused-label # commented-out or conditionally compiled code
|
||||
unused-local-typedefs # commented-out or conditionally compiled code
|
||||
)
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
list(APPEND warnings_disabled deprecated-declarations) # legacy OpenGL calls
|
||||
endif ()
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 6.0)
|
||||
list(APPEND warnings_disabled ignored-attributes) # from Eigen headers marked SYSTEM
|
||||
endif ()
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
list(APPEND warnings_disabled unknown-pragmas) # igl pragmas, GCC bug 66943
|
||||
endif ()
|
||||
foreach (w IN LISTS warnings_disabled)
|
||||
add_compile_options(-Wno-${w})
|
||||
endforeach ()
|
||||
|
||||
# GCC is not built in CI, so don't throw errors CI won't catch.
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
# On GCC and Clang, no return from a non-void function is a warning only. Here, we make it an error.
|
||||
add_compile_options(-Werror=return-type)
|
||||
else ()
|
||||
# Turn everything else into an error. Dependency headers are exempt because the
|
||||
# SYSTEM include flag (-imsvc on clang-cl, -isystem elsewhere) keeps their
|
||||
# diagnostics out.
|
||||
add_compile_options(-Werror)
|
||||
endif ()
|
||||
|
||||
# Demoted. Remove a name once its category is cleared on every compiler.
|
||||
set(warnings_demoted)
|
||||
if (APPLE)
|
||||
list(APPEND warnings_demoted
|
||||
# MacDarkMode.mm makes two calls to AppKit's private titlebarViewController
|
||||
# and one to a wxWidgets category on NSTableColumn whose header is not
|
||||
# imported. Clearing it means declaring the private selectors ourselves, which
|
||||
# needs a macOS build to verify.
|
||||
objc-method-access
|
||||
)
|
||||
endif ()
|
||||
if (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM64")
|
||||
list(APPEND warnings_demoted
|
||||
# About two dozen GetProcAddress casts, most in the vendored dark_mode.hpp,
|
||||
# retype FARPROC to a real signature. The __stdcall typedefs are identical to
|
||||
# FARPROC on x64, so only arm64 reports them. Clearing them is a separate
|
||||
# sweep.
|
||||
cast-function-type-mismatch
|
||||
)
|
||||
endif ()
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
list(APPEND warnings_demoted
|
||||
# enum-constexpr-conversion is a Clang warning that defaults to an error,
|
||||
# present through clang 20 and gone in clang 21.
|
||||
enum-constexpr-conversion
|
||||
)
|
||||
endif ()
|
||||
# Since some portions of code are just commented out or put under conditional compilation, there are
|
||||
# a bunch of warning related to unused functions and variables. Suppress those warnings to not pollute
|
||||
# compilers diagnostics output with warnings we not going to look at
|
||||
add_compile_options(-Wno-unused-function -Wno-unused-variable -Wno-unused-but-set-variable -Wno-unused-label -Wno-unused-local-typedefs)
|
||||
|
||||
# The list mixes names not every compiler has, so add each exception only where the
|
||||
# compiler knows the warning. Probe with the positive -W<name>, which an unknown
|
||||
# warning fails on both compilers (GCC errors, Clang reports unknown-warning-option).
|
||||
# An option that takes a =N argument rejects the bare -W<name>, so fall back to
|
||||
# -W<name>=1 and demote with the trailing =.
|
||||
# Ignore signed/unsigned comparison warnings
|
||||
add_compile_options(-Wno-sign-compare)
|
||||
|
||||
# The mismatch of tabs and spaces throughout the project can sometimes
|
||||
# cause this warning to appear even though the indentation is fine.
|
||||
# Some includes also cause the warning
|
||||
add_compile_options(-Wno-misleading-indentation)
|
||||
|
||||
# Disable warning if enum value does not have a corresponding case in switch statement
|
||||
add_compile_options(-Wno-switch)
|
||||
|
||||
# removes LOTS of extraneous Eigen warnings (GCC only supports it since 6.1)
|
||||
# https://eigen.tuxfamily.org/bz/show_bug.cgi?id=1221
|
||||
if("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang" OR CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 6.0)
|
||||
add_compile_options(-Wno-ignored-attributes) # Tamas: Eigen include dirs are marked as SYSTEM
|
||||
endif()
|
||||
|
||||
# Clang reports legacy OpenGL calls as deprecated. Turn off the warning for now
|
||||
# to reduce the clutter, we know about this one. It should be reenabled after
|
||||
# we finally get rid of the deprecated code.
|
||||
if("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang")
|
||||
add_compile_options(-Wno-deprecated-declarations)
|
||||
endif()
|
||||
|
||||
if((${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang" OR ${CMAKE_CXX_COMPILER_ID} STREQUAL "AppleClang") AND ${CMAKE_CXX_COMPILER_VERSION} VERSION_GREATER 15)
|
||||
include(CheckCXXCompilerFlag)
|
||||
foreach (category IN LISTS warnings_demoted)
|
||||
string(MAKE_C_IDENTIFIER "ORCA_HAS_W_${category}" _orca_has_w)
|
||||
check_cxx_compiler_flag("-W${category}" ${_orca_has_w})
|
||||
if (${_orca_has_w})
|
||||
add_compile_options(-Wno-error=${category})
|
||||
else ()
|
||||
check_cxx_compiler_flag("-W${category}=1" ${_orca_has_w}_arg)
|
||||
if (${${_orca_has_w}_arg})
|
||||
add_compile_options(-Wno-error=${category}=)
|
||||
endif ()
|
||||
endif ()
|
||||
endforeach ()
|
||||
check_cxx_compiler_flag(-Wno-error=enum-constexpr-conversion HAS_WNO_ERROR_ENUM_CONSTEXPR_CONV)
|
||||
if(HAS_WNO_ERROR_ENUM_CONSTEXPR_CONV)
|
||||
add_compile_options(-Wno-error=enum-constexpr-conversion)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
#GCC generates loads of -Wunknown-pragmas when compiling igl. The fix is not easy due to a bug in gcc, see
|
||||
# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66943 or
|
||||
# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=53431
|
||||
# We will turn the warning of for GCC for now:
|
||||
if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
|
||||
# GCC generates loads of -Wunknown-pragmas when compiling igl. The fix is not easy due to a bug in gcc, see
|
||||
# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66943 or
|
||||
# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=53431
|
||||
# We will turn the warning of for GCC for now:
|
||||
add_compile_options(-Wno-unknown-pragmas)
|
||||
endif()
|
||||
|
||||
# Compress the debug info with zstd to save space in Flatpak CI builds
|
||||
if(FLATPAK)
|
||||
@@ -657,6 +585,10 @@ if ((NOT MSVC OR IS_CLANG_CL) AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 14)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-error=template-id-cdtor" )
|
||||
endif()
|
||||
|
||||
endif()
|
||||
|
||||
if (SLIC3R_ASAN)
|
||||
@@ -1081,150 +1013,77 @@ function(orcaslicer_copy_dlls target config postfix output_dlls)
|
||||
${TOP_LEVEL_PROJECT_DIR}/deps/WebView2/lib/win-${_arch}/WebView2Loader.dll
|
||||
DESTINATION ${_out_dir})
|
||||
|
||||
# Stage the OCCT toolkits libslic3r links (published as OCCT_LIBS), not whatever the
|
||||
# deps prefix happens to hold, and fail the configure if one of them is missing.
|
||||
if (NOT OCCT_LIBS)
|
||||
message(FATAL_ERROR "OCCT_LIBS is not set; libslic3r must be configured first.")
|
||||
endif ()
|
||||
set(_occt_bin "${CMAKE_PREFIX_PATH}/bin/occt")
|
||||
set(_occt_dlls "")
|
||||
set(_occt_staged "")
|
||||
set(_missing_occt "")
|
||||
foreach (_tk IN LISTS OCCT_LIBS)
|
||||
if (EXISTS "${_occt_bin}/${_tk}.dll")
|
||||
list(APPEND _occt_dlls "${_occt_bin}/${_tk}.dll")
|
||||
list(APPEND _occt_staged "${_out_dir}/${_tk}.dll")
|
||||
else ()
|
||||
list(APPEND _missing_occt "${_tk}.dll")
|
||||
endif ()
|
||||
endforeach ()
|
||||
if (_missing_occt)
|
||||
message(FATAL_ERROR
|
||||
"OCCT DLLs missing from ${_occt_bin}/: ${_missing_occt}\n"
|
||||
"Rebuild the dependencies (build_release_vs2022.bat deps) with the same "
|
||||
"SLIC3R_CAD setting as this project.")
|
||||
endif ()
|
||||
file(COPY ${_occt_dlls}
|
||||
file(COPY ${CMAKE_PREFIX_PATH}/bin/occt/TKBO.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKBRep.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKCAF.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKCDF.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKernel.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKG2d.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKG3d.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKGeomAlgo.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKGeomBase.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKHLR.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKLCAF.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKMath.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKMesh.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKPrim.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKService.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKShHealing.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKSTEP.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKSTEP209.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKSTEPAttr.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKSTEPBase.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKTopAlgo.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKV3d.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKVCAF.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKXCAF.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKXDESTEP.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKXSBase.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/freetype.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/avcodec-61.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/swresample-5.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/swscale-8.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/avutil-59.dll
|
||||
DESTINATION ${_out_dir})
|
||||
|
||||
set(_dll_list
|
||||
set(${output_dlls}
|
||||
${_out_dir}/libgmp-10.dll
|
||||
${_out_dir}/libmpfr-4.dll
|
||||
${_out_dir}/WebView2Loader.dll
|
||||
|
||||
${_out_dir}/TKBO.dll
|
||||
${_out_dir}/TKBRep.dll
|
||||
${_out_dir}/TKCAF.dll
|
||||
${_out_dir}/TKCDF.dll
|
||||
${_out_dir}/TKernel.dll
|
||||
${_out_dir}/TKG2d.dll
|
||||
${_out_dir}/TKG3d.dll
|
||||
${_out_dir}/TKGeomAlgo.dll
|
||||
${_out_dir}/TKGeomBase.dll
|
||||
${_out_dir}/TKHLR.dll
|
||||
${_out_dir}/TKLCAF.dll
|
||||
${_out_dir}/TKMath.dll
|
||||
${_out_dir}/TKMesh.dll
|
||||
${_out_dir}/TKPrim.dll
|
||||
${_out_dir}/TKService.dll
|
||||
${_out_dir}/TKShHealing.dll
|
||||
${_out_dir}/TKSTEP.dll
|
||||
${_out_dir}/TKSTEP209.dll
|
||||
${_out_dir}/TKSTEPAttr.dll
|
||||
${_out_dir}/TKSTEPBase.dll
|
||||
${_out_dir}/TKTopAlgo.dll
|
||||
${_out_dir}/TKV3d.dll
|
||||
${_out_dir}/TKVCAF.dll
|
||||
${_out_dir}/TKXCAF.dll
|
||||
${_out_dir}/TKXDESTEP.dll
|
||||
${_out_dir}/TKXSBase.dll
|
||||
|
||||
${_out_dir}/freetype.dll
|
||||
${_out_dir}/avcodec-61.dll
|
||||
${_out_dir}/swresample-5.dll
|
||||
${_out_dir}/swscale-8.dll
|
||||
${_out_dir}/avutil-59.dll
|
||||
|
||||
PARENT_SCOPE
|
||||
)
|
||||
list(APPEND _dll_list ${_occt_staged})
|
||||
set(${output_dlls} ${_dll_list} PARENT_SCOPE)
|
||||
|
||||
endfunction()
|
||||
|
||||
function(orcaslicer_copy_sos target config postfix output_sos)
|
||||
|
||||
get_property(_is_multi GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
|
||||
get_target_property(_alt_out_dir ${target} RUNTIME_OUTPUT_DIRECTORY)
|
||||
|
||||
if (_alt_out_dir)
|
||||
set(_out_dir "${_alt_out_dir}")
|
||||
elseif (_is_multi)
|
||||
set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}/${config}")
|
||||
else ()
|
||||
set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
endif ()
|
||||
|
||||
file(COPY ${CMAKE_PREFIX_PATH}/lib/libavcodec.so
|
||||
${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61
|
||||
${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61.3.100
|
||||
${CMAKE_PREFIX_PATH}/lib/libavutil.so
|
||||
${CMAKE_PREFIX_PATH}/lib/libavutil.so.59
|
||||
${CMAKE_PREFIX_PATH}/lib/libavutil.so.59.8.100
|
||||
${CMAKE_PREFIX_PATH}/lib/libswscale.so
|
||||
${CMAKE_PREFIX_PATH}/lib/libswscale.so.8
|
||||
${CMAKE_PREFIX_PATH}/lib/libswscale.so.8.1.100
|
||||
${CMAKE_PREFIX_PATH}/lib/libswresample.so
|
||||
${CMAKE_PREFIX_PATH}/lib/libswresample.so.5
|
||||
${CMAKE_PREFIX_PATH}/lib/libswresample.so.5.1.100
|
||||
DESTINATION ${_out_dir})
|
||||
|
||||
set(${output_sos}
|
||||
${_out_dir}/libavcodec.so
|
||||
${_out_dir}/libavcodec.so.61
|
||||
${_out_dir}/libavcodec.so.61.3.100
|
||||
${_out_dir}/libavutil.so
|
||||
${_out_dir}/libavutil.so.59
|
||||
${_out_dir}/libavutil.so.59.8.100
|
||||
${_out_dir}/libswscale.so
|
||||
${_out_dir}/libswscale.so.8
|
||||
${_out_dir}/libswscale.so.8.1.100
|
||||
${_out_dir}/libswresample.so
|
||||
${_out_dir}/libswresample.so.5
|
||||
${_out_dir}/libswresample.so.5.1.100
|
||||
PARENT_SCOPE
|
||||
)
|
||||
endfunction()
|
||||
|
||||
# Bundled sources set their own warning flags, and a plain -Wall there means /Wall
|
||||
# (= -Weverything) under clang-cl. Target options are applied after the ones a target
|
||||
# set on itself, so these win. Targets are discovered rather than listed so a newly
|
||||
# bundled library needs no maintenance here.
|
||||
function(orcaslicer_silence_third_party_warnings _dir)
|
||||
get_property(_subdirs DIRECTORY "${_dir}" PROPERTY SUBDIRECTORIES)
|
||||
foreach (_subdir IN LISTS _subdirs)
|
||||
orcaslicer_silence_third_party_warnings("${_subdir}")
|
||||
endforeach ()
|
||||
get_property(_targets DIRECTORY "${_dir}" PROPERTY BUILDSYSTEM_TARGETS)
|
||||
foreach (_target IN LISTS _targets)
|
||||
get_target_property(_type ${_target} TYPE)
|
||||
if (NOT _type STREQUAL "INTERFACE_LIBRARY" AND NOT _type STREQUAL "UTILITY")
|
||||
if (MSVC AND NOT IS_CLANG_CL)
|
||||
# Drop any level the target set for itself, or -w overrides it and cl
|
||||
# reports D9025 once per file.
|
||||
get_target_property(_opts ${_target} COMPILE_OPTIONS)
|
||||
if (_opts)
|
||||
string(REGEX REPLACE "/W[0-4]|/Wall" "" _opts "${_opts}")
|
||||
string(REGEX REPLACE ";;+" ";" _opts "${_opts}")
|
||||
set_target_properties(${_target} PROPERTIES COMPILE_OPTIONS "${_opts}")
|
||||
endif ()
|
||||
# CMake maps a level into the VS generator's WarningLevel element, while a
|
||||
# bare -w stays on the command line and trips D9025 there, once per file.
|
||||
target_compile_options(${_target} PRIVATE /W0)
|
||||
else ()
|
||||
target_compile_options(${_target} PRIVATE -w)
|
||||
endif ()
|
||||
endif ()
|
||||
endforeach ()
|
||||
endfunction()
|
||||
|
||||
|
||||
# libslic3r, OrcaSlicer GUI and the OrcaSlicer executable.
|
||||
add_subdirectory(deps_src)
|
||||
|
||||
if (NOT SLIC3R_BUNDLED_WARNINGS)
|
||||
orcaslicer_silence_third_party_warnings("${CMAKE_CURRENT_SOURCE_DIR}/deps_src")
|
||||
endif ()
|
||||
|
||||
# Warning level for the targets added below: our sources, plus glad and libvgcode,
|
||||
# which are vendored but live under src/. The deps_src libraries were configured just
|
||||
# above. CMP0092 left MSVC without a default level, so it is set here.
|
||||
if (NOT SLIC3R_WARNINGS)
|
||||
add_compile_options(-w)
|
||||
elseif (MSVC AND NOT IS_CLANG_CL)
|
||||
# /we4715 is C4715, no return from a non-void function, an error on the GNU/Clang
|
||||
# builds under -Werror. MSVC is not in that model, so this stays a single promoted
|
||||
# warning.
|
||||
add_compile_options(/W3 /we4715)
|
||||
endif ()
|
||||
|
||||
add_subdirectory(src)
|
||||
set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT OrcaSlicer_app_gui)
|
||||
|
||||
@@ -1236,10 +1095,6 @@ endif()
|
||||
|
||||
if(BUILD_TESTS)
|
||||
add_subdirectory(tests)
|
||||
if (NOT SLIC3R_BUNDLED_WARNINGS)
|
||||
# Catch2 is vendored under tests/ and sets its own warning flags too.
|
||||
orcaslicer_silence_third_party_warnings("${CMAKE_CURRENT_SOURCE_DIR}/tests/catch2")
|
||||
endif ()
|
||||
endif()
|
||||
|
||||
if (NOT WIN32 AND NOT APPLE)
|
||||
@@ -1284,20 +1139,6 @@ else ()
|
||||
endif()
|
||||
endif ()
|
||||
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
set(LIBRARY_FILES
|
||||
${LIBDIR_BIN}/libavcodec.so.61
|
||||
${LIBDIR_BIN}/libavcodec.so.61.3.100
|
||||
${LIBDIR_BIN}/libavutil.so.59
|
||||
${LIBDIR_BIN}/libavutil.so.59.8.100
|
||||
${LIBDIR_BIN}/libswresample.so.5
|
||||
${LIBDIR_BIN}/libswresample.so.5.1.100
|
||||
${LIBDIR_BIN}/libswscale.so.8
|
||||
${LIBDIR_BIN}/libswscale.so.8.1.100
|
||||
)
|
||||
install(FILES ${LIBRARY_FILES} DESTINATION "${CMAKE_INSTALL_PREFIX}/bin")
|
||||
endif ()
|
||||
|
||||
install(FILES ${CMAKE_SOURCE_DIR}/LICENSE.txt DESTINATION ".")
|
||||
configure_file(${LIBDIR}/dev-utils/platform/unix/fhs.hpp.in ${LIBDIR_BIN}/dev-utils/platform/unix/fhs.hpp)
|
||||
|
||||
|
||||
@@ -260,9 +260,6 @@ if [[ ! -f "./scripts/flatpak/com.orcaslicer.OrcaSlicer.yml" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}Packing deps/ for the manifest...${NC}"
|
||||
./scripts/flatpak/make_deps_tar.sh
|
||||
|
||||
# Build the Flatpak
|
||||
echo -e "${YELLOW}Building Flatpak package...${NC}"
|
||||
echo -e "This may take a while (30+ minutes depending on your system)..."
|
||||
|
||||
@@ -567,8 +567,6 @@ if [[ -n "${BUILD_ORCA}" ]] || [[ -n "${BUILD_TESTS}" ]] ; then
|
||||
print_and_run cmake --build $BUILD_DIR --config "${BUILD_CONFIG}" --target OrcaSlicer
|
||||
echo "Building OrcaSlicer_profile_validator .."
|
||||
print_and_run cmake --build $BUILD_DIR --config "${BUILD_CONFIG}" --target OrcaSlicer_profile_validator
|
||||
echo "Building generate_system_cache ..."
|
||||
print_and_run cmake --build $BUILD_DIR --config "${BUILD_CONFIG}" --target generate_system_cache
|
||||
./scripts/run_gettext.sh
|
||||
fi
|
||||
if [[ -n "${BUILD_TESTS}" ]] ; then
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
set WP=%CD%
|
||||
|
||||
set debug=OFF
|
||||
set debuginfo=OFF
|
||||
if "%1"=="debug" set debug=ON
|
||||
if "%2"=="debug" set debug=ON
|
||||
if "%1"=="debuginfo" set debuginfo=ON
|
||||
if "%2"=="debuginfo" set debuginfo=ON
|
||||
if "%debug%"=="ON" (
|
||||
set build_type=Debug
|
||||
set build_dir=build-dbg
|
||||
) else (
|
||||
if "%debuginfo%"=="ON" (
|
||||
set build_type=RelWithDebInfo
|
||||
set build_dir=build-dbginfo
|
||||
) else (
|
||||
set build_type=Release
|
||||
set build_dir=build
|
||||
)
|
||||
)
|
||||
echo build type set to %build_type%
|
||||
|
||||
cd deps
|
||||
mkdir %build_dir%
|
||||
cd %build_dir%
|
||||
set DEPS=%CD%/OrcaSlicer_dep
|
||||
set "SIG_FLAG="
|
||||
if defined ORCA_UPDATER_SIG_KEY set "SIG_FLAG=-DORCA_UPDATER_SIG_KEY=%ORCA_UPDATER_SIG_KEY%"
|
||||
if "%1"=="slicer" (
|
||||
GOTO :slicer
|
||||
)
|
||||
echo "building deps.."
|
||||
|
||||
echo cmake ../ -G "Visual Studio 16 2019" -A x64 -DCMAKE_BUILD_TYPE=%build_type%
|
||||
cmake ../ -G "Visual Studio 16 2019" -A x64 -DCMAKE_BUILD_TYPE=%build_type%
|
||||
cmake --build . --config %build_type% --target deps -- -m
|
||||
|
||||
if "%1"=="deps" exit /b 0
|
||||
|
||||
:slicer
|
||||
echo "building Orca Slicer..."
|
||||
cd %WP%
|
||||
mkdir %build_dir%
|
||||
cd %build_dir%
|
||||
|
||||
echo cmake .. -G "Visual Studio 16 2019" -A x64 -DCMAKE_BUILD_TYPE=%build_type%
|
||||
cmake .. -G "Visual Studio 16 2019" -A x64 -DCMAKE_BUILD_TYPE=%build_type% %SIG_FLAG%
|
||||
cmake --build . --config %build_type% --target ALL_BUILD -- -m
|
||||
cd ..
|
||||
call scripts/run_gettext.bat
|
||||
cd %build_dir%
|
||||
cmake --build . --target install --config %build_type%
|
||||
@@ -4,7 +4,7 @@ set -e
|
||||
set -o pipefail
|
||||
SECONDS=0
|
||||
|
||||
while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
|
||||
while getopts ":dpa:snt:xbc:i:1Tuh" opt; do
|
||||
case "${opt}" in
|
||||
d )
|
||||
export BUILD_TARGET="deps"
|
||||
@@ -38,8 +38,8 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
|
||||
i )
|
||||
export CMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH:+$CMAKE_IGNORE_PREFIX_PATH;}$OPTARG"
|
||||
;;
|
||||
j )
|
||||
export CMAKE_BUILD_PARALLEL_LEVEL="$OPTARG"
|
||||
1 )
|
||||
export CMAKE_BUILD_PARALLEL_LEVEL=1
|
||||
;;
|
||||
T )
|
||||
export BUILD_TESTS="1"
|
||||
@@ -58,7 +58,7 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
|
||||
echo " -b: Build without reconfiguring CMake"
|
||||
echo " -c: Set CMake build configuration, default is Release"
|
||||
echo " -i: Add a prefix to ignore during CMake dependency discovery (repeatable), defaults to /opt/local:/usr/local:/opt/homebrew"
|
||||
echo " -j: Set the number of parallel build jobs (CMAKE_BUILD_PARALLEL_LEVEL)"
|
||||
echo " -1: Use single job for building"
|
||||
echo " -T: Build and run tests (set ORCA_TESTS_BUILD_ONLY=1 to build without running)"
|
||||
exit 0
|
||||
;;
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
@REM OrcaSlicer build script for Windows with VS auto-detect
|
||||
@echo off
|
||||
set WP=%CD%
|
||||
set _START_TIME=%TIME%
|
||||
|
||||
@REM Default target architecture to the host CPU arch; override by passing
|
||||
@REM "x64" or "arm64" as an argument. PROCESSOR_ARCHITEW6432 covers a 32-bit
|
||||
@REM shell running on a 64-bit OS, where PROCESSOR_ARCHITECTURE reads "x86".
|
||||
set arch=x64
|
||||
if /I "%PROCESSOR_ARCHITECTURE%"=="ARM64" set arch=ARM64
|
||||
if /I "%PROCESSOR_ARCHITEW6432%"=="ARM64" set arch=ARM64
|
||||
if /I "%1"=="arm64" set arch=ARM64
|
||||
if /I "%2"=="arm64" set arch=ARM64
|
||||
if /I "%1"=="x64" set arch=x64
|
||||
if /I "%2"=="x64" set arch=x64
|
||||
|
||||
@REM Check for Ninja Multi-Config option (-x)
|
||||
set USE_NINJA=0
|
||||
for %%a in (%*) do (
|
||||
if "%%a"=="-x" set USE_NINJA=1
|
||||
)
|
||||
|
||||
@REM Check for unit-tests option ("tests")
|
||||
set BUILD_TESTS=OFF
|
||||
for %%a in (%*) do (
|
||||
if /I "%%a"=="tests" set BUILD_TESTS=ON
|
||||
)
|
||||
|
||||
if "%USE_NINJA%"=="1" (
|
||||
echo Using Ninja Multi-Config generator
|
||||
set CMAKE_GENERATOR="Ninja Multi-Config"
|
||||
set VS_VERSION=Ninja
|
||||
goto :generator_ready
|
||||
)
|
||||
|
||||
@REM Detect Visual Studio version using msbuild
|
||||
echo Detecting Visual Studio version using msbuild...
|
||||
|
||||
@REM Try to get MSBuild version - the output format varies by VS version
|
||||
set VS_MAJOR=
|
||||
for /f "tokens=*" %%i in ('msbuild -version 2^>^&1 ^| findstr /r "^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"') do (
|
||||
for /f "tokens=1 delims=." %%a in ("%%i") do set VS_MAJOR=%%a
|
||||
set MSBUILD_OUTPUT=%%i
|
||||
goto :version_found
|
||||
)
|
||||
|
||||
@REM Alternative method for newer MSBuild versions
|
||||
if "%VS_MAJOR%"=="" (
|
||||
for /f "tokens=*" %%i in ('msbuild -version 2^>^&1 ^| findstr /r "[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"') do (
|
||||
for /f "tokens=1 delims=." %%a in ("%%i") do set VS_MAJOR=%%a
|
||||
set MSBUILD_OUTPUT=%%i
|
||||
goto :version_found
|
||||
)
|
||||
)
|
||||
|
||||
:version_found
|
||||
echo MSBuild version detected: %MSBUILD_OUTPUT%
|
||||
echo Major version: %VS_MAJOR%
|
||||
|
||||
if "%VS_MAJOR%"=="" (
|
||||
echo Error: Could not determine Visual Studio version from msbuild
|
||||
echo Please ensure Visual Studio and MSBuild are properly installed
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if "%VS_MAJOR%"=="16" (
|
||||
set VS_VERSION=2019
|
||||
set CMAKE_GENERATOR="Visual Studio 16 2019"
|
||||
) else if "%VS_MAJOR%"=="17" (
|
||||
set VS_VERSION=2022
|
||||
set CMAKE_GENERATOR="Visual Studio 17 2022"
|
||||
) else if "%VS_MAJOR%"=="18" (
|
||||
set VS_VERSION=2026
|
||||
set CMAKE_GENERATOR="Visual Studio 18 2026"
|
||||
) else (
|
||||
echo Error: Unsupported Visual Studio version: %VS_MAJOR%
|
||||
echo Supported versions: VS2019 (16.x^), VS2022 (17.x^), VS2026 (18.x^)
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Detected Visual Studio %VS_VERSION% (version %VS_MAJOR%)
|
||||
echo Using CMake generator: %CMAKE_GENERATOR%
|
||||
|
||||
:generator_ready
|
||||
|
||||
@REM Pack deps
|
||||
if "%1"=="pack" (
|
||||
setlocal ENABLEDELAYEDEXPANSION
|
||||
cd %WP%/deps/build
|
||||
if "%arch%"=="ARM64" cd %WP%/deps/build-arm64
|
||||
for /f "tokens=2-4 delims=/ " %%a in ('date /t') do set build_date=%%c%%b%%a
|
||||
echo packing deps: OrcaSlicer_dep_win-!arch!_!build_date!_vs!VS_VERSION!.zip
|
||||
|
||||
%WP%/tools/7z.exe a OrcaSlicer_dep_win-!arch!_!build_date!_vs!VS_VERSION!.zip OrcaSlicer_dep
|
||||
goto :done
|
||||
)
|
||||
|
||||
set debug=OFF
|
||||
set debuginfo=OFF
|
||||
if "%1"=="debug" set debug=ON
|
||||
if "%2"=="debug" set debug=ON
|
||||
if "%1"=="debuginfo" set debuginfo=ON
|
||||
if "%2"=="debuginfo" set debuginfo=ON
|
||||
if "%debug%"=="ON" (
|
||||
set build_type=Debug
|
||||
set build_dir=build-dbg
|
||||
) else (
|
||||
if "%debuginfo%"=="ON" (
|
||||
set build_type=RelWithDebInfo
|
||||
set build_dir=build-dbginfo
|
||||
) else (
|
||||
set build_type=Release
|
||||
set build_dir=build
|
||||
)
|
||||
)
|
||||
if "%arch%"=="ARM64" set build_dir=%build_dir%-arm64
|
||||
echo build type set to %build_type%, arch=%arch%
|
||||
|
||||
setlocal DISABLEDELAYEDEXPANSION
|
||||
cd deps
|
||||
mkdir %build_dir%
|
||||
cd %build_dir%
|
||||
set "SIG_FLAG="
|
||||
if defined ORCA_UPDATER_SIG_KEY set "SIG_FLAG=-DORCA_UPDATER_SIG_KEY=%ORCA_UPDATER_SIG_KEY%"
|
||||
|
||||
if "%1"=="slicer" (
|
||||
GOTO :slicer
|
||||
)
|
||||
echo "building deps.."
|
||||
|
||||
echo on
|
||||
REM Set minimum CMake policy to avoid <3.5 errors
|
||||
set CMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||
if "%USE_NINJA%"=="1" (
|
||||
cmake ../ -G %CMAKE_GENERATOR% -DCMAKE_BUILD_TYPE=%build_type%
|
||||
cmake --build . --config %build_type% --target deps
|
||||
) else (
|
||||
cmake ../ -G %CMAKE_GENERATOR% -A %arch% -DCMAKE_BUILD_TYPE=%build_type%
|
||||
cmake --build . --config %build_type% --target deps -- -m
|
||||
)
|
||||
@echo off
|
||||
|
||||
if "%1"=="deps" goto :done
|
||||
|
||||
:slicer
|
||||
echo "building Orca Slicer..."
|
||||
cd %WP%
|
||||
mkdir %build_dir%
|
||||
cd %build_dir%
|
||||
|
||||
echo on
|
||||
set CMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||
if "%USE_NINJA%"=="1" (
|
||||
cmake .. -G %CMAKE_GENERATOR% -DORCA_TOOLS=ON %SIG_FLAG% -DBUILD_TESTS=%BUILD_TESTS% -DCMAKE_BUILD_TYPE=%build_type%
|
||||
cmake --build . --config %build_type% --target ALL_BUILD
|
||||
) else (
|
||||
cmake .. -G %CMAKE_GENERATOR% -A %arch% -DORCA_TOOLS=ON %SIG_FLAG% -DBUILD_TESTS=%BUILD_TESTS% -DCMAKE_BUILD_TYPE=%build_type%
|
||||
cmake --build . --config %build_type% --target ALL_BUILD -- -m
|
||||
)
|
||||
@echo off
|
||||
cd ..
|
||||
call scripts/run_gettext.bat
|
||||
cd %build_dir%
|
||||
cmake --build . --target install --config %build_type%
|
||||
|
||||
:done
|
||||
@echo off
|
||||
for /f "tokens=1-3 delims=:.," %%a in ("%_START_TIME: =0%") do set /a "_start_s=%%a*3600+%%b*60+%%c"
|
||||
for /f "tokens=1-3 delims=:.," %%a in ("%TIME: =0%") do set /a "_end_s=%%a*3600+%%b*60+%%c"
|
||||
set /a "_elapsed=_end_s - _start_s"
|
||||
if %_elapsed% lss 0 set /a "_elapsed+=86400"
|
||||
set /a "_hours=_elapsed / 3600"
|
||||
set /a "_remainder=_elapsed - _hours * 3600"
|
||||
set /a "_mins=_remainder / 60"
|
||||
set /a "_secs=_remainder - _mins * 60"
|
||||
echo.
|
||||
echo Build completed in %_hours%h %_mins%m %_secs%s
|
||||
@@ -0,0 +1,80 @@
|
||||
@REM OrcaSlicer build script for Windows
|
||||
@echo off
|
||||
set WP=%CD%
|
||||
|
||||
@REM Pack deps
|
||||
if "%1"=="pack" (
|
||||
setlocal ENABLEDELAYEDEXPANSION
|
||||
cd %WP%/deps/build
|
||||
for /f "tokens=2-4 delims=/ " %%a in ('date /t') do set build_date=%%c%%b%%a
|
||||
echo packing deps: OrcaSlicer_dep_win64_!build_date!_vs2022.zip
|
||||
|
||||
%WP%/tools/7z.exe a OrcaSlicer_dep_win64_!build_date!_vs2022.zip OrcaSlicer_dep
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
set debug=OFF
|
||||
set debuginfo=OFF
|
||||
@REM Default target architecture to the host CPU arch; override with x64/arm64 arg.
|
||||
set arch=x64
|
||||
if /I "%PROCESSOR_ARCHITECTURE%"=="ARM64" set arch=ARM64
|
||||
if /I "%PROCESSOR_ARCHITEW6432%"=="ARM64" set arch=ARM64
|
||||
if "%1"=="debug" set debug=ON
|
||||
if "%2"=="debug" set debug=ON
|
||||
if "%1"=="debuginfo" set debuginfo=ON
|
||||
if "%2"=="debuginfo" set debuginfo=ON
|
||||
if /I "%1"=="arm64" set arch=ARM64
|
||||
if /I "%2"=="arm64" set arch=ARM64
|
||||
if /I "%1"=="x64" set arch=x64
|
||||
if /I "%2"=="x64" set arch=x64
|
||||
if "%debug%"=="ON" (
|
||||
set build_type=Debug
|
||||
set build_dir=build-dbg
|
||||
) else (
|
||||
if "%debuginfo%"=="ON" (
|
||||
set build_type=RelWithDebInfo
|
||||
set build_dir=build-dbginfo
|
||||
) else (
|
||||
set build_type=Release
|
||||
set build_dir=build
|
||||
)
|
||||
)
|
||||
if "%arch%"=="ARM64" set build_dir=%build_dir%-arm64
|
||||
echo build type set to %build_type%, arch=%arch%
|
||||
|
||||
setlocal DISABLEDELAYEDEXPANSION
|
||||
cd deps
|
||||
mkdir %build_dir%
|
||||
cd %build_dir%
|
||||
set "SIG_FLAG="
|
||||
if defined ORCA_UPDATER_SIG_KEY set "SIG_FLAG=-DORCA_UPDATER_SIG_KEY=%ORCA_UPDATER_SIG_KEY%"
|
||||
|
||||
if "%1"=="slicer" (
|
||||
GOTO :slicer
|
||||
)
|
||||
echo "building deps.."
|
||||
|
||||
echo on
|
||||
REM Set minimum CMake policy to avoid <3.5 errors
|
||||
set CMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||
cmake ../ -G "Visual Studio 17 2022" -A %arch% -DCMAKE_BUILD_TYPE=%build_type%
|
||||
cmake --build . --config %build_type% --target deps -- -m
|
||||
@echo off
|
||||
|
||||
if "%1"=="deps" exit /b 0
|
||||
|
||||
:slicer
|
||||
echo "building Orca Slicer..."
|
||||
cd %WP%
|
||||
mkdir %build_dir%
|
||||
cd %build_dir%
|
||||
|
||||
echo on
|
||||
set CMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||
cmake .. -G "Visual Studio 17 2022" -A %arch% -DORCA_TOOLS=ON %SIG_FLAG% -DCMAKE_BUILD_TYPE=%build_type%
|
||||
cmake --build . --config %build_type% --target ALL_BUILD -- -m
|
||||
@echo off
|
||||
cd ..
|
||||
call scripts/run_gettext.bat
|
||||
cd %build_dir%
|
||||
cmake --build . --target install --config %build_type%
|
||||
@@ -256,13 +256,6 @@ function(add_precompiled_header _target _input)
|
||||
message(STATUS "Adding precompiled header ${_input} to target ${_target}.")
|
||||
target_precompile_headers(${_target} PRIVATE ${_input})
|
||||
|
||||
# Clang records the modification time of every input in the precompiled
|
||||
# header, which makes it differ between two checkouts of the same source
|
||||
# and defeats a compiler cache. The build system already rebuilds the
|
||||
# header when an input changes.
|
||||
target_compile_options(${_target} PRIVATE
|
||||
"$<$<CXX_COMPILER_ID:Clang,AppleClang>:SHELL:-Xclang -fno-pch-timestamp>")
|
||||
|
||||
get_target_property(_sources ${_target} SOURCES)
|
||||
list(FILTER _sources INCLUDE REGEX ".*\\.mm?")
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
if(CMAKE_VERSION VERSION_LESS 3.22)
|
||||
set(_assimp_url "https://github.com/assimp/assimp/archive/refs/tags/v5.3.1.tar.gz")
|
||||
set(_assimp_hash "SHA256=a07666be71afe1ad4bc008c2336b7c688aca391271188eb9108d0c6db1be53f1")
|
||||
else()
|
||||
set(_assimp_url "https://github.com/assimp/assimp/archive/refs/tags/v5.4.3.tar.gz")
|
||||
set(_assimp_hash "SHA256=66dfbaee288f2bc43172440a55d0235dfc7bf885dda6435c038e8000e79582cb")
|
||||
endif()
|
||||
|
||||
# Assimp's bundled zlib (contrib/zlib) is too old to compile against the modern
|
||||
# macOS SDK: its zutil.h takes the classic-Mac branch under TARGET_OS_MAC and
|
||||
# does `#define fdopen(fd,mode) NULL`, which then clobbers the SDK's real
|
||||
# `fdopen` prototype in <stdio.h> and breaks the build. On macOS use the system
|
||||
# zlib (already found by find_package(ZLIB) in deps-unix-common) instead.
|
||||
if(APPLE)
|
||||
set(_assimp_build_zlib "-DASSIMP_BUILD_ZLIB=OFF")
|
||||
else()
|
||||
set(_assimp_build_zlib "-DASSIMP_BUILD_ZLIB=ON")
|
||||
endif()
|
||||
|
||||
orcaslicer_add_cmake_project(Assimp
|
||||
URL ${_assimp_url}
|
||||
URL_HASH ${_assimp_hash}
|
||||
CMAKE_ARGS
|
||||
# Assimp's ccache support sets the global RULE_LAUNCH_COMPILE, which breaks
|
||||
# the Ninja RC rule. The superbuild forwards CMAKE_<LANG>_COMPILER_LAUNCHER.
|
||||
-DASSIMP_BUILD_USE_CCACHE=OFF
|
||||
-DASSIMP_BUILD_TESTS=OFF
|
||||
-DASSIMP_BUILD_SAMPLES=OFF
|
||||
-DASSIMP_BUILD_ASSIMP_TOOLS=OFF
|
||||
-DASSIMP_INSTALL_PDB=OFF
|
||||
-DASSIMP_NO_EXPORT=ON
|
||||
-DASSIMP_BUILD_ALL_IMPORTERS_BY_DEFAULT=OFF
|
||||
-DASSIMP_BUILD_GLTF_IMPORTER=ON
|
||||
-DASSIMP_BUILD_OBJ_IMPORTER=ON
|
||||
-DASSIMP_BUILD_FBX_IMPORTER=ON
|
||||
${_assimp_build_zlib}
|
||||
-DASSIMP_WARNINGS_AS_ERRORS=OFF
|
||||
-DBUILD_WITH_STATIC_CRT=OFF
|
||||
)
|
||||
|
||||
if (MSVC)
|
||||
add_debug_dep(dep_Assimp)
|
||||
endif ()
|
||||
@@ -24,20 +24,6 @@ if (MSVC AND DEP_DEBUG)
|
||||
set(_options "FORWARD_CONFIG")
|
||||
endif ()
|
||||
|
||||
# Boost.Container's bundled dlmalloc passes int* where the Win32 Interlocked API
|
||||
# takes volatile long*; cl compiles that with a warning, clang errors out.
|
||||
set(_boost_c_flags_line "")
|
||||
set(_boost_cxx_flags_line "")
|
||||
if (MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
|
||||
set(_boost_c_flags_line "-DCMAKE_C_FLAGS:STRING=-Wno-incompatible-pointer-types")
|
||||
# The Visual Studio generator applies only the link language's flags to a
|
||||
# project, and boost_container links as C++, so its C file never sees
|
||||
# CMAKE_C_FLAGS. The C++ flags reach every file; keep CMake's defaults.
|
||||
if (CMAKE_GENERATOR MATCHES "Visual Studio")
|
||||
set(_boost_cxx_flags_line "-DCMAKE_CXX_FLAGS:STRING=${CMAKE_CXX_FLAGS} -Wno-incompatible-pointer-types")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
orcaslicer_add_cmake_project(Boost
|
||||
${_options}
|
||||
URL "https://github.com/boostorg/boost/releases/download/boost-1.84.0/boost-1.84.0.tar.gz"
|
||||
@@ -52,8 +38,6 @@ orcaslicer_add_cmake_project(Boost
|
||||
"${_context_abi_line}"
|
||||
"${_context_arch_line}"
|
||||
"${_context_impl_line}"
|
||||
"${_boost_c_flags_line}"
|
||||
"${_boost_cxx_flags_line}"
|
||||
)
|
||||
|
||||
set(DEP_Boost_DEPENDS ZLIB)
|
||||
@@ -55,7 +55,6 @@ endif ()
|
||||
|
||||
set(DEP_DOWNLOAD_DIR ${CMAKE_CURRENT_SOURCE_DIR}/DL_CACHE CACHE PATH "Path for downloaded source packages.")
|
||||
set(FLATPAK FALSE CACHE BOOL "Toggles various build settings for flatpak, like /usr/local in DESTDIR or not building wxwidgets")
|
||||
option(SLIC3R_CAD "Build the SolveSpace solver and OCCT ModelingAlgorithms module the parametric Design/CAD tab needs. Must match the main project's SLIC3R_CAD." ON)
|
||||
|
||||
if ("${DESTDIR}" STREQUAL "" OR "${DESTDIR}" STREQUAL "${AUTOGENERATED_DESTDIR}")
|
||||
if (LINUX AND (NOT DEFINED USE_OLD_DESTDIR_PREV OR USE_OLD_DESTDIR_PREV) AND EXISTS "${CMAKE_BINARY_DIR}/destdir/usr/local" AND NOT EXISTS "${CMAKE_BINARY_DIR}/OrcaSlicer_dep/usr/local")
|
||||
@@ -158,16 +157,8 @@ endif ()
|
||||
function(orcaslicer_add_cmake_project projectname)
|
||||
cmake_parse_arguments(P_ARGS "FORWARD_CONFIG" "INSTALL_DIR;BUILD_COMMAND;INSTALL_COMMAND" "CMAKE_ARGS" ${ARGN})
|
||||
|
||||
# MSVC is true for clang-cl as well, so the sub-build toolchain has to key on the
|
||||
# generator. A non-Visual-Studio superbuild passes its own generator down, and with
|
||||
# it the CMAKE_C_COMPILER / CMAKE_CXX_COMPILER forwarded below.
|
||||
set(_dep_msvc_gen FALSE)
|
||||
if (MSVC AND CMAKE_GENERATOR MATCHES "Visual Studio")
|
||||
set(_dep_msvc_gen TRUE)
|
||||
endif ()
|
||||
|
||||
set(_configs_line -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE})
|
||||
if (_is_multi OR _dep_msvc_gen)
|
||||
if (_is_multi OR MSVC)
|
||||
if (P_ARGS_FORWARD_CONFIG)
|
||||
set(_configs_line -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE})
|
||||
elseif (ORCA_INCLUDE_DEBUG_INFO AND NOT DEP_DEBUG)
|
||||
@@ -183,20 +174,15 @@ function(orcaslicer_add_cmake_project projectname)
|
||||
set(_target_config "Release")
|
||||
endif()
|
||||
|
||||
if (_dep_msvc_gen)
|
||||
if (MSVC)
|
||||
set(_gen CMAKE_GENERATOR "${DEP_MSVC_GEN}" CMAKE_GENERATOR_PLATFORM "${DEP_PLATFORM}")
|
||||
# The toolset picks the compiler here, not the CMAKE_<LANG>_COMPILER
|
||||
# forwarded below, so without it a clang-cl superbuild builds with cl.
|
||||
if (CMAKE_GENERATOR_TOOLSET)
|
||||
list(APPEND _gen CMAKE_GENERATOR_TOOLSET "${CMAKE_GENERATOR_TOOLSET}")
|
||||
endif ()
|
||||
else()
|
||||
set(_gen "")
|
||||
endif()
|
||||
|
||||
if ($ENV{CMAKE_BUILD_PARALLEL_LEVEL})
|
||||
set(_build_j "") # assume environment will control --build parallel setting
|
||||
elseif(_dep_msvc_gen)
|
||||
elseif(MSVC)
|
||||
set(_build_j "/m")
|
||||
else()
|
||||
set(_build_j "-j${NPROC}")
|
||||
@@ -364,11 +350,6 @@ include(GLEW/GLEW.cmake)
|
||||
|
||||
include(GLFW/GLFW.cmake)
|
||||
include(OpenCSG/OpenCSG.cmake)
|
||||
set(SLVS_PKG "")
|
||||
if (SLIC3R_CAD)
|
||||
include(SLVS/SLVS.cmake)
|
||||
set(SLVS_PKG dep_SLVS)
|
||||
endif ()
|
||||
|
||||
include(TBB/TBB.cmake)
|
||||
|
||||
@@ -386,9 +367,6 @@ include(libnoise/libnoise.cmake)
|
||||
|
||||
include(Draco/Draco.cmake)
|
||||
|
||||
include(FFMPEG/FFMPEG.cmake)
|
||||
include(Assimp/Assimp.cmake)
|
||||
|
||||
|
||||
# I *think* 1.1 is used for *just* md5 hashing?
|
||||
# 3.1 has everything in the right place, but the md5 funcs used are deprecated
|
||||
@@ -458,7 +436,6 @@ set(_dep_list
|
||||
dep_NLopt
|
||||
dep_OpenVDB
|
||||
dep_OpenCSG
|
||||
${SLVS_PKG}
|
||||
dep_OpenCV
|
||||
dep_Eigen
|
||||
dep_CGAL
|
||||
@@ -471,8 +448,6 @@ set(_dep_list
|
||||
dep_libnoise
|
||||
dep_python3
|
||||
dep_wxInspector
|
||||
dep_FFMPEG
|
||||
dep_Assimp
|
||||
)
|
||||
|
||||
if (MSVC)
|
||||
|
||||
@@ -56,18 +56,6 @@ else()
|
||||
set(_curl_static ON)
|
||||
endif()
|
||||
|
||||
# curl 7.75's configure probes and code rely on C laxness cl allows but clang
|
||||
# errors on (implicit function declarations, int* vs u_long* in ioctlsocket),
|
||||
# which flips probe results and misconfigures nonblock.c into the AmigaOS
|
||||
# IoctlSocket branch. Relax both diagnostics so the probes behave like cl, and
|
||||
# pin the camel-case probes off since they only "pass" by implicit declaration.
|
||||
set(_curl_c_flags_line "")
|
||||
set(_curl_probe_overrides "")
|
||||
if (MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
|
||||
set(_curl_c_flags_line "-DCMAKE_C_FLAGS:STRING=-Wno-implicit-function-declaration -Wno-incompatible-pointer-types")
|
||||
set(_curl_probe_overrides -DHAVE_IOCTLSOCKET_CAMEL=0 -DHAVE_IOCTLSOCKET_CAMEL_FIONBIO=0)
|
||||
endif ()
|
||||
|
||||
orcaslicer_add_cmake_project(CURL
|
||||
# GIT_REPOSITORY https://github.com/curl/curl.git
|
||||
# GIT_TAG curl-7_75_0
|
||||
@@ -81,8 +69,6 @@ orcaslicer_add_cmake_project(CURL
|
||||
-DBUILD_CURL_EXE:BOOL=OFF
|
||||
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
|
||||
-DCURL_STATICLIB=${_curl_static}
|
||||
"${_curl_c_flags_line}"
|
||||
${_curl_probe_overrides}
|
||||
${_curl_platform_flags}
|
||||
)
|
||||
|
||||
|
||||
@@ -7,7 +7,4 @@ orcaslicer_add_cmake_project(Draco
|
||||
${_options}
|
||||
URL https://github.com/google/draco/archive/refs/tags/1.5.7.zip
|
||||
URL_HASH SHA256=27b72ba2d5ff3d0a9814ad40d4cb88f8dc89a35491c0866d952473f8f9416b77
|
||||
CMAKE_ARGS
|
||||
# The encoder and decoder tools duplicate draco.lib; see deps-windows.cmake.
|
||||
"${DEP_LLD_FORCE_MULTIPLE}"
|
||||
)
|
||||
@@ -7,20 +7,5 @@ orcaslicer_add_cmake_project(Eigen
|
||||
URL https://gitlab.com/libeigen/eigen/-/archive/5.0.1/eigen-5.0.1.zip
|
||||
URL_HASH SHA256=0dbb1f9e3aaad66f352c03227d8c983f6f0b49e0b07e71a7300f4abcc01aee12
|
||||
CMAKE_ARGS "${_eigen_extra_flags}"
|
||||
# Only the headers are consumed here. Everything below builds nothing we
|
||||
# use, and all three enable_language(Fortran): test/CMakeLists.txt:9,
|
||||
# lapack/CMakeLists.txt:6 and blas/testing/CMakeLists.txt:2. They default
|
||||
# to ON because the dependency configures as its own top-level project.
|
||||
#
|
||||
# Whether that probe is harmless depends on what CMake finds. The Visual
|
||||
# Studio generator supports no Fortran, so it finds nothing; clang-cl sits
|
||||
# next to the LLVM toolset's flang, which works. MSVC with Ninja finds
|
||||
# Strawberry Perl's MinGW gfortran instead, which the deps build already
|
||||
# requires for OpenSSL, and hands it the MSVC-style /machine:x64 that
|
||||
# MinGW's ld reads as a missing input file. The configure dies there and
|
||||
# takes the rest of the superbuild with it.
|
||||
-DEIGEN_BUILD_TESTING=OFF
|
||||
-DEIGEN_BUILD_BLAS=OFF
|
||||
-DEIGEN_BUILD_LAPACK=OFF
|
||||
DEPENDS dep_Boost dep_GMP dep_MPFR
|
||||
)
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
set(_conf_cmd ./configure)
|
||||
|
||||
if (MSVC)
|
||||
set(_source_dir "${CMAKE_BINARY_DIR}/dep_FFMPEG-prefix/src/dep_FFMPEG")
|
||||
|
||||
set(PREBUILD_URL_arm64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-07-17-14-28/ffmpeg-n7.0.3-31-g9b6ffd74b5-winarm64-orca-shared-7.0.zip")
|
||||
set(PREBUILD_HASH_arm64 "12f4140279f2f8469885e1b5b2e8be9d788882914c21523cacd56989f3548054")
|
||||
set(PREBUILD_URL_x64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-07-17-14-28/ffmpeg-n7.0.3-31-g9b6ffd74b5-win64-orca-shared-7.0.zip")
|
||||
set(PREBUILD_HASH_x64 "e65916020ddb9ef84b2666dfbcbfc9b1d67f69d15b4a66db53754637bf2d498c")
|
||||
|
||||
ExternalProject_Add(dep_FFMPEG
|
||||
URL ${PREBUILD_URL_${DEPS_ARCH}}
|
||||
URL_HASH SHA256=${PREBUILD_HASH_${DEPS_ARCH}}
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG
|
||||
CONFIGURE_COMMAND ""
|
||||
BUILD_COMMAND ""
|
||||
INSTALL_COMMAND
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory "${_source_dir}/bin" "${DESTDIR}/bin"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory "${_source_dir}/lib" "${DESTDIR}/lib"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory "${_source_dir}/include" "${DESTDIR}/include"
|
||||
)
|
||||
|
||||
else ()
|
||||
if (APPLE)
|
||||
set(_minos_cmd
|
||||
"--extra-cflags=-mmacosx-version-min=${DEP_OSX_TARGET}"
|
||||
"--extra-ldflags=-mmacosx-version-min=${DEP_OSX_TARGET}"
|
||||
)
|
||||
# Static FFmpeg: nothing to bundle into the .app, no rpath handling.
|
||||
# Disable the VideoToolbox/AudioToolbox HW-accel paths: the player decodes
|
||||
# in software (swscale), and the auto-detected HW objects would drag in
|
||||
# system frameworks that the static libs would then depend on.
|
||||
set(_link_cmd --enable-static --disable-shared --disable-videotoolbox --disable-audiotoolbox)
|
||||
if (IS_CROSS_COMPILE)
|
||||
set(_cross_cmd --enable-cross-compile)
|
||||
set(_pic_cmd --enable-pic)
|
||||
if (${CMAKE_SYSTEM_PROCESSOR} MATCHES "x86_64")
|
||||
set(_arch_cmd --arch=arm64)
|
||||
set(_cc_cmd "--cc=clang -arch arm64")
|
||||
else()
|
||||
set(_arch_cmd --arch=x86_64)
|
||||
set(_cc_cmd "--cc=clang -arch x86_64")
|
||||
endif()
|
||||
endif()
|
||||
else ()
|
||||
set(_link_cmd --enable-shared)
|
||||
endif ()
|
||||
|
||||
set(_build_j -j)
|
||||
if(DEFINED ENV{CMAKE_BUILD_PARALLEL_LEVEL})
|
||||
set(_build_j "-j$ENV{CMAKE_BUILD_PARALLEL_LEVEL}")
|
||||
endif()
|
||||
|
||||
ExternalProject_Add(dep_FFMPEG
|
||||
URL https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n7.0.3.tar.gz
|
||||
URL_HASH SHA256=DEEDCABE339165214A3637DF4C86A507AEF0D793CF8774FF68735F4737E8DDBC
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG
|
||||
CONFIGURE_COMMAND ${_conf_cmd}
|
||||
${_cross_cmd}
|
||||
${_pic_cmd}
|
||||
${_arch_cmd}
|
||||
${_cc_cmd}
|
||||
"--prefix=${DESTDIR}"
|
||||
${_link_cmd}
|
||||
${_minos_cmd}
|
||||
--disable-doc
|
||||
--enable-small
|
||||
--disable-outdevs
|
||||
--disable-filters
|
||||
--enable-filter=*null*,afade,*fifo,*format,*resample,aeval,allrgb,allyuv,atempo,pan,*bars,color,*key,crop,draw*,eq*,framerate,*_qsv,*_vaapi,*v4l2*,hw*,scale,volume,test*
|
||||
--disable-protocols
|
||||
--enable-protocol=file,fd,pipe,rtp,udp
|
||||
--disable-muxers
|
||||
--enable-muxer=rtp
|
||||
--disable-encoders
|
||||
--disable-decoders
|
||||
--enable-decoder=*aac*,h264*,mp3*,mjpeg,rv*
|
||||
--disable-demuxers
|
||||
--enable-demuxer=h264,mp3,mov
|
||||
--disable-zlib
|
||||
--disable-avdevice
|
||||
BUILD_IN_SOURCE ON
|
||||
BUILD_COMMAND make ${_build_j}
|
||||
INSTALL_COMMAND make install
|
||||
)
|
||||
|
||||
endif()
|
||||
@@ -8,8 +8,6 @@ orcaslicer_add_cmake_project(NLopt
|
||||
-DNLOPT_GUILE:BOOL=OFF
|
||||
-DNLOPT_SWIG:BOOL=OFF
|
||||
-DNLOPT_TESTS:BOOL=OFF
|
||||
# testopt is built regardless of NLOPT_TESTS; see deps-windows.cmake.
|
||||
"${DEP_LLD_FORCE_MULTIPLE}"
|
||||
)
|
||||
|
||||
if (MSVC)
|
||||
|
||||
@@ -1,20 +1,3 @@
|
||||
diff --git a/adm/cmake/occt_defs_flags.cmake b/adm/cmake/occt_defs_flags.cmake
|
||||
index 00000000..00000001 100644
|
||||
--- a/adm/cmake/occt_defs_flags.cmake
|
||||
+++ b/adm/cmake/occt_defs_flags.cmake
|
||||
@@ -134,7 +134,11 @@
|
||||
set (CMAKE_CXX_FLAGS "-std=c++0x ${CMAKE_CXX_FLAGS}")
|
||||
endif()
|
||||
# Optimize size of binaries
|
||||
- set (CMAKE_SHARED_LINKER_FLAGS "-Wl,-s ${CMAKE_SHARED_LINKER_FLAGS}")
|
||||
+ # clang-cl reports the Clang compiler ID, and OCCT builds shared on Windows,
|
||||
+ # where the MSVC-style linker gets this flag as an argument it does not know.
|
||||
+ if (NOT WIN32)
|
||||
+ set (CMAKE_SHARED_LINKER_FLAGS "-Wl,-s ${CMAKE_SHARED_LINKER_FLAGS}")
|
||||
+ endif()
|
||||
elseif(MINGW)
|
||||
add_definitions(-D_WIN32_WINNT=0x0601)
|
||||
# _WIN32_WINNT=0x0601 (use Windows 7 SDK)
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index d98acc0f..28eb8eb4 100644
|
||||
--- a/CMakeLists.txt
|
||||
@@ -185,32 +168,6 @@ index d98acc0f..28eb8eb4 100644
|
||||
endforeach()
|
||||
|
||||
if (BUILD_SAMPLES_QT)
|
||||
diff --git a/adm/cmake/occt_macros.cmake b/adm/cmake/occt_macros.cmake
|
||||
index 224c96b1..8c94a1c5 100644
|
||||
--- a/adm/cmake/occt_macros.cmake
|
||||
+++ b/adm/cmake/occt_macros.cmake
|
||||
@@ -608,7 +608,7 @@ macro (OCCT_INSERT_CODE_FOR_TARGET)
|
||||
install(CODE "if (\"\${CMAKE_INSTALL_CONFIG_NAME}\" MATCHES \"^([Rr][Ee][Ll][Ee][Aa][Ss][Ee])$\")
|
||||
set (OCCT_INSTALL_BIN_LETTER \"\")
|
||||
elseif (\"\${CMAKE_INSTALL_CONFIG_NAME}\" MATCHES \"^([Rr][Ee][Ll][Ww][Ii][Tt][Hh][Dd][Ee][Bb][Ii][Nn][Ff][Oo])$\")
|
||||
- set (OCCT_INSTALL_BIN_LETTER \"i\")
|
||||
+ set (OCCT_INSTALL_BIN_LETTER \"\")
|
||||
elseif (\"\${CMAKE_INSTALL_CONFIG_NAME}\" MATCHES \"^([Dd][Ee][Bb][Uu][Gg])$\")
|
||||
set (OCCT_INSTALL_BIN_LETTER \"d\")
|
||||
endif()")
|
||||
diff --git a/adm/cmake/occt_toolkit.cmake b/adm/cmake/occt_toolkit.cmake
|
||||
index 550e0e2f..7ac1a3b8 100644
|
||||
--- a/adm/cmake/occt_toolkit.cmake
|
||||
+++ b/adm/cmake/occt_toolkit.cmake
|
||||
@@ -241,7 +241,7 @@
|
||||
else()
|
||||
set (aReleasePdbConf)
|
||||
endif()
|
||||
- install (FILES ${CMAKE_BINARY_DIR}/${OS_WITH_BIT}/${COMPILER}/bin\${OCCT_INSTALL_BIN_LETTER}/${PROJECT_NAME}.pdb
|
||||
+ install (FILES $<TARGET_PDB_FILE:${PROJECT_NAME}>
|
||||
CONFIGURATIONS Debug ${aReleasePdbConf} RelWithDebInfo
|
||||
DESTINATION "${INSTALL_DIR_BIN}\${OCCT_INSTALL_BIN_LETTER}")
|
||||
endif()
|
||||
diff --git a/src/Font/Font_FTFont.cxx b/src/Font/Font_FTFont.cxx
|
||||
index 5ae9899f..0a17372b 100644
|
||||
--- a/src/Font/Font_FTFont.cxx
|
||||
|
||||
@@ -1,31 +1,9 @@
|
||||
# clang-cl cannot emit IGESAppli_GeneralModule.cxx on ARM64
|
||||
# (llvm/llvm-project#62081). cl and clang-cl share an ABI.
|
||||
set(_occt_compiler_args "")
|
||||
if ("${DEPS_ARCH}" STREQUAL "arm64" AND CMAKE_CXX_COMPILER_ID STREQUAL Clang)
|
||||
set(_occt_compiler_args -DCMAKE_C_COMPILER:STRING=cl -DCMAKE_CXX_COMPILER:STRING=cl)
|
||||
endif ()
|
||||
|
||||
if(WIN32)
|
||||
set(library_build_type "Shared")
|
||||
else()
|
||||
set(library_build_type "Static")
|
||||
endif()
|
||||
|
||||
# SLIC3R_CAD (declared in deps/CMakeLists.txt) builds OCCT's ModelingAlgorithms module
|
||||
# (fillet/offset/loft), whose only consumer is the parametric Design/CAD tab. With it OFF
|
||||
# the deps prefix matches upstream exactly.
|
||||
#
|
||||
# With it ON the delta is THREE toolkits, not two: TKFillet (7.40 MiB archive, used via
|
||||
# BRepFilletAPI), TKOffset (5.38 MiB, used via BRepOffsetAPI) and TKFeat (4.42 MiB), which
|
||||
# nothing here references but which the module flag builds anyway -- it is all-or-nothing
|
||||
# per module. The module's other nine toolkits are built either way, because DataExchange
|
||||
# (the STEP path upstream already ships) depends on them.
|
||||
#
|
||||
# On macOS/Linux OCCT links statically, so an unreferenced toolkit costs build time and no
|
||||
# shipped bytes. The Windows figure is a real DLL cost and has NOT been measured -- an
|
||||
# earlier "3.77 MiB, Windows only" note here covered only two of the three toolkits and is
|
||||
# not a number to quote. See docs/cad_dependency_weight.md.
|
||||
|
||||
if (IN_GIT_REPO)
|
||||
set(OCCT_DIRECTORY_FLAG --directory ${BINARY_DIR_REL}/dep_OCCT-prefix/src/dep_OCCT)
|
||||
endif ()
|
||||
@@ -50,10 +28,9 @@ orcaslicer_add_cmake_project(OCCT
|
||||
#-DBUILD_MODULE_DataExchange=OFF
|
||||
-DBUILD_MODULE_Draw=OFF
|
||||
-DBUILD_MODULE_FoundationClasses=OFF
|
||||
-DBUILD_MODULE_ModelingAlgorithms=${SLIC3R_CAD}
|
||||
-DBUILD_MODULE_ModelingAlgorithms=OFF
|
||||
-DBUILD_MODULE_ModelingData=OFF
|
||||
-DBUILD_MODULE_Visualization=OFF
|
||||
${_occt_compiler_args}
|
||||
)
|
||||
|
||||
# add_dependencies(dep_OCCT ${FREETYPE_PKG})
|
||||
|
||||
@@ -10,13 +10,6 @@ else ()
|
||||
set(_options "")
|
||||
endif ()
|
||||
|
||||
# carotene is OpenCV's ARM NEON HAL. It uses M_PI without _USE_MATH_DEFINES
|
||||
# and does not compile with clang-cl.
|
||||
set(_disable_carotene "")
|
||||
if ("${DEPS_ARCH}" STREQUAL "arm64" AND CMAKE_CXX_COMPILER_ID STREQUAL Clang)
|
||||
set(_disable_carotene "-DWITH_CAROTENE=OFF")
|
||||
endif ()
|
||||
|
||||
if (IN_GIT_REPO)
|
||||
set(OpenCV_DIRECTORY_FLAG --directory ${BINARY_DIR_REL}/dep_OpenCV-prefix/src/dep_OpenCV)
|
||||
endif ()
|
||||
@@ -90,6 +83,5 @@ orcaslicer_add_cmake_project(OpenCV
|
||||
-DWITH_PROTOBUF=OFF
|
||||
-DWITH_WIN32UI=OFF
|
||||
-DHAVE_WIN32UI=FALSE
|
||||
${_disable_carotene}
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ if(DEFINED OPENSSL_ARCH)
|
||||
set(_cross_arch ${OPENSSL_ARCH})
|
||||
else()
|
||||
if(WIN32)
|
||||
if("${DEPS_ARCH}" STREQUAL "arm64")
|
||||
if("${CMAKE_GENERATOR_PLATFORM}" STREQUAL "ARM64")
|
||||
set(_cross_arch "VC-WIN64-ARM")
|
||||
else()
|
||||
set(_cross_arch "VC-WIN64A")
|
||||
@@ -17,20 +17,10 @@ else()
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
set(_openssl_msvc_env CC=cl CXX=cl RC=rc CL=/FS)
|
||||
# OpenSSL's perl Configure honors the CC environment variable, but the
|
||||
# VC-WIN64A makefile only works with cl (an unquoted clang-cl path with
|
||||
# spaces, e.g. exported by CLion, silently produces no .obj files and the
|
||||
# lib step fails with LNK1181). Pin the upstream toolchain.
|
||||
# Keep rc.exe resolved from the MSVC developer environment as well. The
|
||||
# absolute Windows SDK path contains spaces and OpenSSL 1.1.1 writes it to
|
||||
# the generated nmake file without quoting, which skips .res generation.
|
||||
# /FS serializes access to OpenSSL's shared generated PDB when cl is
|
||||
# driven through nmake from a Ninja configure step.
|
||||
set(_conf_cmd ${CMAKE_COMMAND} -E env ${_openssl_msvc_env} perl Configure )
|
||||
set(_conf_cmd perl Configure )
|
||||
set(_cross_comp_prefix_line "")
|
||||
set(_make_cmd ${CMAKE_COMMAND} -E env ${_openssl_msvc_env} nmake)
|
||||
set(_install_cmd ${CMAKE_COMMAND} -E env ${_openssl_msvc_env} nmake install_sw )
|
||||
set(_make_cmd nmake)
|
||||
set(_install_cmd nmake install_sw )
|
||||
else()
|
||||
if(APPLE)
|
||||
set(_conf_cmd export MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} && ./Configure -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET})
|
||||
@@ -62,14 +52,6 @@ ExternalProject_Add(dep_OpenSSL
|
||||
CONFIGURE_COMMAND ${_conf_cmd} ${_cross_arch}
|
||||
"--openssldir=${DESTDIR}"
|
||||
"--prefix=${DESTDIR}"
|
||||
# OpenSSL's linux-x86_64 target sets multilib=64, so it installs to
|
||||
# <prefix>/lib64 while every other dep uses <prefix>/lib. CPython's
|
||||
# --with-openssl only ever emits -L<dir>/lib, so it misses the bundled
|
||||
# static libs and silently links the system OpenSSL instead -- which,
|
||||
# against 1.1.1w headers, leaves _ssl.so with an undefined
|
||||
# SSL_get_peer_certificate (removed in OpenSSL 3.x). Pin libdir so the
|
||||
# prefix stays single-layout.
|
||||
"--libdir=lib"
|
||||
${_cross_comp_prefix_line}
|
||||
no-shared
|
||||
no-asm
|
||||
@@ -80,12 +62,6 @@ ExternalProject_Add(dep_OpenSSL
|
||||
INSTALL_COMMAND ${_install_cmd}
|
||||
)
|
||||
|
||||
if (CMAKE_GENERATOR MATCHES "Visual Studio")
|
||||
# OpenSSL builds with cl, but MSBuild runs nmake in this project's toolset
|
||||
# environment, and ClangCL's puts clang's headers first. Use the default.
|
||||
set_target_properties(dep_OpenSSL PROPERTIES VS_PLATFORM_TOOLSET "$(DefaultPlatformToolset)")
|
||||
endif ()
|
||||
|
||||
ExternalProject_Add_Step(dep_OpenSSL install_cmake_files
|
||||
DEPENDEES install
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
if (APPLE)
|
||||
# Only disable NEON extension for Apple ARM builds, leave it enabled for Raspberry PI.
|
||||
set(_disable_neon_extension "-DPNG_ARM_NEON=off")
|
||||
elseif ("${DEPS_ARCH}" STREQUAL "arm64" AND CMAKE_CXX_COMPILER_ID STREQUAL Clang)
|
||||
# libpng's CMake ignores PNG_ARM_NEON on Windows ARM64 and skips the NEON
|
||||
# sources, but pngpriv.h enables NEON anyway.
|
||||
set(_disable_neon_extension "-DCMAKE_C_FLAGS=/DWIN32 /D_WINDOWS /DPNG_ARM_NEON_OPT=0")
|
||||
else ()
|
||||
set(_disable_neon_extension "")
|
||||
endif ()
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
# Replaces the upstream SolveSpaceLib CMakeLists, which builds a demo executable and
|
||||
# has no install rules. The sources themselves are used verbatim.
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
project(SLVS VERSION 3.0)
|
||||
|
||||
add_library(slvs
|
||||
libslvs/constrainteq.cpp
|
||||
libslvs/entity.cpp
|
||||
libslvs/expr.cpp
|
||||
libslvs/system.cpp
|
||||
libslvs/util.cpp
|
||||
libslvs/platform/unixutil.cpp
|
||||
libslvs/lib.cpp
|
||||
libslvs/SolveSpaceSystem.cpp)
|
||||
|
||||
target_compile_features(slvs PUBLIC cxx_std_11)
|
||||
|
||||
# LIBRARY strips the solver core out of the SolveSpace application it was extracted from.
|
||||
target_compile_definitions(slvs PRIVATE -DLIBRARY)
|
||||
if (MSVC)
|
||||
target_compile_definitions(slvs PRIVATE -D_CRT_SECURE_NO_WARNINGS -D_SCL_SECURE_NO_WARNINGS)
|
||||
endif ()
|
||||
|
||||
target_include_directories(slvs
|
||||
PUBLIC $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/libslvs/include>
|
||||
PRIVATE ${PROJECT_SOURCE_DIR}/libslvs)
|
||||
|
||||
# libslic3r is linked into shared targets, so this has to be position independent.
|
||||
set_target_properties(slvs PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
# 2018 code, predating the project's warning settings; it is not ours to clean up.
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
target_compile_options(slvs PRIVATE -w -fno-strict-aliasing)
|
||||
endif ()
|
||||
|
||||
include(CMakePackageConfigHelpers)
|
||||
include(GNUInstallDirs)
|
||||
|
||||
write_basic_package_version_file(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake"
|
||||
VERSION ${PROJECT_VERSION}
|
||||
COMPATIBILITY AnyNewerVersion)
|
||||
|
||||
install(TARGETS slvs
|
||||
EXPORT ${PROJECT_NAME}Targets
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
|
||||
set(ConfigPackageLocation ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})
|
||||
|
||||
install(EXPORT ${PROJECT_NAME}Targets
|
||||
FILE "${PROJECT_NAME}Config.cmake"
|
||||
NAMESPACE ${PROJECT_NAME}::
|
||||
DESTINATION ${ConfigPackageLocation})
|
||||
|
||||
install(FILES
|
||||
${PROJECT_SOURCE_DIR}/libslvs/include/slvs.h
|
||||
${PROJECT_SOURCE_DIR}/libslvs/include/SolveSpaceSystem.h
|
||||
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
|
||||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake"
|
||||
DESTINATION ${ConfigPackageLocation})
|
||||
@@ -1,13 +0,0 @@
|
||||
# libslvs — the geometric constraint solver behind the Design tab's sketch constraints.
|
||||
# Extraction of solvespace.com's libslvs, taken verbatim from JacobStoren/SolveSpaceLib;
|
||||
# only the CMakeLists is ours, because upstream's builds a demo and installs nothing.
|
||||
# GPLv3, compatible with this fork's licence. Self-contained: no external dependencies.
|
||||
orcaslicer_add_cmake_project(SLVS
|
||||
URL https://github.com/JacobStoren/SolveSpaceLib/archive/4d8704523e4bf212fadf5189f92484244f670fea.zip
|
||||
URL_HASH SHA256=1c4bdde9c3c6ef20ea4b50b73601de56769f2eb131b36927d7c6489f102e6c30
|
||||
PATCH_COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/CMakeLists.txt.in ./CMakeLists.txt
|
||||
)
|
||||
|
||||
if (MSVC)
|
||||
add_debug_dep(dep_SLVS)
|
||||
endif ()
|
||||
@@ -1,98 +0,0 @@
|
||||
# Copyright (c) 2020-2021 Intel Corporation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
set(TBB_LINK_DEF_FILE_FLAG ${CMAKE_LINK_DEF_FILE_FLAG})
|
||||
set(TBB_DEF_FILE_PREFIX win${TBB_ARCH})
|
||||
|
||||
# Workaround for CMake issue https://gitlab.kitware.com/cmake/cmake/issues/18317.
|
||||
# TODO: consider use of CMP0092 CMake policy.
|
||||
string(REGEX REPLACE "/W[0-4]" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
|
||||
|
||||
set(TBB_WARNING_LEVEL $<$<BOOL:${TBB_STRICT}>:/W4> $<$<BOOL:${TBB_STRICT}>:/WX>)
|
||||
|
||||
# Warning suppression C4324: structure was padded due to alignment specifier
|
||||
set(TBB_WARNING_SUPPRESS /wd4324)
|
||||
set(TBB_TEST_COMPILE_FLAGS /bigobj)
|
||||
|
||||
if (MSVC_VERSION LESS_EQUAL 1900)
|
||||
# Warning suppression C4503 for VS2015 and earlier:
|
||||
# decorated name length exceeded, name was truncated.
|
||||
# More info can be found at
|
||||
# https://docs.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-1-c4503
|
||||
set(TBB_TEST_COMPILE_FLAGS ${TBB_TEST_COMPILE_FLAGS} /wd4503)
|
||||
endif()
|
||||
|
||||
set(TBB_LIB_COMPILE_FLAGS -D_CRT_SECURE_NO_WARNINGS /GS)
|
||||
set(TBB_COMMON_COMPILE_FLAGS /volatile:iso /FS /EHsc)
|
||||
|
||||
# Ignore /WX set through add_compile_options() or added to CMAKE_CXX_FLAGS if TBB_STRICT is disabled.
|
||||
if (NOT TBB_STRICT AND COMMAND tbb_remove_compile_flag)
|
||||
tbb_remove_compile_flag(/WX)
|
||||
endif()
|
||||
|
||||
if (WINDOWS_STORE OR TBB_WINDOWS_DRIVER)
|
||||
set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} /D_WIN32_WINNT=0x0A00)
|
||||
set(TBB_COMMON_LINK_FLAGS -NODEFAULTLIB:kernel32.lib -INCREMENTAL:NO)
|
||||
set(TBB_COMMON_LINK_LIBS OneCore.lib)
|
||||
endif()
|
||||
|
||||
if (WINDOWS_STORE)
|
||||
if (NOT CMAKE_SYSTEM_VERSION EQUAL 10.0)
|
||||
message(FATAL_ERROR "CMAKE_SYSTEM_VERSION must be equal to 10.0")
|
||||
endif()
|
||||
set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} /ZW /ZW:nostdlib)
|
||||
# CMake define this extra lib, remove it for this build type
|
||||
string(REGEX REPLACE "WindowsApp.lib" "" CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES}")
|
||||
|
||||
if (TBB_NO_APPCONTAINER)
|
||||
set(TBB_LIB_LINK_FLAGS ${TBB_LIB_LINK_FLAGS} -APPCONTAINER:NO)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (TBB_WINDOWS_DRIVER)
|
||||
# Since this is universal driver disable this variable
|
||||
set(CMAKE_SYSTEM_PROCESSOR "")
|
||||
# CMake define list additional libs, remove it for this build type
|
||||
set(CMAKE_CXX_STANDARD_LIBRARIES "")
|
||||
set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} /D _UNICODE /DUNICODE /DWINAPI_FAMILY=WINAPI_FAMILY_APP /D__WRL_NO_DEFAULT_LIB__)
|
||||
endif()
|
||||
|
||||
if (NOT DEFINED TBB_ENABLE_IPO)
|
||||
if (DEFINED CMAKE_INTERPROCEDURAL_OPTIMIZATION)
|
||||
set(TBB_ENABLE_IPO ${CMAKE_INTERPROCEDURAL_OPTIMIZATION})
|
||||
else()
|
||||
set(TBB_ENABLE_IPO ON)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (TBB_ENABLE_IPO)
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES "(Clang|IntelLLVM)")
|
||||
if (CMAKE_SYSTEM_PROCESSOR MATCHES "(x86|AMD64)")
|
||||
set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} -mrtm -mwaitpkg)
|
||||
endif()
|
||||
set(TBB_OPENMP_NO_LINK_FLAG TRUE)
|
||||
set(TBB_IPO_COMPILE_FLAGS $<$<NOT:$<CONFIG:Debug>>:-flto>)
|
||||
else()
|
||||
set(TBB_IPO_COMPILE_FLAGS $<$<NOT:$<CONFIG:Debug>>:/GL>)
|
||||
set(TBB_IPO_LINK_FLAGS $<$<NOT:$<CONFIG:Debug>>:-LTCG> $<$<NOT:$<CONFIG:Debug>>:-INCREMENTAL:NO>)
|
||||
endif()
|
||||
else()
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES "(Clang|IntelLLVM)" AND CMAKE_SYSTEM_PROCESSOR MATCHES "(x86|AMD64)")
|
||||
set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} -mrtm -mwaitpkg)
|
||||
endif()
|
||||
set(TBB_IPO_COMPILE_FLAGS "")
|
||||
set(TBB_IPO_LINK_FLAGS "")
|
||||
endif()
|
||||
|
||||
set(TBB_OPENMP_FLAG /openmp)
|
||||
@@ -1,6 +1,4 @@
|
||||
if (MSVC)
|
||||
set(_patch_command ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/MSVC.cmake ./cmake/compilers/MSVC.cmake)
|
||||
elseif (FLATPAK AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
|
||||
if (FLATPAK AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
|
||||
set(_patch_command ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/GNU.cmake ./cmake/compilers/GNU.cmake)
|
||||
else()
|
||||
set(_patch_command "")
|
||||
@@ -15,8 +13,6 @@ orcaslicer_add_cmake_project(
|
||||
-DTBB_BUILD_SHARED=OFF
|
||||
-DTBB_BUILD_TESTS=OFF
|
||||
-DTBB_TEST=OFF
|
||||
-DTBB_ENABLE_IPO=OFF
|
||||
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF
|
||||
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
|
||||
-DCMAKE_DEBUG_POSTFIX=_debug
|
||||
)
|
||||
|
||||
@@ -42,15 +42,6 @@ else ()
|
||||
message(FATAL_ERROR "Unsupported OS architecture: ${DEPS_ARCH}")
|
||||
endif ()
|
||||
|
||||
# Draco's tools and NLopt's testopt compile sources that are also in their
|
||||
# static library. MSBuild passes the library before the objects and lld-link
|
||||
# resolves as it goes, so the library's copy wins and the object then reads as
|
||||
# a duplicate. Nothing uses those executables, so let lld keep the first one.
|
||||
set(DEP_LLD_FORCE_MULTIPLE "")
|
||||
if (CMAKE_GENERATOR MATCHES "Visual Studio" AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
set(DEP_LLD_FORCE_MULTIPLE "-DCMAKE_EXE_LINKER_FLAGS:STRING=${CMAKE_EXE_LINKER_FLAGS} /FORCE:MULTIPLE")
|
||||
endif ()
|
||||
|
||||
if (${DEP_DEBUG})
|
||||
set(DEP_BOOST_DEBUG "debug")
|
||||
else ()
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Compiles Objects/unicodectype.c without optimisation. VS 2026's ARM64 code
|
||||
generator needs about 27 GB for _PyUnicode_ToNumeric, a switch with 1951
|
||||
cases. CPython has the same workaround (python/cpython#153668). -->
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<ClCompile Update="..\Objects\unicodectype.c">
|
||||
<Optimization>Disabled</Optimization>
|
||||
<WholeProgramOptimization>false</WholeProgramOptimization>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -15,13 +15,7 @@ if(WIN32)
|
||||
# See https://github.com/python/cpython/issues/153438
|
||||
# Patch from https://github.com/python/cpython/pull/153608
|
||||
# This patch has not been merged to 3.12 yet so we need to apply it manually
|
||||
#
|
||||
# Without core.autocrlf=false the patched find_python.bat comes out LF and
|
||||
# cmd.exe cannot find its goto labels.
|
||||
set(_patch_cmd git init
|
||||
&& ${GIT_EXECUTABLE} -c core.autocrlf=false apply --verbose
|
||||
--ignore-space-change --whitespace=fix
|
||||
${CMAKE_CURRENT_LIST_DIR}/01-windows-nuget.patch)
|
||||
set(_patch_cmd git init && ${PATCH_CMD} ${CMAKE_CURRENT_LIST_DIR}/01-windows-nuget.patch)
|
||||
|
||||
if(MSVC_VERSION EQUAL 1800)
|
||||
set(_python_platform_toolset v120)
|
||||
@@ -59,9 +53,12 @@ if(WIN32)
|
||||
set(_python_pcbuild_output_dir win32)
|
||||
endif()
|
||||
|
||||
# pybind11 undefines _DEBUG around Python.h so a debug build links the
|
||||
# release python3xx.lib; Py_DEBUG could not load release plugin modules.
|
||||
set(_python_pcbuild_config Release)
|
||||
set(_python_layout_debug OFF)
|
||||
if(DEFINED DEP_DEBUG AND DEP_DEBUG)
|
||||
set(_python_pcbuild_config Debug)
|
||||
set(_python_layout_debug ON)
|
||||
endif()
|
||||
|
||||
# CPython's PCbuild needs a 64-bit-hosted toolchain: find_msbuild.bat picks the
|
||||
# 32-bit Bin\MSBuild.exe, whose x86 cl.exe/link.exe run out of address space
|
||||
@@ -88,18 +85,8 @@ if(WIN32)
|
||||
list(APPEND _python_env_args "PreferredToolArchitecture=${_python_tool_arch}")
|
||||
endif()
|
||||
|
||||
# MSBuild reads extra switches from PCbuild/msbuild.rsp.
|
||||
set(_python_rsp "/p:PlatformToolset=${_python_platform_toolset}\n")
|
||||
# VS 2026's ARM64 code generator needs about 27 GB for one function in
|
||||
# Objects/unicodectype.c (python/cpython#153668); the property sheet compiles
|
||||
# that file without optimisation.
|
||||
if(_python_pcbuild_platform STREQUAL "ARM64")
|
||||
file(TO_NATIVE_PATH "${CMAKE_CURRENT_LIST_DIR}/arm64-unicodectype.props" _python_arm64_props)
|
||||
string(APPEND _python_rsp "/p:ForceImportAfterCppTargets=\"${_python_arm64_props}\"\n")
|
||||
endif()
|
||||
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/python3-msbuild.rsp" "${_python_rsp}")
|
||||
set(_conf_cmd
|
||||
${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_BINARY_DIR}/python3-msbuild.rsp" <SOURCE_DIR>/PCbuild/msbuild.rsp
|
||||
cmd /c "echo /p:PlatformToolset=${_python_platform_toolset}>PCbuild\\msbuild.rsp"
|
||||
)
|
||||
set(_build_cmd
|
||||
${CMAKE_COMMAND} -E env ${_python_env_args}
|
||||
@@ -114,6 +101,7 @@ if(WIN32)
|
||||
-DPYTHON_BUILD_DIR=<SOURCE_DIR>/PCbuild/${_python_pcbuild_output_dir}
|
||||
-DPYTHON_DEST_DIR=${DESTDIR}/libpython
|
||||
-DPYTHON_LAYOUT_ARCH=${_python_layout_arch}
|
||||
-DPYTHON_DEBUG=${_python_layout_debug}
|
||||
-P ${CMAKE_CURRENT_LIST_DIR}/stage_windows.cmake
|
||||
)
|
||||
elseif(APPLE)
|
||||
|
||||
@@ -9,6 +9,9 @@ foreach(_var PYTHON_SOURCE_DIR PYTHON_BUILD_DIR PYTHON_DEST_DIR PYTHON_LAYOUT_AR
|
||||
endforeach()
|
||||
|
||||
set(_python_exe "${PYTHON_BUILD_DIR}/python.exe")
|
||||
if(PYTHON_DEBUG)
|
||||
set(_python_exe "${PYTHON_BUILD_DIR}/python_d.exe")
|
||||
endif()
|
||||
|
||||
if(NOT EXISTS "${_python_exe}")
|
||||
message(FATAL_ERROR "Built Python executable not found: ${_python_exe}")
|
||||
@@ -46,10 +49,21 @@ endif()
|
||||
set(_required_files
|
||||
"${PYTHON_DEST_DIR}/Lib/encodings/__init__.py"
|
||||
"${PYTHON_DEST_DIR}/include/Python.h"
|
||||
)
|
||||
|
||||
if(PYTHON_DEBUG)
|
||||
list(APPEND _required_files
|
||||
"${PYTHON_DEST_DIR}/python_d.exe"
|
||||
"${PYTHON_DEST_DIR}/python${_python_abi}_d.dll"
|
||||
"${PYTHON_DEST_DIR}/libs/python${_python_abi}_d.lib"
|
||||
)
|
||||
else()
|
||||
list(APPEND _required_files
|
||||
"${PYTHON_DEST_DIR}/python.exe"
|
||||
"${PYTHON_DEST_DIR}/python${_python_abi}.dll"
|
||||
"${PYTHON_DEST_DIR}/libs/python${_python_abi}.lib"
|
||||
)
|
||||
)
|
||||
endif()
|
||||
|
||||
foreach(_required_file IN LISTS _required_files)
|
||||
if(NOT EXISTS "${_required_file}")
|
||||
|
||||
@@ -1,26 +1,3 @@
|
||||
# wxInspector finds wxWidgets through CMake's FindwxWidgets module, which only
|
||||
# searches lib/vc*_lib because _WX_TOOL is hardcoded to "vc". A superbuild driven
|
||||
# by clang-cl installs wxWidgets into lib/clang_x64_lib, so hand the module the
|
||||
# directory wxWidgets actually used, derived the same way wxWidgetsConfig.cmake
|
||||
# derives it.
|
||||
set(_wxinspector_wx_hints "")
|
||||
if (MSVC)
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
set(_wx_compiler_prefix "clang")
|
||||
else ()
|
||||
set(_wx_compiler_prefix "vc")
|
||||
endif ()
|
||||
set(_wx_arch_suffix "")
|
||||
if (CMAKE_GENERATOR_PLATFORM AND NOT CMAKE_GENERATOR_PLATFORM STREQUAL "Win32")
|
||||
string(TOLOWER "_${CMAKE_GENERATOR_PLATFORM}" _wx_arch_suffix)
|
||||
elseif (CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
set(_wx_arch_suffix "_x64")
|
||||
endif ()
|
||||
set(_wxinspector_wx_hints
|
||||
"-DwxWidgets_ROOT_DIR=${DESTDIR}"
|
||||
"-DwxWidgets_LIB_DIR=${DESTDIR}/lib/${_wx_compiler_prefix}${_wx_arch_suffix}_lib")
|
||||
endif ()
|
||||
|
||||
orcaslicer_add_cmake_project(
|
||||
wxInspector
|
||||
URL https://github.com/Noisyfox/wxInspector/archive/refs/tags/v1.0.0.zip
|
||||
@@ -29,7 +6,6 @@ orcaslicer_add_cmake_project(
|
||||
CMAKE_ARGS
|
||||
-DCMAKE_CXX_FLAGS="-DwxDEBUG_LEVEL=0"
|
||||
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
|
||||
${_wxinspector_wx_hints}
|
||||
)
|
||||
|
||||
if (MSVC)
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
diff --git a/src/osx/cocoa/colour.mm b/src/osx/cocoa/colour.mm
|
||||
index 31515d146f..86b33e94a2 100644
|
||||
--- a/src/osx/cocoa/colour.mm
|
||||
+++ b/src/osx/cocoa/colour.mm
|
||||
@@ -125,3 +125,3 @@
|
||||
wxOSXEffectiveAppearanceSetter helper;
|
||||
- if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpaceName:NSCalibratedRGBColorSpace] )
|
||||
+ if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpace:[NSColorSpace sRGBColorSpace]] )
|
||||
return [colRGBA redComponent];
|
||||
@@ -134,3 +134,3 @@
|
||||
wxOSXEffectiveAppearanceSetter helper;
|
||||
- if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpaceName:NSCalibratedRGBColorSpace] )
|
||||
+ if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpace:[NSColorSpace sRGBColorSpace]] )
|
||||
return [colRGBA greenComponent];
|
||||
@@ -143,3 +143,3 @@
|
||||
wxOSXEffectiveAppearanceSetter helper;
|
||||
- if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpaceName:NSCalibratedRGBColorSpace] )
|
||||
+ if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpace:[NSColorSpace sRGBColorSpace]] )
|
||||
return [colRGBA blueComponent];
|
||||
@@ -152,3 +152,3 @@
|
||||
wxOSXEffectiveAppearanceSetter helper;
|
||||
- if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpaceName:NSCalibratedRGBColorSpace] )
|
||||
+ if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpace:[NSColorSpace sRGBColorSpace]] )
|
||||
return [colRGBA alphaComponent];
|
||||
@@ -160,3 +160,3 @@
|
||||
{
|
||||
- return [m_nsColour colorUsingColorSpaceName:NSCalibratedRGBColorSpace] != nil;
|
||||
+ return [m_nsColour colorUsingColorSpace:[NSColorSpace sRGBColorSpace]] != nil;
|
||||
}
|
||||
@@ -21,22 +21,12 @@ else ()
|
||||
set(_wx_edge "-DwxUSE_WEBVIEW_EDGE=OFF")
|
||||
endif ()
|
||||
|
||||
set(_wx_patch_command "")
|
||||
if (APPLE)
|
||||
set(_wx_patch_command
|
||||
${GIT_EXECUTABLE} checkout -f -- src/osx/cocoa/colour.mm
|
||||
COMMAND ${GIT_EXECUTABLE} apply --verbose
|
||||
${CMAKE_CURRENT_LIST_DIR}/0001-macos-use-srgb-colour-components.patch
|
||||
)
|
||||
endif ()
|
||||
|
||||
orcaslicer_add_cmake_project(
|
||||
wxWidgets
|
||||
GIT_REPOSITORY "https://github.com/SoftFever/Orca-deps-wxWidgets"
|
||||
GIT_TAG v3.3.2
|
||||
GIT_SHALLOW ON
|
||||
GIT_SUBMODULES 3rdparty/catch 3rdparty/pcre 3rdparty/libwebp
|
||||
PATCH_COMMAND ${_wx_patch_command}
|
||||
DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} ${JPEG_PKG}
|
||||
CMAKE_ARGS
|
||||
-DwxBUILD_PRECOMP=ON
|
||||
|
||||
@@ -162,7 +162,7 @@ static bool stl_read(stl_file *stl, FILE *fp, int first_facet, bool first, Impor
|
||||
rewind(fp);
|
||||
try{
|
||||
char solid_name[256];
|
||||
int res_solid = fscanf(fp, " solid %255[^\n]", solid_name);
|
||||
int res_solid = fscanf(fp, " solid %[^\n]", solid_name);
|
||||
if (res_solid == 1) {
|
||||
char* mw_position = strstr(solid_name, "MW");
|
||||
if (mw_position != NULL) {
|
||||
@@ -170,7 +170,7 @@ static bool stl_read(stl_file *stl, FILE *fp, int first_facet, bool first, Impor
|
||||
char version_str[16];
|
||||
char model_id_str[128];
|
||||
char country_code_str[16];
|
||||
int num_values = sscanf(mw_position + 3, "%15s %127s %15s", version_str, model_id_str, country_code_str);
|
||||
int num_values = sscanf(mw_position + 3, "%s %s %s", version_str, model_id_str, country_code_str);
|
||||
if (num_values == 3) {
|
||||
if (strcmp(version_str, "1.0") == 0) {
|
||||
model_id = model_id_str;
|
||||
|
||||
@@ -37,11 +37,7 @@ target_include_directories(Clipper2
|
||||
)
|
||||
|
||||
if (WIN32)
|
||||
if (MSVC AND NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
target_compile_options(Clipper2 PRIVATE /W4 /WX)
|
||||
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
|
||||
target_compile_options(Clipper2 PRIVATE /W4)
|
||||
endif()
|
||||
else()
|
||||
target_compile_options(Clipper2 PRIVATE -Wall -Wextra -Wpedantic -Werror)
|
||||
target_link_libraries(Clipper2 PUBLIC -lm)
|
||||
|
||||
@@ -2856,7 +2856,6 @@ const ImWchar* ImFontAtlas::GetGlyphRangesDefault()
|
||||
{
|
||||
0x0020, 0x00FF, // Basic Latin + Latin Supplement
|
||||
0x2000, 0x206F, // General Punctuation
|
||||
0x2103, 0x2103, // ℃ Celsius symbol
|
||||
0x3000, 0x30FF, // CJK Symbols and Punctuations, Hiragana, Katakana
|
||||
0x31F0, 0x31FF, // Katakana Phonetic Extensions
|
||||
0xFF00, 0xFFEF, // Half-width characters
|
||||
|
||||
@@ -465,57 +465,6 @@ static void stb_textedit_click(STB_TEXTEDIT_STRING *str, STB_TexteditState *stat
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, 0);
|
||||
y = r.ymin;
|
||||
}
|
||||
else
|
||||
{
|
||||
// In multi-line mode, clamp y to stay within the text vertical bounds.
|
||||
// This lets the click still land at a valid location if the mouse is slightly
|
||||
// above or below the text.
|
||||
StbTexteditRow r;
|
||||
int n = STB_TEXTEDIT_STRINGLEN(str);
|
||||
int i = 0;
|
||||
float base_y = 0, y_min, y_max;
|
||||
|
||||
// Get the first row to establish y_min and start the iteration
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, 0);
|
||||
if (r.num_chars <= 0)
|
||||
{
|
||||
state->cursor = 0;
|
||||
state->select_start = state->cursor;
|
||||
state->select_end = state->cursor;
|
||||
state->has_preferred_x = 0;
|
||||
return;
|
||||
}
|
||||
y_min = r.ymin;
|
||||
y_max = base_y + r.ymax;
|
||||
i = r.num_chars;
|
||||
base_y += r.baseline_y_delta;
|
||||
|
||||
// Walk the remaining rows to find the bottom of the last row
|
||||
while (i < n)
|
||||
{
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, i);
|
||||
if (r.num_chars <= 0)
|
||||
break;
|
||||
y_max = base_y + r.ymax;
|
||||
i += r.num_chars;
|
||||
base_y += r.baseline_y_delta;
|
||||
}
|
||||
|
||||
// If the text ends with a newline, account for the empty trailing line
|
||||
// so the cursor can be placed on it
|
||||
if (n > 0 && STB_TEXTEDIT_GETCHAR(str, n - 1) == STB_TEXTEDIT_NEWLINE)
|
||||
{
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, n);
|
||||
y_max = base_y + r.ymax;
|
||||
}
|
||||
|
||||
// Subtract half the last line height to avoid rounding issues when the mouse
|
||||
// is just barely below the last line (keep cursor on the last line, not after the text)
|
||||
y_max -= (r.ymax - r.ymin) * 0.5f;
|
||||
|
||||
if (y < y_min) y = y_min;
|
||||
if (y > y_max) y = y_max;
|
||||
}
|
||||
|
||||
state->cursor = stb_text_locate_coord(str, x, y);
|
||||
state->select_start = state->cursor;
|
||||
@@ -536,50 +485,6 @@ static void stb_textedit_drag(STB_TEXTEDIT_STRING *str, STB_TexteditState *state
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, 0);
|
||||
y = r.ymin;
|
||||
}
|
||||
else
|
||||
{
|
||||
// In multi-line mode, clamp y to stay within the text vertical bounds.
|
||||
// This lets the drag keep working if the mouse goes off the top or bottom of the text.
|
||||
StbTexteditRow r;
|
||||
int n = STB_TEXTEDIT_STRINGLEN(str);
|
||||
int i = 0;
|
||||
float base_y = 0, y_min, y_max;
|
||||
|
||||
// Get the first row to establish y_min and start the iteration
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, 0);
|
||||
if (r.num_chars <= 0)
|
||||
return;
|
||||
y_min = r.ymin;
|
||||
y_max = base_y + r.ymax;
|
||||
i = r.num_chars;
|
||||
base_y += r.baseline_y_delta;
|
||||
|
||||
// Walk the remaining rows to find the bottom of the last row
|
||||
while (i < n)
|
||||
{
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, i);
|
||||
if (r.num_chars <= 0)
|
||||
break;
|
||||
y_max = base_y + r.ymax;
|
||||
i += r.num_chars;
|
||||
base_y += r.baseline_y_delta;
|
||||
}
|
||||
|
||||
// If the text ends with a newline, account for the empty trailing line
|
||||
// so the cursor can be placed on it
|
||||
if (n > 0 && STB_TEXTEDIT_GETCHAR(str, n - 1) == STB_TEXTEDIT_NEWLINE)
|
||||
{
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, n);
|
||||
y_max = base_y + r.ymax;
|
||||
}
|
||||
|
||||
// Subtract half the last line height to avoid rounding issues when the mouse
|
||||
// is just barely below the last line (keep cursor on the last line, not after the text)
|
||||
y_max -= (r.ymax - r.ymin) * 0.5f;
|
||||
|
||||
if (y < y_min) y = y_min;
|
||||
if (y > y_max) y = y_max;
|
||||
}
|
||||
|
||||
if (state->select_start == state->select_end)
|
||||
state->select_start = state->cursor;
|
||||
|
||||
@@ -11,8 +11,6 @@ add_library(miniz_static STATIC
|
||||
|
||||
if(${CMAKE_C_COMPILER_ID} STREQUAL "GNU")
|
||||
target_compile_definitions(miniz_static PRIVATE _GNU_SOURCE)
|
||||
elseif (CMAKE_C_COMPILER_ID STREQUAL "Clang" AND CMAKE_C_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
|
||||
target_compile_options(miniz_static PRIVATE /clang:-Wno-error=incompatible-pointer-types)
|
||||
endif()
|
||||
|
||||
target_link_libraries(miniz INTERFACE miniz_static)
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
# Orca-CAD vs Onshape — capability gap analysis
|
||||
|
||||
Generated 2026-07-22 by enumerating the source, not from recollection:
|
||||
`CadFeatureType` and `add_*` in `src/libslic3r/CAD/CadDocument.hpp`, `Tool` in
|
||||
`src/slic3r/GUI/CAD/DesignPanel.hpp`, `Mode` in `src/slic3r/GUI/CAD/DesignSketchTool.hpp`,
|
||||
`SketchConstraintType` + `SketchEntity::Type` in `src/libslic3r/CAD/SketchEngine.hpp`,
|
||||
and the JSON-RPC dispatch in `src/slic3r/GUI/CAD/McpControl.cpp`.
|
||||
|
||||
**Scope note.** Onshape is a cloud PLM platform; Orca is a Design tab inside a
|
||||
slicer. A large share of Onshape's surface (release management, branching, real-time
|
||||
collaboration, FEA, rendering, PDM) is out of scope by construction and is listed
|
||||
separately at the bottom rather than counted as a "missing tool".
|
||||
|
||||
---
|
||||
|
||||
## 1. What Orca already has
|
||||
|
||||
### 2D sketcher — near parity with Onshape
|
||||
This is the strongest area. Very little is missing.
|
||||
|
||||
| Category | Orca |
|
||||
|---|---|
|
||||
| Entities | Line, Polyline, Arc (3-point / tangent / center), Circle (center / 2-point / 3-point), Point, Ellipse, Elliptical arc, B-spline |
|
||||
| Shapes | Rectangle (corner / center / oblique / rounded), Slot, Arc-slot, Polygon |
|
||||
| Edit ops | Fillet, Chamfer, Offset, Mirror, Trim, Extend |
|
||||
| Transforms | Move, Rotate, Scale, Linear array, Polar array |
|
||||
| Constraints (19) | Fix, Coincident, Horizontal, Vertical, Distance, LockX, LockY, EqualLength, Parallel, Perpendicular, Concentric, Tangent, Midpoint, Symmetric, Angle, Radius, Diameter, PointOnLine, PointOnObject |
|
||||
| Dimensions | Length, Diameter, Radius, Angle, Distance, Distance-to-line |
|
||||
|
||||
Solver: vendored SolveSpace (`libslvs`, GPL-3.0) — the same solver lineage as a
|
||||
commercial-grade sketcher.
|
||||
|
||||
### Part features
|
||||
|
||||
| Present | Notes |
|
||||
|---|---|
|
||||
| Extrude | + up-to-face / up-to-point, taper, flip |
|
||||
| Revolve | angle-arc gizmo |
|
||||
| Sweep | along a path |
|
||||
| Loft | multi-profile |
|
||||
| Fillet / Chamfer | edge-level |
|
||||
| Draft | face taper |
|
||||
| Shell | wall thickness + open face |
|
||||
| Hole / Thread | face-aware placement |
|
||||
| Pattern | linear + circular |
|
||||
| Boolean | New / Add / Cut / Intersect, with face-mating |
|
||||
| Cut | plane-based, signed offset |
|
||||
| Datum plane | offset / 2-face / 2-edge derived |
|
||||
| Import | STEP (B-rep) + mesh→B-rep (native mesh2step port) |
|
||||
| Export | STEP (native B-rep, not tessellated) |
|
||||
| Multi-body | + per-body colour |
|
||||
| Section view | with flip |
|
||||
| Undo/redo | full feature-tree recompute |
|
||||
| 3MF persistence | parametric recipe survives save/load |
|
||||
|
||||
### Automation
|
||||
9 MCP JSON-RPC methods: `describe_tools`, `describe_scene`, `query_topology`,
|
||||
`measure`, `slice_body`, `import_step`, `import_mesh`, `validate_against`, plus
|
||||
build actions `extrude`, `revolve`, `fillet`, `chamfer`, `hole`, `boolean`, `pattern`.
|
||||
Onshape's equivalent is its REST API + FeatureScript.
|
||||
|
||||
---
|
||||
|
||||
## 2. Missing tools — ranked by impact
|
||||
|
||||
### Tier 1 — structural absences (whole subsystems)
|
||||
|
||||
**1. Assemblies and mates.** Entirely absent. No assembly document, no mate
|
||||
connectors, no fastened / revolute / slider / cylindrical / planar / ball / pin-slot
|
||||
mates, no assembly patterns, no interference detection, no exploded views.
|
||||
`bool_target_face` / `bool_tool_face` do face-to-face *mating* for a boolean, which
|
||||
is geometric alignment, not a kinematic joint.
|
||||
*Impact:* multi-part products cannot be positioned or validated as a mechanism.
|
||||
*Note:* an MCP-side `align_instance_to_face` / `create_*_mate` vocabulary already
|
||||
exists on the Onshape bridge in this workspace, so the target semantics are known.
|
||||
|
||||
**2. Drawings / 2D documentation.** Absent. No drawing sheets, dimensioned views,
|
||||
section/detail views, GD&T, title blocks, or BOM.
|
||||
*Impact:* nothing manufacturable-by-a-third-party leaves the tool. For 3D printing
|
||||
this matters less than for machining, which is the honest reason it is Tier 1 by
|
||||
CAD convention but arguably Tier 3 for this product.
|
||||
|
||||
**3. Variables, equations, configurations.** Absent — no `add_variable`, no
|
||||
expression evaluation, no configuration table. Every dimension is a literal double.
|
||||
*Impact:* this is the biggest *parametric* gap. "Make this bracket for an M4 vs M5
|
||||
bolt" requires re-editing every dependent feature by hand. Onshape's Variable
|
||||
Studio + configurations are a core differentiator, and this is the cheapest Tier 1
|
||||
item to close for the size of the payoff.
|
||||
|
||||
**4. Surface modelling.** Absent. No surface extrude/revolve/loft/sweep, no fill,
|
||||
knit, trim/extend surface, offset surface, or thicken. Orca is solid-only.
|
||||
*Impact:* organic/complex shapes and repair of imported junk geometry are impossible.
|
||||
OCCT already provides all of it (`TKOffset`, `TKBRep`), so the kernel is not the
|
||||
blocker — only UI and feature plumbing.
|
||||
|
||||
**5. Sheet metal.** Absent. No flange, bend, tab, relief, or flat-pattern unfold.
|
||||
*Impact:* arguably out of scope for an FDM slicer; listed for completeness.
|
||||
|
||||
### Tier 2 — individual features with clear demand
|
||||
|
||||
| Missing | Why it matters | Cheap? |
|
||||
|---|---|---|
|
||||
| **Mirror body** (part-level) | Sketch mirror exists; mirroring a *solid* about a plane does not. Extremely common. | Yes — OCCT `gp_Trsf` mirror + fuse |
|
||||
| **Helix / spiral curve** | No helix ⇒ no springs, no custom threads, no spiral vase geometry. Sweep exists but has no helical path to sweep along. | Yes |
|
||||
| **Move / rotate body as a real feature** | `m_body_xform` exists but is **display-only** (memory #1655) — it never enters the B-rep. Export/boolean see the original position. | Medium |
|
||||
| **Split body** | Cut removes material; splitting one body into two independently-usable bodies is absent. Very relevant for print-in-parts. | Medium |
|
||||
| **Thicken** | Solid from a surface/face offset. | Needs surfaces |
|
||||
| **Rib** | Standard structural feature. | Medium |
|
||||
| **Delete face / move face / replace face** | Direct/dumb-solid editing — the main tool for fixing imported STEP. Given Orca imports STEP *and* meshes, its absence is felt. | Medium |
|
||||
| **Datum axis, coordinate system** | Only datum *planes* exist. Axes are needed for revolve/pattern references. | Yes |
|
||||
| **Mass properties** | `GeometryEngine` computes a volume internally, but there is no volume/mass/COM/inertia readout. For print cost/time estimation this is nearly free to expose. | Yes — trivial |
|
||||
| **Measure tool in the GUI** | `measure` exists over MCP but there is no interactive measure in the UI. | Yes |
|
||||
| **Hole standards library** | Hole exists, but no counterbore/countersink/tapped standards (ISO/ANSI) with callouts. | Medium |
|
||||
| **Project / convert edges into a sketch** | Cannot reference existing solid edges as sketch geometry ("Use" in SolidWorks). A significant sketcher gap given everything else is present. | Medium |
|
||||
| **Construction geometry** | Could not confirm a construction/reference-line flag on sketch entities. | Yes if absent |
|
||||
| **Curve tools** | Projected curve, bridging curve, composite curve, 3D fit spline. | Medium |
|
||||
| **Pattern on curve / pattern faces** | Pattern is linear + circular of whole bodies only; no curve-driven pattern, no feature/face pattern. | Medium |
|
||||
| **Wrap / emboss** | Text or sketch wrapped onto a curved face. | Hard |
|
||||
| **Enclose** | Solid from bounded void regions. | Medium |
|
||||
|
||||
### Tier 3 — platform capabilities (out of scope by construction)
|
||||
|
||||
Version control with branching/merging, release management, real-time multi-user
|
||||
collaboration, cloud PDM, FeatureScript custom-feature authoring, simulation/FEA,
|
||||
photorealistic rendering, app store/integrations. These are Onshape-the-platform,
|
||||
not Onshape-the-modeller. Not defects in Orca.
|
||||
|
||||
---
|
||||
|
||||
## 3. Recommended priority
|
||||
|
||||
If the goal is "credible parametric CAD inside a slicer", the ordering that buys
|
||||
the most capability per unit of work:
|
||||
|
||||
1. **Variables + expressions** — unlocks genuine parametric reuse; no new kernel work.
|
||||
2. **Mass properties + GUI measure** — nearly free, immediately useful for printing.
|
||||
3. **Mirror body, datum axis, helix** — small, self-contained, high-frequency features.
|
||||
4. **Promote move/rotate body from display-only to a real B-rep feature** — closes a
|
||||
correctness gap, not just a missing tool (exports currently disagree with the view).
|
||||
5. **Split body** — high value for print-in-parts workflows.
|
||||
6. **Project edges into sketch** — the sketcher's most conspicuous hole.
|
||||
7. **Surface modelling** — large, but OCCT already ships the algorithms.
|
||||
8. **Assemblies** — largest effort; only worth it if Orca targets multi-part products.
|
||||
|
||||
Deliberately last: drawings and sheet metal — high cost, low relevance to an
|
||||
FDM-oriented tool.
|
||||
|
||||
---
|
||||
|
||||
## 4. Honest summary
|
||||
|
||||
Orca's **sketcher is at or near Onshape parity**, and its **solid feature set
|
||||
covers the mainstream modelling path** (sketch → extrude/revolve/sweep/loft →
|
||||
dress-up → boolean/pattern). What is absent is *breadth*: assemblies, surfaces,
|
||||
sheet metal, drawings, and — most importantly for a tool calling itself parametric —
|
||||
**variables and configurations**.
|
||||
|
||||
The single most defensible criticism is #3: without variables, the feature tree is
|
||||
parametric in *structure* but not in *value*, so the promise of "change one number
|
||||
and the model updates" is only half delivered.
|
||||
@@ -1,136 +0,0 @@
|
||||
# Dependency weight of the Design/CAD subsystem
|
||||
|
||||
What the Design tab actually costs a maintainer who merges it. Written to be checkable:
|
||||
every number below is reproducible with the command that produced it, and the places where
|
||||
a number is still missing say so instead of guessing.
|
||||
|
||||
Measured on Linux x86_64, OCCT V7_6_0, in the `snapmaker-deps` build image.
|
||||
|
||||
## Summary
|
||||
|
||||
| | Cost |
|
||||
|---|---|
|
||||
| New third-party dependencies | **none** |
|
||||
| OCCT build flag | `BUILD_MODULE_ModelingAlgorithms=ON` |
|
||||
| Extra OCCT toolkits *built* | 3 (TKFillet, TKOffset, TKFeat) |
|
||||
| Extra OCCT toolkits *linked* | 2 (TKFillet, TKOffset) |
|
||||
| Vendored code | `src/libslic3r/slvs`, 9,339 lines, 380 KiB, GPLv3 |
|
||||
| Own object code | 6.79 MiB unstripped `.o` (7.13 MiB with the solver) |
|
||||
|
||||
OCCT is **already** an upstream dependency — Orca uses it for STEP import. The Design tab
|
||||
does not add a library; it turns on one more OCCT module.
|
||||
|
||||
## The OCCT module flag
|
||||
|
||||
`deps/OCCT/OCCT.cmake` gates the module on `SLIC3R_CAD`:
|
||||
|
||||
```cmake
|
||||
-DBUILD_MODULE_ModelingAlgorithms=${SLIC3R_CAD} # was hard-coded OFF
|
||||
```
|
||||
|
||||
With `SLIC3R_CAD=OFF` the deps prefix matches upstream exactly.
|
||||
|
||||
`ModelingAlgorithms` contains 12 toolkits, but **most were already being built**, because
|
||||
`DataExchange` — the STEP path upstream already ships — depends on them. The honest delta is
|
||||
only the toolkits that DataExchange's dependency closure does *not* reach:
|
||||
|
||||
```
|
||||
ModelingAlgorithms = TKGeomAlgo TKTopAlgo TKPrim TKBO TKBool TKHLR
|
||||
TKFillet TKOffset TKFeat TKMesh TKXMesh TKShHealing
|
||||
|
||||
already required by DataExchange: TKBO TKBool TKGeomAlgo TKHLR TKMesh
|
||||
TKPrim TKShHealing TKTopAlgo
|
||||
true delta: TKFeat TKFillet TKOffset TKXMesh
|
||||
```
|
||||
|
||||
Reproduce by walking `adm/MODULES` and each toolkit's `src/<TK>/EXTERNLIB` in the OCCT
|
||||
source tree.
|
||||
|
||||
### Sizes of the delta toolkits
|
||||
|
||||
Static archives in the deps prefix. These are *build artifacts*, not shipped bytes — a
|
||||
static link pulls in only the objects it references:
|
||||
|
||||
| Toolkit | Archive | Referenced by the Design tab? |
|
||||
|---|---|---|
|
||||
| TKFillet | 7.40 MiB | yes — `BRepFilletAPI` |
|
||||
| TKOffset | 5.38 MiB | yes — `BRepOffsetAPI`, `BRepOffset_` |
|
||||
| TKFeat | 4.42 MiB | **no** |
|
||||
| TKXMesh | — | not produced at all |
|
||||
|
||||
TKFeat is worth calling out: nothing in the Design tab references it, and it is absent from
|
||||
the `TKFillet`/`TKOffset` dependency closure, so it is built for nothing. OCCT's module flag
|
||||
is all-or-nothing per module, which is why it comes along. It costs build time and zero
|
||||
shipped bytes on any platform that links OCCT statically.
|
||||
|
||||
**A correction to the record.** The comment in `deps/OCCT/OCCT.cmake` and the earlier
|
||||
summary both said the delta was "TKFillet + TKOffset — 3.77 MiB, Windows only". The toolkit
|
||||
list was incomplete: TKFeat is built too. The 3.77 MiB figure covers 2 of the 3 built
|
||||
toolkits and has not been re-derived here — see the gap below.
|
||||
|
||||
## What is not measured yet
|
||||
|
||||
Two numbers a maintainer may reasonably ask for are **not** in this document, because
|
||||
producing them honestly needs a build this machine cannot do:
|
||||
|
||||
1. **Windows DLL delta.** OCCT builds shared on Windows, so the shipped cost there is real
|
||||
DLL bytes rather than linker-selected objects. That needs a Windows build to size —
|
||||
tracked as the cross-platform build proof (`gix`).
|
||||
2. **Clean-build time delta.** Measuring it means building the deps prefix twice, with the
|
||||
flag ON and OFF, on the same machine. The incremental figures from day-to-day work do not
|
||||
answer the question and are not offered as if they did.
|
||||
|
||||
Do not quote a number for either until it has been measured.
|
||||
|
||||
## Vendored solver
|
||||
|
||||
`src/libslic3r/slvs` — the 2D sketch constraint solver extracted from SolveSpace.
|
||||
|
||||
- 19 files: 8 `.cpp`, 11 `.h`, plus `LICENSE`
|
||||
- 9,339 lines, 380 KiB of source, 0.34 MiB of object code
|
||||
- **GPLv3**, `LICENSE` preserved verbatim in the vendored directory
|
||||
|
||||
The fork is **AGPLv3**. GPLv3 code combines into an AGPLv3 work without difficulty: AGPLv3
|
||||
§13 provides explicit compatibility in that direction. No licence question to resolve.
|
||||
|
||||
It is live code, not a carried corpse — `SketchSolver.cpp` is its only consumer and drives
|
||||
every sketch constraint in the Design tab.
|
||||
|
||||
## Own code
|
||||
|
||||
Object sizes from the release build (unstripped, so these include debug information and
|
||||
overstate the shipped contribution):
|
||||
|
||||
| Object | Size |
|
||||
|---|---|
|
||||
| DesignPanel.o | 2.22 MiB |
|
||||
| McpControl.o | 1.69 MiB |
|
||||
| DesignSketchTool.o | 0.88 MiB |
|
||||
| CadDocument.o | 0.76 MiB |
|
||||
| SketchEngine.o | 0.40 MiB |
|
||||
| DesignCanvas.o | 0.37 MiB |
|
||||
| GeometryEngine.o | 0.32 MiB |
|
||||
| SketchSolver.o | 0.15 MiB |
|
||||
| slvs (all objects) | 0.34 MiB |
|
||||
| **total** | **7.13 MiB** |
|
||||
|
||||
For scale, the linked binary is 137.1 MiB.
|
||||
|
||||
## Reproducing
|
||||
|
||||
```bash
|
||||
# toolkit membership and dependency closure
|
||||
R=<occt-source>
|
||||
cat $R/adm/MODULES # module -> toolkits
|
||||
cat $R/src/<TK>/EXTERNLIB # toolkit -> its dependencies
|
||||
|
||||
# archive sizes
|
||||
ls -l <deps-prefix>/lib/libTK{Fillet,Offset,Feat}.a
|
||||
|
||||
# what the Design tab actually references
|
||||
grep -rE 'BRepFilletAPI|BRepOffsetAPI|BRepOffset_|BRepFeat' src/libslic3r/
|
||||
|
||||
# vendored solver
|
||||
wc -l src/libslic3r/slvs/*.cpp src/libslic3r/slvs/**/*.h
|
||||
head -3 src/libslic3r/slvs/LICENSE
|
||||
```
|
||||
@@ -1,704 +0,0 @@
|
||||
# Orca-CAD — UX guidelines and design charter
|
||||
|
||||
Status: proposed, v1. Owner: design working group. Applies to the Design tab —
|
||||
the parametric CAD environment inside OrcaSlicer.
|
||||
|
||||
This document is a **review instrument**, not an essay. Sections 3–9 are written
|
||||
so that a reviewer can hold a pull request against them and get a yes or a no.
|
||||
If a rule here cannot be failed, it is badly written and should be rewritten.
|
||||
|
||||
---
|
||||
|
||||
## 1. Why this exists
|
||||
|
||||
A CAD tool acquires its interface by accretion. Every feature arrives needing
|
||||
"just one more field", the side panel is the cheapest place to put it, and after
|
||||
forty features the product is FreeCAD: complete, respected, and abandoned by
|
||||
almost everyone who opens it once. That end state is not a failure of any single
|
||||
decision. It is the sum of forty locally reasonable ones taken without a written
|
||||
rule to violate.
|
||||
|
||||
So we write the rule down first, and we make additions argue against it.
|
||||
|
||||
## 2. Product thesis
|
||||
|
||||
**Orca-CAD is a modelling space for people who want a part, inside the tool that
|
||||
prints it.**
|
||||
|
||||
Three audiences, one interface:
|
||||
|
||||
- **The fourteen-year-old on a school laptop.** Free software, on the machine
|
||||
they already have, with no account, no subscription, no licence and no
|
||||
tutorial. They open the tab because they want a bracket for a bike light, and
|
||||
an hour later it is printing. This is not the charity case at the bottom of
|
||||
the list — it is the reason the project is worth doing. A CAD tool that only
|
||||
the equipped can run is a tool for people who were already going to design
|
||||
something; this one has to be a creative instrument in the hands of someone
|
||||
who did not yet know they could make things. Everything in §6.1 exists to
|
||||
keep that door open, and nothing gets to close it for the convenience of the
|
||||
other two audiences.
|
||||
- **The maker** who has an idea and a printer, and who has bounced off FreeCAD.
|
||||
They should be modelling something real within ten minutes of first opening
|
||||
the tab, without a tutorial, without knowing the word "constraint".
|
||||
- **The mechanical designer** who needs assemblies, mates, exploded views,
|
||||
variables, and a feature history they can edit six months later. They should
|
||||
not have to leave for SolidWorks the moment the work gets serious.
|
||||
|
||||
The order matters. When a decision helps one audience and hurts another, the
|
||||
earlier one wins unless there is a written argument for why not.
|
||||
|
||||
The reference for *how it feels* is Shapr3D: direct, gestural, quiet, almost no
|
||||
chrome, depth revealed by what you touch rather than by what is on screen. The
|
||||
anti-references are Blender (a modal keyboard language you must learn before the
|
||||
first success) and FreeCAD (a workbench-and-dialog architecture where the
|
||||
geometry is a preview of a form you fill in elsewhere).
|
||||
|
||||
We are not cloning Shapr3D's feature set. We are adopting its *interaction
|
||||
economy*: the smallest number of visible controls that still makes an expert
|
||||
fast.
|
||||
|
||||
**And one thing neither reference has:** Orca-CAD lives inside a slicer. The
|
||||
plate, the nozzle, the material and the print constraints are known to the
|
||||
application at design time. Designing for print is not a plugin here, it is the
|
||||
home advantage. Where a rule below trades generality for print-awareness, it
|
||||
trades in favour of print-awareness.
|
||||
|
||||
## 3. The laws
|
||||
|
||||
Non-negotiable. A change that breaks one of these does not get merged on the
|
||||
grounds that it was easier, that the alternative is more work, or that another
|
||||
CAD does it that way. Each law carries a test — the question a reviewer asks.
|
||||
|
||||
### L1 — Geometry first: you point, then you act
|
||||
|
||||
Controls live **on the geometry**: handles, arrows, points, small circles and
|
||||
boxes, with an inline label tab for typed values. Not in a side panel of combos
|
||||
and spin fields.
|
||||
|
||||
The canonical gesture: **select a face or plane in the viewport, then click the
|
||||
sketch tool.** Never: click the sketch tool, then choose a plane from a list.
|
||||
The tool consumes what you pointed at — and, better still, the thing you pointed
|
||||
at offers the tool itself (§4).
|
||||
|
||||
> **Test.** Can the operation be performed start to finish without the pointer
|
||||
> leaving the viewport, except to press the tool itself? If a control had to be
|
||||
> added to a panel to make it work, the design is not finished.
|
||||
|
||||
This is the law the others serve. It was stated after two proposals in a row
|
||||
reached for a dropdown, and the failure mode it names is real and recurrent: a
|
||||
fix that "adds a row to the plane combo" is the side-panel pattern wearing a
|
||||
different hat.
|
||||
|
||||
### L2 — Everything draggable is typable, and everything typable is draggable
|
||||
|
||||
Any value produced by direct manipulation (a fillet radius, an extrude depth, a
|
||||
pattern spacing, a plane offset) shows a live label on the geometry, and that
|
||||
label is an editable field. Any value entered numerically has a corresponding
|
||||
handle in the viewport.
|
||||
|
||||
Dragging is for finding the answer. Typing is for committing to it. A tool that
|
||||
offers only one of the two is half a tool.
|
||||
|
||||
> **Test.** Point at the number the tool produces. Can you drag it? Can you
|
||||
> click it and type? Both must be yes.
|
||||
|
||||
### L3 — Noun then verb, always the same way round
|
||||
|
||||
Selection precedes action, without exception, across sketch tools, features,
|
||||
dress-up, booleans and mates. There is no tool in the product that is armed
|
||||
first and asks for its input afterwards.
|
||||
|
||||
> **Test.** Does this tool work if the user has already selected the thing they
|
||||
> want it applied to? Does it work *only* that way?
|
||||
|
||||
### L4 — No modal dialog in the modelling loop
|
||||
|
||||
Dialogs belong to document-level actions: open, save, import, export, preferences.
|
||||
Modelling never opens one. A feature that needs three values gets three labels on
|
||||
the geometry, not a form; a feature that needs confirming gets a ghost preview and
|
||||
a confirm/cancel puck in the scene beside it (§4.2) — an object, not a window: the
|
||||
camera still orbits, the values are still editable, nothing is blocked.
|
||||
|
||||
> **Test.** Between starting an operation and seeing its result, does a window
|
||||
> appear that must be dismissed? If yes, redesign.
|
||||
|
||||
### L5 — One click, one visible change
|
||||
|
||||
Every click either changes what is on screen or tells the user why it did not.
|
||||
A click that opens something invisible, arms an invisible state, or requires a
|
||||
second identical click to have any effect is a defect, not a design.
|
||||
|
||||
This law exists because we shipped its violation twice. Sketch-tool family
|
||||
buttons were flyouts whose first click only rendered a pressed state — three
|
||||
separate sessions filed bugs against tools that were working. Solid picking used
|
||||
a click *cycle* (first click selects the body, second refines to the face), so
|
||||
sketching on a face appeared broken to anyone who clicked a face once, the way
|
||||
every human does.
|
||||
|
||||
> **Test.** Perform the gesture exactly once, as a first-time user would. Take a
|
||||
> screenshot. Is the state visibly different, and is the difference the one the
|
||||
> user intended?
|
||||
|
||||
### L6 — The default is the answer four times out of five
|
||||
|
||||
Every option that has a default must have the *common* answer as its default,
|
||||
measured against real parts, not against generality. "New body" as the default
|
||||
result of an extrude is wrong: most extrudes join. Radius as the input for a
|
||||
circle is wrong: drawings give diameter.
|
||||
|
||||
> **Test.** Take ten real parts. In how many is the default correct? Below eight,
|
||||
> change the default or infer it from context.
|
||||
|
||||
### L7 — Errors are caught before the commit, in the user's words
|
||||
|
||||
A self-intersecting profile, a cut that removes no material, a wall thinner than
|
||||
the nozzle: these are reported at the moment they become knowable, on the
|
||||
geometry that is wrong, phrased as what happened and what to do — not as a kernel
|
||||
exception after the fact, and never silently.
|
||||
|
||||
> **Test.** Is the failure detectable before the user commits? Then it must be
|
||||
> reported before the user commits. Read the message aloud: does it name a thing
|
||||
> the user can see and an action they can take?
|
||||
|
||||
### L8 — The camera is the application's job
|
||||
|
||||
Selecting a sketch plane orients the view to it. Committing a feature does not
|
||||
throw the camera away. Zoom-to-fit exists and is one keystroke. The user is never
|
||||
required to fight the view in order to reach the geometry, and orbit is bound to
|
||||
the gesture people actually try.
|
||||
|
||||
> **Test.** Count camera manipulations in a representative modelling session.
|
||||
> Any camera action the application could have performed for the user is a bug.
|
||||
|
||||
### L9 — Accessible by construction, not by retrofit
|
||||
|
||||
The floor, applied to every new interaction (details in §6.2): full keyboard
|
||||
reach, no meaning carried by colour alone, hit targets that survive a shaky hand
|
||||
and a HiDPI screen, legible labels over an arbitrary 3D background, no gesture
|
||||
that depends on timing.
|
||||
|
||||
> **Test.** Drive the whole interaction from the keyboard. Then drive it in
|
||||
> greyscale. Both must work.
|
||||
|
||||
### L10 — Vocabulary from the drawing office
|
||||
|
||||
Names come from the language of people who make parts: fillet, chamfer, boss,
|
||||
rib, counterbore, mate, exploded view. Not from the kernel (no "boolean
|
||||
subtract", no "B-rep"), not from invented product-speak. Where the drawing-office
|
||||
word and the beginner's word differ, use the drawing-office word and make the
|
||||
tooltip teach it — an approachable tool that leaves the user unable to talk to a
|
||||
machinist has failed them.
|
||||
|
||||
> **Test.** Would a shop-floor engineer recognise this word? Would a first-time
|
||||
> user be able to look it up and find a real definition?
|
||||
|
||||
### L11 — The floor is a school laptop, and nothing is behind a door
|
||||
|
||||
The product runs, completely, on a low-end laptop with integrated graphics and a
|
||||
small screen, offline, with no account, no subscription and no feature withheld.
|
||||
No capability in this document is reserved for a paid tier, a cloud service, a
|
||||
plugin, or a machine with a discrete GPU — there is one product and everybody
|
||||
gets all of it.
|
||||
|
||||
> **Test.** On the reference low-end machine (§6.1), at 1366×768, with the
|
||||
> network cable pulled and no account ever created: does this feature work, and
|
||||
> is it usable at an honest frame rate? Any "no" is a defect, not a limitation.
|
||||
|
||||
## 4. Interaction grammar — object-driven
|
||||
|
||||
The rules above compose into one sentence the whole product obeys:
|
||||
|
||||
> **Point at geometry → the geometry offers what can be done to it → choose the
|
||||
> tool → manipulate handles and type exact values → confirm or cancel.**
|
||||
|
||||
The selection does not merely feed the tool. **The selection determines which
|
||||
tools exist.** Pick a planar face and the product shows you the small set of
|
||||
things a planar face can become — sketch on it, extrude it, hole it, shell it,
|
||||
put a datum on it. Pick an edge and that set is fillet, chamfer, and the sketch
|
||||
tools that can use it as a reference. Nothing else is offered, because nothing
|
||||
else is possible.
|
||||
|
||||
This is the single largest thing we can do for a first-time user, and it is
|
||||
worth stating as the reason: a beginner's difficulty is not operating a tool,
|
||||
it is **not knowing which tools apply to what they are looking at**. A palette
|
||||
of sixty icons answers a question they cannot yet ask. A face that offers its
|
||||
own five verbs teaches the model of the product by using it. It also removes an
|
||||
entire class of failure — a tool that silently does nothing because the
|
||||
selection was wrong can no longer be reached.
|
||||
|
||||
### 4.1 The offer, and the one thing that makes it work
|
||||
|
||||
The flow, in full:
|
||||
|
||||
> **left-click the geometry to select it → right-click to open the offer → a
|
||||
> vertical list, always in the same order, each row an icon, a name and its
|
||||
> keyboard shortcut → click.**
|
||||
|
||||
- **Selecting and acting are separate gestures.** Left-click only ever selects,
|
||||
so pointing at things is quiet — nothing pops up while you look around.
|
||||
Right-click on the selection opens the offer, at the pointer, over the
|
||||
geometry it acts on.
|
||||
- **Order is fixed and it is the whole point.** A verb occupies one permanent
|
||||
row, and that row is the same in every selection where the verb appears.
|
||||
Dress-up is the fourth row on an edge, on a face, on a body, on the day the
|
||||
product ships and two years later. The hand learns the position; the eye stops
|
||||
being needed.
|
||||
- **What does not apply is DISABLED IN PLACE, never removed.** This is the
|
||||
single strongest thing the list does, and it is why it beat the radial we
|
||||
drew first: a greyed row still carries its name *and the reason it is grey* —
|
||||
"Create a sketch, or pick a solid face, first", "Create a solid body to
|
||||
pattern first" — in the words the product already ships. On a first-run
|
||||
document the offer is therefore not a mostly-empty control but a map of what
|
||||
the product does and what you have to do first.
|
||||
- **It is an accelerator, not a toll gate.** The toolbar and the single-letter
|
||||
shortcuts keep working exactly as they do now, and pressing a tool directly
|
||||
consumes the same selection (L3). An expert never has to open the offer; a
|
||||
beginner never has to know the toolbar exists. Both routes land in the same
|
||||
place — this is the only way one interface serves §2's three audiences.
|
||||
- **Every row shows its keyboard shortcut**, right-aligned so the keys stack
|
||||
into a column the eye learns without trying, beside the icon and the
|
||||
drawing-office word (L10). This is deliberate: the offer is the path by which
|
||||
a user stops needing the offer. You reach for fillet in its row, the row says
|
||||
"F", and one day your hand types F before the menu has finished opening. A
|
||||
menu that teaches its own shortcut is how a beginner becomes the power user
|
||||
who never opens it — the same interface at two speeds, with no "advanced mode"
|
||||
between them (§7).
|
||||
- **A family with more than one applicable verb opens a submenu** to the side,
|
||||
in its own fixed order. A family with exactly one shows that verb directly, so
|
||||
the common path is never one click longer than it needs to be.
|
||||
- **It never blocks the view of what it acts on**: it opens beside the pick,
|
||||
never over it, with a thin leader back to the point it belongs to, and it
|
||||
dismisses the moment the selection changes.
|
||||
- **The header names what is selected** ("Top face · Body 1"), because a user
|
||||
who mis-picked should find that out before choosing a verb, not after.
|
||||
|
||||
#### Opening the offer on every machine
|
||||
|
||||
Right-click is the primary gesture and every platform must have a first-class
|
||||
equivalent — this is a reach requirement (L11), not a nicety:
|
||||
|
||||
| Input | Gesture |
|
||||
|---|---|
|
||||
| Two-button mouse | right-click |
|
||||
| Trackpad | two-finger tap (the OS-standard secondary click) |
|
||||
| macOS, one-button mouse | **long-press**, and Ctrl-click, which is the platform convention |
|
||||
| Keyboard | the Menu key, or Shift+F10, on the current selection |
|
||||
| Touch / pen | long-press |
|
||||
|
||||
The long-press is an **additional** route, never the only one — §6.2 forbids
|
||||
press-and-hold as a sole path to a function, and it stays forbidden. Every
|
||||
opening gesture is reachable at least two ways on every platform, and the
|
||||
keyboard route exists everywhere. A long-press must show that it is charging
|
||||
(a growing ring under the finger) so a user who holds too briefly learns why
|
||||
nothing happened rather than concluding the product is broken (L5).
|
||||
|
||||
#### The row-constancy invariant
|
||||
|
||||
This is the rule that has to survive every future feature, so it is written as
|
||||
an invariant rather than as advice:
|
||||
|
||||
> **Every verb has exactly one row index in the offer. That index is identical
|
||||
> for every selection type in which the verb appears. Verbs that do not apply to
|
||||
> the current selection are DISABLED IN PLACE, with their reason — the offer is
|
||||
> never compacted, re-sorted or re-ordered. Adding a verb never changes the
|
||||
> index of an existing one.**
|
||||
|
||||
Two consequences the group must accept together with the invariant:
|
||||
|
||||
- **No adaptive ordering. Ever.** Not most-used-first, not recently-used-first,
|
||||
not per-selection frequency. An offer that rearranges itself to be helpful
|
||||
destroys the only thing that made it fast, and it does so precisely for the
|
||||
user who has just started to learn it. (Office 2000's adaptive menus are the
|
||||
textbook case; they were removed.)
|
||||
- **Greyed rows are the price, and they are cheap.** A compacted menu is shorter
|
||||
and unlearnable. A constant one is a few rows longer, teaches while it waits,
|
||||
and is memorised in a week.
|
||||
|
||||
#### The map — RATIFIED 2026-07-31
|
||||
|
||||
The invariant is not negotiable, and as of 2026-07-31 neither is the assignment:
|
||||
the row order below is **ratified**. It was argued once; it is not argued again.
|
||||
Changing an index from here on is a breaking change to every user's muscle
|
||||
memory and needs the group, not a pull request (§9 q12).
|
||||
|
||||
Eight families, ordered so the sequence itself has a logic: material is created,
|
||||
grows, is taken away, is refined, is repeated, is moved, is referred to, is
|
||||
edited.
|
||||
|
||||
| Row | Family | On a face | On an edge | On a body | On text/art |
|
||||
|---|---|---|---|---|---|
|
||||
| **1** | Create | Sketch on it | — | — | Edit text |
|
||||
| **2** | Add material | Extrude, thicken | — | Combine, thicken | Extrude |
|
||||
| **3** | Remove | Hole, shell | Thread | Shell, cut, split | — |
|
||||
| **4** | Dress-up | Draft | Fillet, chamfer | Fillet, chamfer | — |
|
||||
| **5** | Repeat | Pattern | Pattern along it | Pattern, mirror | Pattern |
|
||||
| **6** | Transform | Align to, mate | — | Move, mate | Move, size |
|
||||
| **7** | Reference | Plane, axis, measure | Axis, measure | Project, measure, mass | — |
|
||||
| **8** | Modify | Delete face, edit | — | Edit, colour, delete | Replace art |
|
||||
|
||||
A dash means the row is drawn greyed for that selection, with its reason.
|
||||
|
||||
The authoritative version of this table is **`docs/ux/tool_atlas.json`**, which
|
||||
carries all 52 verbs with their preconditions and their refusal strings, taken
|
||||
from the code rather than from memory. Every state it produces — 20 selection
|
||||
kinds × 2 document states, 40 primary menus and 73 submenus — is rendered by
|
||||
`docs/ux/mockups/gen_offer_mockups.py` into `docs/ux/offer_atlas.html`. Read the
|
||||
atlas before proposing a change to the map; the generator refuses to render an
|
||||
address collision, so the map cannot silently rot.
|
||||
|
||||
#### Rejected: the radial ring
|
||||
|
||||
The first design put the eight families at eight compass points around the pick.
|
||||
It is recorded here because it is a good idea that loses on evidence, and
|
||||
someone will propose it again:
|
||||
|
||||
- an inapplicable slot could only be drawn empty, and **an empty slot says
|
||||
nothing** — the reason text above has nowhere to live;
|
||||
- the measured fill was **3.45 of 8 slots**, so most of the control was blank
|
||||
most of the time, and on a fresh document only two of eight were live;
|
||||
- sketch-mode *Create* needs **nine** addresses; eight forced two primitives
|
||||
behind a "More" slot, and a ninth position costs the 45° spacing that made the
|
||||
ring worth having;
|
||||
- long translated names do not fit around a circle, and screen readers and arrow
|
||||
keys need bespoke handling a list gets for free;
|
||||
- a 380 px disc over the model costs more on a 1366×768 screen than a 324 px
|
||||
list beside it (§6.1).
|
||||
|
||||
What it kept — equidistant targets and a future flick gesture — buys little in a
|
||||
product whose experts live on the keyboard by design.
|
||||
|
||||
### 4.2 Confirm and cancel are objects, not gestures
|
||||
|
||||
The old rule — click empty space to commit — is withdrawn. It was an invisible
|
||||
gesture with a destructive meaning: nothing on screen said it, and a stray click
|
||||
committed a feature the user was still adjusting. That is exactly what L5
|
||||
forbids, and it is hostile to the audience §6.1 exists for.
|
||||
|
||||
- **A pending feature carries a confirm/cancel puck**, attached to the geometry
|
||||
it is editing, next to its handles: ✓ commits, ✗ discards. Enter and Escape
|
||||
mirror them for the keyboard (L9). It is drawn where the user's attention
|
||||
already is, and it is the only thing in the viewport that commits.
|
||||
- **Empty space now means "clear the selection"** — the safe meaning, and the
|
||||
same meaning everywhere.
|
||||
- **This is not a dialog** (L4). It is two objects in the scene, on the
|
||||
geometry, non-modal: the camera still orbits, the tree is still there, the
|
||||
values are still editable while it waits.
|
||||
- **Continuous tools do not ask.** Drawing a line, a rectangle, a circle commits
|
||||
each entity as its own gesture completes — a ✓ per line would destroy the
|
||||
inner loop. The puck belongs to *features* (extrude, fillet, hole, pattern,
|
||||
mate) and to sketch edits that hold a pending state. Enter/Escape end a
|
||||
continuous tool rather than confirming an entity.
|
||||
- **Ambiguity resolves toward keeping work, never toward losing it.** Starting
|
||||
another operation while a valid feature is pending commits it rather than
|
||||
discarding it; if it is not valid, the product says why (L7) and keeps it
|
||||
pending. Since undo reaches everything (§6.1), the recoverable direction is
|
||||
always the right default.
|
||||
|
||||
### 4.3 The rest of the grammar
|
||||
|
||||
- **The status line is one imperative sentence** naming what the tool wants
|
||||
next, and it names the target when the target came from a selection
|
||||
("Circle — click centre, then radius · on the picked face"). It is the
|
||||
authoritative feedback surface for the armed tool; the toolbar is not.
|
||||
- **Hover previews, click commits.** A hover shows the ghost of what a click
|
||||
would do wherever this is cheap to compute.
|
||||
- **Selection is persistent and visible** until consumed or cleared. A tool that
|
||||
consumes a selection clears it, so the next feature cannot silently inherit it.
|
||||
- **Every gesture is undoable**, and the feature tree is editable history, not a
|
||||
log. Re-editing a feature re-enters the same on-geometry interaction that
|
||||
created it — including its offer and its puck.
|
||||
|
||||
## 5. Layout and screen budget
|
||||
|
||||
The viewport is the application. Chrome is a tax on it.
|
||||
|
||||
- **One toolbar**, contextual to the mode (model / sketch). Tools are grouped by
|
||||
what they make, not by which subsystem implements them.
|
||||
- **A left rail for the document, not for parameters**: feature tree, bodies,
|
||||
variables. It answers "what exists", never "what value should this be".
|
||||
- **No parameter panel.** Where one exists today it is technical debt with a
|
||||
scheduled removal (§10).
|
||||
- **Print context is ambient**, not a panel: the plate is visible in the design
|
||||
space, and print-domain warnings appear on the geometry that will fail.
|
||||
- **Nothing is added to permanent chrome without removing something**, or
|
||||
demonstrating that the addition is used in the majority of sessions.
|
||||
- **The budget is set by the smallest screen we serve**, 1366×768 (§6.1) — not
|
||||
by the reviewer's monitor. Chrome that fits a 27-inch display and swallows a
|
||||
laptop's has not fitted, it has just failed somewhere the author cannot see.
|
||||
|
||||
## 6. Accessibility — reach first, then the assistive floor
|
||||
|
||||
"Accessible" means two different things and the product owes both. §6.1 is about
|
||||
**who can get in at all**; §6.2 is about **who can operate it once inside**.
|
||||
Neither is a phase. Both are merge requirements.
|
||||
|
||||
### 6.1 Reach — the door has to be open
|
||||
|
||||
The premise of the whole project: someone with no money, no licence, no account,
|
||||
no fast machine and no teacher can open this and make a real thing. Free
|
||||
software on a school laptop is the only path to a CAD tool that reaches people
|
||||
who were never going to be handed one. If a design decision quietly raises the
|
||||
cost of entry, it has broken the premise, however elegant it is.
|
||||
|
||||
- **The reference machine.** A 5-year-old laptop: dual/quad-core CPU,
|
||||
**integrated graphics**, 8 GB RAM, **1366×768** screen, no discrete GPU. The
|
||||
Design tab must be usable there, and any interaction that needs more is a
|
||||
design failure to be solved, not a requirement to be documented. The GPU path
|
||||
degrades gracefully to software rendering rather than refusing to start; the
|
||||
viewport stays interactive while the kernel thinks.
|
||||
- **1366×768 is the layout target, not the stretch case.** A form-heavy side
|
||||
panel is not merely inelegant on that screen — it takes the model off it.
|
||||
This is the second, independent argument for the whole of L1 and §5.
|
||||
- **No account, no cloud, no connection.** The product works forever with the
|
||||
network unplugged. Nothing is uploaded, no sign-in gates any feature, no
|
||||
telemetry is required to use it. A school network that blocks everything must
|
||||
not be able to block this.
|
||||
- **No tier, no plugin wall, no "pro".** Every feature named in this document is
|
||||
in the product everyone downloads. Assemblies and exploded views are not the
|
||||
paid half.
|
||||
- **Files belong to the user**, on their disk, in a format that outlives the
|
||||
project: the design travels inside the ordinary project file, and the geometry
|
||||
exports to STEP and mesh formats anyone can open.
|
||||
- **Learnable without instruction.** The first solid comes with no
|
||||
documentation, no video and no tutorial mode — from noticing that a face can
|
||||
be clicked. Tooltips teach the vocabulary (L10) at the moment it is needed;
|
||||
nothing is explained in a manual the user will never open.
|
||||
- **Plain language at the entry tier.** The Make tier speaks in words a
|
||||
thirteen-year-old reads without stopping. Precision comes with the tier that
|
||||
needs it, and everything is translated, because "accessible" in English only
|
||||
is not accessible.
|
||||
- **Exploration must be free.** Undo reaches everything, work is never lost to a
|
||||
wrong click, and no dialog ever asks the user to be sure. A tool that punishes
|
||||
experiments teaches people to stop experimenting, which is the one thing this
|
||||
audience cannot afford to learn.
|
||||
- **The product never blames the user.** Failures are stated as what happened
|
||||
and what to do (L7). "Invalid input" is not an acceptable sentence anywhere.
|
||||
|
||||
### 6.2 Assistive floor
|
||||
|
||||
- **Keyboard**: every operation reachable and completable without a pointer.
|
||||
Single-letter shortcuts for sketch tools, shown in the offer itself (§4.1) as
|
||||
well as in the tooltip. The offer opens from the keyboard (Menu key or
|
||||
Shift+F10) and walks by arrow key and by type-ahead, so the row map works for
|
||||
someone who never touches the pointer. A visible focus state on every
|
||||
focusable element. No shortcut that only works while the pointer happens to be
|
||||
over the canvas.
|
||||
- **Colour**: never the sole carrier of meaning. Selection is colour *and*
|
||||
outline; an error is colour *and* an icon *and* text. Verify in greyscale.
|
||||
- **Contrast**: labels over the 3D viewport get a scrim or halo so 4.5:1 holds
|
||||
against any background the model can produce, including a white body under a
|
||||
white plate.
|
||||
- **Targets**: handles and grips no smaller than 32 px at 100 % scale, scaling
|
||||
with the OS factor; the grab tolerance is larger than the drawn glyph.
|
||||
- **Timing**: no double-click-to-mean-something-else, no press-and-hold as the
|
||||
only route to a function, no cycle that depends on repeated clicks
|
||||
(see L5). The long-press that opens the offer on a one-button Mac and on touch
|
||||
(§4.1) is explicitly an *additional* route — Ctrl-click, two-finger tap and
|
||||
the keyboard all reach the same place — and it shows its own progress while
|
||||
charging, so it never fails silently.
|
||||
- **Motion**: animation is functional (showing where a thing went), never
|
||||
decorative, and it respects the reduced-motion preference.
|
||||
- **Text**: no fixed-width assumptions; the UI holds together in German and in
|
||||
Chinese, at 125 % and 200 % scale. Every string routed through the normal
|
||||
translation path.
|
||||
|
||||
## 7. Depth without clutter — the three tiers
|
||||
|
||||
Power for experts is delivered by **progressive disclosure of tools, never by
|
||||
relocation of tools**. A tool that appears in a later tier is in the same place
|
||||
it will always be; it is simply not shown yet.
|
||||
|
||||
| Tier | Who | What appears |
|
||||
|---|---|---|
|
||||
| **Make** | first hour | Sketch, extrude, revolve, hole, fillet/chamfer, move, commit to plate |
|
||||
| **Model** | competent user | Patterns, shell, draft, sweep/loft, booleans, reference geometry, variables, import/export |
|
||||
| **Mechanism** | mechanical designer | Assemblies and mates, exploded views, interference detection, surfaces, feature-level editing of imported solids |
|
||||
|
||||
Rules that keep this honest:
|
||||
|
||||
1. **Tiers are non-modal.** No mode switch, no workbench selector, no "advanced
|
||||
mode" toggle that changes the meaning of anything. The tier only governs what
|
||||
is *offered*.
|
||||
2. **A tier reveals itself by use.** Using a body reveals boolean tools; adding
|
||||
a second body reveals assembly tools. The product notices what you are doing.
|
||||
3. **Nothing moves when a tier appears.** A user who learned where fillet lives
|
||||
finds it in the same place forever.
|
||||
4. **An expert tool obeys the same grammar** as a beginner tool. Mates are
|
||||
picked in 3D like everything else, not configured in a table.
|
||||
5. **Exploded views are a view state**, not a document mode — reversible,
|
||||
draggable along mate axes, and never a separate file.
|
||||
|
||||
## 8. Designing for print — the home advantage
|
||||
|
||||
Design-time knowledge the application already has, and must use:
|
||||
|
||||
- **The plate is present** in the design space, at the real size, with the real
|
||||
origin. Committing a body to the plate is one action and preserves placement.
|
||||
- **Print-domain checks run on the model, on the geometry, before slicing**:
|
||||
walls thinner than the nozzle, unsupported overhangs beyond the material's
|
||||
angle, features smaller than the layer height, a part that does not fit the
|
||||
build volume.
|
||||
- **These are warnings on the geometry, never a report.** The thin wall glows;
|
||||
the tooltip says how thin and what the nozzle is.
|
||||
- **Material and machine context is inherited** from the active slicer profile,
|
||||
not re-entered in the Design tab.
|
||||
- **The round trip is preserved**: editing a design after slicing returns to the
|
||||
feature history, not to a mesh.
|
||||
|
||||
## 9. The review gate
|
||||
|
||||
Every pull request that touches the Design tab UI answers these, in the PR body.
|
||||
A "no" that is not accompanied by an argument is a request for changes.
|
||||
|
||||
1. Which law (L1–L11) does the change most directly serve?
|
||||
2. Can the whole operation be completed without the pointer leaving the
|
||||
viewport? If not, why is this the exception?
|
||||
And: does the relevant selection *offer* this tool (§4.1), or must the user
|
||||
already know it exists?
|
||||
3. Are the values draggable *and* typable?
|
||||
4. Screenshot of the state after **exactly one** click of the new gesture,
|
||||
performed as a first-time user.
|
||||
5. Keyboard-only walkthrough: does it complete?
|
||||
6. Greyscale screenshot: is every state still distinguishable?
|
||||
7. What was **removed**? (Net additions to permanent chrome require an argument.)
|
||||
8. Which tier does it belong to, and does it appear without moving anything else?
|
||||
9. What does it do when the geometry is invalid, and is that reported before the
|
||||
commit?
|
||||
10. Interaction cost: actions required for the canonical task it addresses,
|
||||
before and after.
|
||||
11. Reach (L11): screenshot at 1366×768 with the panel open — is the model still
|
||||
on screen? Does it run on integrated graphics? Does it need the network, an
|
||||
account, or a file the user cannot keep?
|
||||
12. If the change adds or moves a verb in the offer: which row, and is it that
|
||||
verb's row in **every** selection where it appears? Did any existing verb's
|
||||
index change? (If yes, this is not a UI change, it is a breaking change to
|
||||
every user's muscle memory, and it needs the group — see §4.1.) Was
|
||||
`docs/ux/tool_atlas.json` updated and the atlas regenerated?
|
||||
13. If the change adds a pointer gesture: what is its keyboard equivalent, and
|
||||
what does a one-button Mac, a trackpad and a touch screen do (§4.1)?
|
||||
|
||||
## 10. Where we stand today — honest inventory
|
||||
|
||||
Complying with the laws already:
|
||||
|
||||
- Sketch inline editors — draw an entity and its dimension tab opens on the
|
||||
geometry; Tab walks Length → Width → Angle.
|
||||
- Fillet/chamfer draggable radius arrow with an editable value label.
|
||||
- Extrude depth arrow; move-body three-axis arrows.
|
||||
- Datum-plane resize handles and offset arrow; ghost reference planes picked in
|
||||
3D.
|
||||
- Imported-art place/size gizmo.
|
||||
- Sketch plane taken from the picked face, with the target named in the status
|
||||
line, and the sketch-plane dropdown deleted outright.
|
||||
|
||||
Violating them, with removal scheduled:
|
||||
|
||||
- **Every tool card is a two-column form** of combos and spin fields in the left
|
||||
panel. This is the single largest debt in the product and the reason this
|
||||
document exists. Tracked as an epic; each card is replaced by its on-geometry
|
||||
equivalent, not improved in place. It fails L1 and it fails L11 twice over —
|
||||
on a 1366×768 screen the cards leave the model a strip.
|
||||
- Seven remaining plane pickers still populate a combo instead of consuming a
|
||||
viewport selection.
|
||||
- Pattern has no on-geometry spacing arrow or count badge.
|
||||
- Hole is positioned by X/Y fields rather than by a point on a face.
|
||||
- Booleans and cuts pick their operands from lists rather than in 3D.
|
||||
- Fillet/chamfer edge selection still requires the click cycle L5 forbids.
|
||||
- **Selecting geometry offers nothing.** There is no contextual offer (§4.1):
|
||||
the user faces the full toolbar whatever they have picked, and finds out that
|
||||
a tool did not apply by it doing nothing. This is the largest single item of
|
||||
new work the charter asks for. The map and every state of it are already
|
||||
drawn (`docs/ux/offer_atlas.html`); what the group owes itself before the code
|
||||
is ratifying the row order, since every verb built before that lands has to be
|
||||
addressed afterwards anyway.
|
||||
- **Committing is an invisible click in empty space** rather than the
|
||||
confirm/cancel puck of §4.2 — the exact gesture that rule withdraws.
|
||||
|
||||
Nothing on the violating list is defended. The only open question for each is
|
||||
what its on-geometry replacement should be.
|
||||
|
||||
## 11. How the group works
|
||||
|
||||
**Roles.** Product/UX lead (owns this document and casts the tie-break vote on
|
||||
interaction questions); kernel maintainer; GUI maintainer; a print-domain
|
||||
reviewer; a mechanical-design reviewer who uses the product on real work; an
|
||||
accessibility reviewer covering both senses of §6 — reach and assistive — who
|
||||
owns the reference machine and actually runs on it. One person may hold more
|
||||
than one role; the UX lead and the mechanical-design reviewer should not be the
|
||||
same person, and nobody reviews reach from a workstation.
|
||||
|
||||
**The absent audience needs a seat.** The fourteen-year-old is not in the room
|
||||
and cannot file an issue. Someone in the group is accountable for B5 and B6, and
|
||||
the group watches real first-timers use the product on the reference machine at
|
||||
least once a quarter — school, makerspace, or a friend's kid. Everything else in
|
||||
this document can be argued from principle; approachability can only be
|
||||
observed.
|
||||
|
||||
**Cadence.** A short weekly review of open interaction proposals. A monthly pass
|
||||
over the violating inventory in §10 — anything that has not moved in two months
|
||||
is either scheduled or explicitly accepted as permanent, with a reason written
|
||||
into this document.
|
||||
|
||||
**How a change moves.**
|
||||
|
||||
1. *Problem* — a described user difficulty, ideally with an interaction-cost
|
||||
measurement, never a solution in disguise.
|
||||
2. *Sketch* — one or two on-geometry interaction proposals, drawn or described
|
||||
as a gesture sequence. Reviewed against §3 before any code.
|
||||
3. *Prototype* — built behind whatever the smallest safe path is, driven end to
|
||||
end on a real display, and screenshotted at each state.
|
||||
4. *Gate* — §9 answered in the PR.
|
||||
5. *Merge*, then update §10.
|
||||
|
||||
**Decisions are written down.** Any resolution that constrains future work is
|
||||
appended to this document as a numbered law or as an accepted exception with its
|
||||
reasoning. A decision that lives only in a call is not a decision.
|
||||
|
||||
**How disagreements resolve.** Against the laws first. If the laws do not decide
|
||||
it, the tie-break is the interaction cost measured on the canonical tasks in
|
||||
§12; if that does not decide it, the UX lead chooses and records why.
|
||||
|
||||
## 12. Canonical tasks — the benchmark
|
||||
|
||||
The measure of every UX change is the cost of these five tasks. Each is timed and
|
||||
counted (clicks, keystrokes, camera actions, mode switches) on the headless rig
|
||||
and, periodically, with real users who have not seen the product.
|
||||
|
||||
| # | Task | What it exercises |
|
||||
|---|---|---|
|
||||
| **B1** | Bracket: sketch an L, extrude, two holes, fillet the inside corner, send to plate | The inner loop |
|
||||
| **B2** | Change a hole diameter and the plate thickness, six features deep, and rebuild | Parametric editability |
|
||||
| **B3** | Take an imported STEP, delete a boss, close the face, thicken a wall to nozzle width | Direct editing + print awareness |
|
||||
| **B4** | Two parts, one revolute mate, check interference, produce an exploded view | The Mechanism tier |
|
||||
| **B5** | First-run: from opening the Design tab to a print-ready solid, no documentation | Approachability |
|
||||
| **B6** | B1 again, on the reference machine at 1366×768, offline, on a fresh account-less install | Reach (L11) |
|
||||
|
||||
Every task is run on the reference machine of §6.1, not on a workstation — a
|
||||
number measured on a fast desktop describes an experience most of our users will
|
||||
never have. B6 repeats the inner loop under the full entry conditions so that
|
||||
reach is a measured quantity and not an intention.
|
||||
|
||||
Targets are set once each task has been measured on the current build. B5's
|
||||
target is expressed in minutes-to-first-solid **by someone who has never seen a
|
||||
CAD program**, and it is the number this project is ultimately judged by.
|
||||
|
||||
---
|
||||
|
||||
### Appendix — anti-patterns we have already paid for
|
||||
|
||||
Kept because each cost real time and each is easy to reintroduce.
|
||||
|
||||
- **The dropdown that grew a row.** Fixing "cannot sketch on a face" by adding a
|
||||
"Face of Body 1" entry to a plane combo. It reads as a small fix and it is the
|
||||
side-panel architecture reproducing itself.
|
||||
- **The invisible first click.** Flyout buttons and pick cycles whose first click
|
||||
changes nothing meaningful. Filed as bugs three separate times against working
|
||||
code, and made a real bug look fixed when it was not.
|
||||
- **The fix verified through a path the user will never take.** A face-sketch fix
|
||||
confirmed by double-clicking to reach face level. Users click once. A fix
|
||||
reachable only by an undiscoverable gesture is indistinguishable from no fix.
|
||||
- **The wrong feedback surface.** Measuring an armed tool by the toolbar, which
|
||||
never renders keyboard-armed state. The status line is the surface that
|
||||
answers.
|
||||
- **The silent success.** A cut that removed no material, reported as done. Now
|
||||
an error naming the likely cause.
|
||||
@@ -1,169 +0,0 @@
|
||||
# BearConnector.step — examination
|
||||
|
||||
> **Scope.** One file was supplied and it contains **one object: the male.** Everything below is
|
||||
> measured from that single solid. Earlier drafts of this note reasoned about a female pocket and a
|
||||
> mating pair — those objects were never supplied, so any statement about them was speculation and
|
||||
> has been removed. The clearance, the fit, and the pocket's legibility are all **unassessed**.
|
||||
|
||||
Measured, not eyeballed. Imported into the Design tab's own OpenCascade kernel
|
||||
(`import_step` → one valid closed solid), topology queried, geometry checked numerically.
|
||||
Flat drawing: `artifacts/shots/bear-flat.png`. Viewport: `artifacts/shots/bear-02-zoom.png`.
|
||||
|
||||
**File:** AP242 Edition 2, ST-Developer. 1 `MANIFOLD_SOLID_BREP`, 1 `CLOSED_SHELL`.
|
||||
**Size:** 83.06 × 66.69 × 17.27 mm. **Faces:** 30 — 24 planar + 6 cylindrical.
|
||||
**Curves:** 69 lines + 12 circles. **No** splines, spheres, tori or cones.
|
||||
**Relief:** only four Z levels — 0, 3.00, 10.66, 17.27.
|
||||
|
||||
---
|
||||
|
||||
## What is right, and precisely so
|
||||
|
||||
**The sloping ridge is implemented exactly as briefed.** From (0.00, 18.40, 17.27) to
|
||||
(0.00, 46.72, 10.66): 28.3 mm long, 6.61 mm drop, **13.1° slope**, and both ends sit dead on
|
||||
x = 0.00. It breaks 180° rotation on its own.
|
||||
|
||||
**20.0° uniform draft on all four snout flanks**, identical to within 0.1°:
|
||||
`(0,−0.94,0.342) (0.936,0.08,0.342) (0,0.94,0.342) (−0.936,0.08,0.342)`. That is a real,
|
||||
deliberate lead-in — it self-centres into a matching pocket, and it demoulds and prints.
|
||||
|
||||
**The eyes are exactly symmetric**: Ø9.87 at x = ±16.43, y = 48.01, matching to 0.01 mm.
|
||||
Someone mirrored those on purpose.
|
||||
|
||||
**The mating feature is extremely economical**: only **five edges** exist above the 3 mm plate —
|
||||
the ridge plus two flank edges at each end. Base plate is exactly 3.00 mm.
|
||||
|
||||
The low-poly constraint is honoured. All six cylinders are outline rounds and eye holes; none of
|
||||
them is a mating surface.
|
||||
|
||||
---
|
||||
|
||||
## The asymmetry is deliberate, and it is complete
|
||||
|
||||
**Correction.** A first pass read the left/right differences as an unfinished mirror. That was wrong:
|
||||
the asymmetry is intentional. Tested properly — every candidate self-symmetry, in the part's own
|
||||
centred frame, with a generous 0.1 mm tolerance:
|
||||
|
||||
| operation | edges mapped onto the part |
|
||||
|---|---|
|
||||
| identity | 81 / 81 — 100 % |
|
||||
| mirror about x = 0 (left/right) | **0 / 81** |
|
||||
| mirror about y = 0 (top/bottom) | **0 / 81** |
|
||||
| rotate 180° about Z | **0 / 81** |
|
||||
| rotate 90° about Z | **0 / 81** |
|
||||
| mirror about the diagonal | **0 / 81** |
|
||||
|
||||
**The symmetry group is trivial.** No rigid motion or reflection maps this part onto itself, so
|
||||
**every partial view determines the orientation uniquely** — you never need to see the whole face to
|
||||
know which way round it goes. That is the strongest possible result for a keying interface and it is
|
||||
exactly what the earlier abstract glyph work kept failing to achieve: a symmetric shape seen at a
|
||||
grazing angle, or half-occluded, gives an ambiguous read.
|
||||
|
||||
### Does it let you GRASP the orientation? Measured, not asserted.
|
||||
|
||||
Unique-in-principle and graspable-at-a-glance are different claims. The symmetry table proves the
|
||||
first. For the second, the front-on picture (outline + eyes + mouth, filled) was rasterised and
|
||||
compared against its own mirror and its own 180° rotation — the two ways a person can get it wrong.
|
||||
|
||||
**By size** (percentage of pixels that differ):
|
||||
|
||||
| width | vs mirror | vs rotated 180° |
|
||||
|---|---|---|
|
||||
| 16 px | 20.7 % | 26.0 % |
|
||||
| 24 px | 21.9 % | 30.9 % |
|
||||
| 32 px | 23.0 % | 28.1 % |
|
||||
| 48 px | 22.4 % | 30.6 % |
|
||||
| 80 px | 24.7 % | 31.0 % |
|
||||
| 160 px | 23.6 % | 31.0 % |
|
||||
|
||||
**The curve is flat.** The full signal is already there at 16 pixels and more resolution adds
|
||||
nothing. That is the whole result: **the orientation cue lives at low spatial frequency**, carried by
|
||||
the overall shape rather than by any detail. It therefore survives distance, blur, poor light,
|
||||
peripheral vision, a small print and a low-resolution screen. It is the exact opposite of the abstract
|
||||
disc glyph, whose roll cue was a small high-frequency feature and died at a grazing angle.
|
||||
|
||||
**Partial views — a claim I made and then withdrew.** I ran a masked-window test and concluded that
|
||||
a single quarter of the face was enough to read the orientation. **That test was invalid and the
|
||||
conclusion is wrong.** It compared a window of the original against *the same window* of the mirrored
|
||||
and rotated versions — which silently hands the observer the registration. It assumes you already
|
||||
know that the patch you are looking at is the top-left quarter, which is exactly the thing you would
|
||||
not know if you could only see a quarter.
|
||||
|
||||
**You need to see the whole face.** The cues here are *relational*: the big ear only means something
|
||||
next to the small ear, and the mouth offset only means something relative to the centreline. None of
|
||||
them is self-locating. Whole-face is the operating condition, and the design should be judged and
|
||||
used on that basis.
|
||||
|
||||
That does not weaken the size result above, which always used the complete silhouette: the whole face
|
||||
reads at 16 px. Needing all of it, and needing very little resolution of it, are compatible — and for
|
||||
a part held in a hand, seeing all of it is the normal case.
|
||||
|
||||
**The signal is allocated to the right risks.** The strongest cue (up to 41.7 %) guards against
|
||||
inserting it upside down — the mistake people actually make. The weakest (~23 %) guards the mirror
|
||||
case, which needs the part flipped over and which the protrusion already prevents mechanically.
|
||||
|
||||
It also does mechanical work beyond the ridge. The ridge alone breaks 180° rotation; the asymmetric
|
||||
outline additionally defeats the **mirrored-part** case — a mirror-image copy will not fit, so a
|
||||
modelling or printing mirror is caught at assembly rather than three steps later.
|
||||
|
||||
And for children specifically, a symmetric cartoon face reads as a mask; illustrators asymmetrise
|
||||
deliberately so a face reads as a *character*. The asymmetry is earning its keep three ways at once.
|
||||
|
||||
### What is worth keeping in mind anyway
|
||||
|
||||
**The ears differ by 42 %** — left 8.33 mm wide (top y 65.68), right 11.81 mm (top y 66.69). Both
|
||||
start at the same y = 60.79, so they read as a deliberate pair rather than an error. 42 % is well
|
||||
above the perceptual threshold: you see it instantly. Good cue.
|
||||
|
||||
**The mouth is a smirk** — x −21.93 … 0.00, centred at x = −10.96, stopping on the centreline. A
|
||||
classic character device and a strong asymmetry.
|
||||
|
||||
**The rounds are the best cue and the one safety question.** All four are on the left — Ø11.71 at
|
||||
(−40.82, 7.38), Ø11.71 at (−34.76, 0.58), Ø10.00 at (−29.85, 60.83), Ø2.90 at (−26.70, 65.95) — and
|
||||
the right side is entirely sharp. This is the *most locally readable* cue in the design: the ears
|
||||
differ only by comparison (you must see both to know which is which), whereas a rounded corner tells
|
||||
you "this is the left" from that corner alone, by eye **or by fingertip**. For children assembling by
|
||||
feel that is the cue doing the real work.
|
||||
|
||||
The tension is that "sharp" on a children's part is a hazard, and the obvious safety fix — round
|
||||
everything — destroys the cue. The resolution is not round-vs-sharp but **large-vs-small radius**:
|
||||
keep R≈6 on the left and give the right R≈1. R1 still reads and feels sharp locally, so the cue
|
||||
survives, and the actual edge hazard goes away. That is the one recommendation that outlives the
|
||||
correction.
|
||||
|
||||
**One measurement that does not fit the story:** the outline is off-centre by **0.54 mm** (left reach
|
||||
40.99, right reach 42.07). A deliberate cue should be unmissable; 0.54 mm is invisible. It is
|
||||
probably a by-product of the other features rather than intent — worth a look, not a defect.
|
||||
|
||||
---
|
||||
|
||||
## Two judgement calls, not defects
|
||||
|
||||
**The snout is highest at the nose tip and slopes down toward the brow** — a real bear's muzzle
|
||||
does the opposite. Anatomically it reads more like a beak or a horn than a snout. But mechanically
|
||||
it is the better choice: the nose tip enters the pocket first and does the finding. Keep it if the
|
||||
lead-in matters more than the likeness; flip it if "it must look like a bear" wins.
|
||||
|
||||
**Only the male was supplied**, so the clearance, the fit and the pocket are unassessed. Nothing in
|
||||
this note should be read as a judgement on them.
|
||||
|
||||
---
|
||||
|
||||
## The strategic point, which is the real reason this design is good
|
||||
|
||||
It gives orientation **a name**. "Ears up, nose down" needs no legend, no convention and no
|
||||
documentation. Face recognition is the most robust pattern-matching humans have: it survives low
|
||||
resolution, poor light, partial occlusion and peripheral vision. That is exactly the robustness the
|
||||
abstract ridge key was reaching for, and here it comes for free.
|
||||
|
||||
**One earlier objection does not transfer — noting it only so it is not carried over by mistake.**
|
||||
In §8c of the design doc a female *pocket* measured as visually invisible — flat-shaded, a recess
|
||||
reads as a blank rectangle — and I concluded male/female
|
||||
is the wrong polarity cue. **That was a viewport finding, and it does not apply to a physical part.**
|
||||
Nobody looks into the pocket of a toy; they feel it. For a part in a child's hands, male/female is
|
||||
exactly the right polarity language. The earlier conclusion stands for the on-screen glyph and must
|
||||
not be carried over to this.
|
||||
|
||||
**The one rule to write down now:** the face and the key must never be allowed to disagree. People
|
||||
will trust the face over the mechanics every time. Here they agree — ridge on the centreline, ears
|
||||
up. If the face is ever restyled independently of the key, a user will orient by the bear and be
|
||||
wrong. Tie them permanently, in the model and in whatever generates it.
|
||||
@@ -1,998 +0,0 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('FreeCAD Model'),'2;1');
|
||||
FILE_NAME('Open CASCADE Shape Model','2026-08-05T12:46:26',('FreeCAD'),(
|
||||
'FreeCAD'),'Open CASCADE STEP processor 7.8','FreeCAD','Unknown');
|
||||
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1 = APPLICATION_PROTOCOL_DEFINITION('international standard',
|
||||
'automotive_design',2000,#2);
|
||||
#2 = APPLICATION_CONTEXT(
|
||||
'core data for automotive mechanical design processes');
|
||||
#3 = SHAPE_DEFINITION_REPRESENTATION(#4,#10);
|
||||
#4 = PRODUCT_DEFINITION_SHAPE('','',#5);
|
||||
#5 = PRODUCT_DEFINITION('design','',#6,#9);
|
||||
#6 = PRODUCT_DEFINITION_FORMATION('','',#7);
|
||||
#7 = PRODUCT('Open CASCADE STEP translator 7.8 1',
|
||||
'Open CASCADE STEP translator 7.8 1','',(#8));
|
||||
#8 = PRODUCT_CONTEXT('',#2,'mechanical');
|
||||
#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design');
|
||||
#10 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#15),#958);
|
||||
#11 = AXIS2_PLACEMENT_3D('',#12,#13,#14);
|
||||
#12 = CARTESIAN_POINT('',(0.,0.,0.));
|
||||
#13 = DIRECTION('',(0.,0.,1.));
|
||||
#14 = DIRECTION('',(1.,0.,-0.));
|
||||
#15 = MANIFOLD_SOLID_BREP('',#16);
|
||||
#16 = CLOSED_SHELL('',(#17,#229,#260,#497,#514,#531,#548,#565,#582,#599,
|
||||
#616,#633,#650,#667,#684,#701,#718,#735,#747,#770,#794,#810,#822,
|
||||
#839,#856,#878,#895,#912,#929,#946));
|
||||
#17 = ADVANCED_FACE('',(#18,#68,#79,#213),#224,.F.);
|
||||
#18 = FACE_BOUND('',#19,.F.);
|
||||
#19 = EDGE_LOOP('',(#20,#30,#38,#46,#54,#62));
|
||||
#20 = ORIENTED_EDGE('',*,*,#21,.F.);
|
||||
#21 = EDGE_CURVE('',#22,#24,#26,.T.);
|
||||
#22 = VERTEX_POINT('',#23);
|
||||
#23 = CARTESIAN_POINT('',(19.029295926024,-0.2,-17.63009960955));
|
||||
#24 = VERTEX_POINT('',#25);
|
||||
#25 = CARTESIAN_POINT('',(16.626582997737,-0.2,-8.940188245231));
|
||||
#26 = LINE('',#27,#28);
|
||||
#27 = CARTESIAN_POINT('',(19.849519003668,-0.2,-20.59660707692));
|
||||
#28 = VECTOR('',#29,1.);
|
||||
#29 = DIRECTION('',(-0.26649542889,0.,0.963836182336));
|
||||
#30 = ORIENTED_EDGE('',*,*,#31,.F.);
|
||||
#31 = EDGE_CURVE('',#32,#22,#34,.T.);
|
||||
#32 = VERTEX_POINT('',#33);
|
||||
#33 = CARTESIAN_POINT('',(22.059435554995,-0.2,-3.734519760785));
|
||||
#34 = LINE('',#35,#36);
|
||||
#35 = CARTESIAN_POINT('',(17.698510515043,-0.2,-23.73280021221));
|
||||
#36 = VECTOR('',#37,1.);
|
||||
#37 = DIRECTION('',(-0.213058124893,0.,-0.977039526026));
|
||||
#38 = ORIENTED_EDGE('',*,*,#39,.F.);
|
||||
#39 = EDGE_CURVE('',#40,#32,#42,.T.);
|
||||
#40 = VERTEX_POINT('',#41);
|
||||
#41 = CARTESIAN_POINT('',(-21.72552223146,-0.2,-3.734519760785));
|
||||
#42 = LINE('',#43,#44);
|
||||
#43 = CARTESIAN_POINT('',(0.297084840953,-0.2,-3.734519760785));
|
||||
#44 = VECTOR('',#45,1.);
|
||||
#45 = DIRECTION('',(1.,0.,0.));
|
||||
#46 = ORIENTED_EDGE('',*,*,#47,.F.);
|
||||
#47 = EDGE_CURVE('',#48,#40,#50,.T.);
|
||||
#48 = VERTEX_POINT('',#49);
|
||||
#49 = CARTESIAN_POINT('',(-21.72552223146,-0.2,-8.903751135252));
|
||||
#50 = LINE('',#51,#52);
|
||||
#51 = CARTESIAN_POINT('',(-21.72552223146,-0.2,-19.83215600037));
|
||||
#52 = VECTOR('',#53,1.);
|
||||
#53 = DIRECTION('',(0.,0.,1.));
|
||||
#54 = ORIENTED_EDGE('',*,*,#55,.F.);
|
||||
#55 = EDGE_CURVE('',#56,#48,#58,.T.);
|
||||
#56 = VERTEX_POINT('',#57);
|
||||
#57 = CARTESIAN_POINT('',(4.383041634064E-04,-0.2,-8.903751615529));
|
||||
#58 = LINE('',#59,#60);
|
||||
#59 = CARTESIAN_POINT('',(-5.279852300138,-0.2,-8.903751135252));
|
||||
#60 = VECTOR('',#61,1.);
|
||||
#61 = DIRECTION('',(-1.,0.,0.));
|
||||
#62 = ORIENTED_EDGE('',*,*,#63,.F.);
|
||||
#63 = EDGE_CURVE('',#24,#56,#64,.T.);
|
||||
#64 = LINE('',#65,#66);
|
||||
#65 = CARTESIAN_POINT('',(4.347099726942,-0.2,-8.913277437397));
|
||||
#66 = VECTOR('',#67,1.);
|
||||
#67 = DIRECTION('',(-0.999997598615,0.,2.191520817069E-03));
|
||||
#68 = FACE_BOUND('',#69,.F.);
|
||||
#69 = EDGE_LOOP('',(#70));
|
||||
#70 = ORIENTED_EDGE('',*,*,#71,.F.);
|
||||
#71 = EDGE_CURVE('',#72,#72,#74,.T.);
|
||||
#72 = VERTEX_POINT('',#73);
|
||||
#73 = CARTESIAN_POINT('',(21.163799345768,-0.2,-48.00951684793));
|
||||
#74 = CIRCLE('',#75,4.735522705283);
|
||||
#75 = AXIS2_PLACEMENT_3D('',#76,#77,#78);
|
||||
#76 = CARTESIAN_POINT('',(16.428276640485,-0.2,-48.00951684793));
|
||||
#77 = DIRECTION('',(-0.,1.,0.));
|
||||
#78 = DIRECTION('',(1.,0.,0.));
|
||||
#79 = FACE_BOUND('',#80,.F.);
|
||||
#80 = EDGE_LOOP('',(#81,#91,#100,#108,#116,#125,#133,#142,#150,#158,#166
|
||||
,#174,#182,#190,#198,#206));
|
||||
#81 = ORIENTED_EDGE('',*,*,#82,.T.);
|
||||
#82 = EDGE_CURVE('',#83,#85,#87,.T.);
|
||||
#83 = VERTEX_POINT('',#84);
|
||||
#84 = CARTESIAN_POINT('',(-27.86158468659,-0.2,-65.78852163128));
|
||||
#85 = VERTEX_POINT('',#86);
|
||||
#86 = CARTESIAN_POINT('',(-29.08766684168,-0.2,-64.52156932717));
|
||||
#87 = LINE('',#88,#89);
|
||||
#88 = CARTESIAN_POINT('',(-29.44004200272,-0.2,-64.15744811364));
|
||||
#89 = VECTOR('',#90,1.);
|
||||
#90 = DIRECTION('',(-0.695421216677,0.,0.718602345805));
|
||||
#91 = ORIENTED_EDGE('',*,*,#92,.F.);
|
||||
#92 = EDGE_CURVE('',#93,#85,#95,.T.);
|
||||
#93 = VERTEX_POINT('',#94);
|
||||
#94 = CARTESIAN_POINT('',(-28.96712497021,-0.2,-57.16864680227));
|
||||
#95 = CIRCLE('',#96,5.2);
|
||||
#96 = AXIS2_PLACEMENT_3D('',#97,#98,#99);
|
||||
#97 = CARTESIAN_POINT('',(-25.35093464349,-0.2,-60.90537900045));
|
||||
#98 = DIRECTION('',(0.,-1.,0.));
|
||||
#99 = DIRECTION('',(-1.,0.,0.));
|
||||
#100 = ORIENTED_EDGE('',*,*,#101,.T.);
|
||||
#101 = EDGE_CURVE('',#93,#102,#104,.T.);
|
||||
#102 = VERTEX_POINT('',#103);
|
||||
#103 = CARTESIAN_POINT('',(-26.93875323652,-0.2,-55.20570756731));
|
||||
#104 = LINE('',#105,#106);
|
||||
#105 = CARTESIAN_POINT('',(-14.90185526362,-0.2,-43.55710346492));
|
||||
#106 = VECTOR('',#107,1.);
|
||||
#107 = DIRECTION('',(0.718602345805,0.,0.695421216677));
|
||||
#108 = ORIENTED_EDGE('',*,*,#109,.T.);
|
||||
#109 = EDGE_CURVE('',#102,#110,#112,.T.);
|
||||
#110 = VERTEX_POINT('',#111);
|
||||
#111 = CARTESIAN_POINT('',(-41.17904151244,-0.2,-10.52828909594));
|
||||
#112 = LINE('',#113,#114);
|
||||
#113 = CARTESIAN_POINT('',(-32.39120436163,-0.2,-38.09921113329));
|
||||
#114 = VECTOR('',#115,1.);
|
||||
#115 = DIRECTION('',(-0.30368282823,0.,0.952773183837));
|
||||
#116 = ORIENTED_EDGE('',*,*,#117,.F.);
|
||||
#117 = EDGE_CURVE('',#118,#110,#120,.T.);
|
||||
#118 = VERTEX_POINT('',#119);
|
||||
#119 = CARTESIAN_POINT('',(-39.69176606491,-0.2,-4.408923352436));
|
||||
#120 = CIRCLE('',#121,6.054044962965);
|
||||
#121 = AXIS2_PLACEMENT_3D('',#122,#123,#124);
|
||||
#122 = CARTESIAN_POINT('',(-35.41090981799,-0.2,-8.689779599357));
|
||||
#123 = DIRECTION('',(0.,-1.,0.));
|
||||
#124 = DIRECTION('',(-1.,0.,0.));
|
||||
#125 = ORIENTED_EDGE('',*,*,#126,.T.);
|
||||
#126 = EDGE_CURVE('',#118,#127,#129,.T.);
|
||||
#127 = VERTEX_POINT('',#128);
|
||||
#128 = CARTESIAN_POINT('',(-36.85603142851,-0.2,-1.573188716044));
|
||||
#129 = LINE('',#130,#131);
|
||||
#130 = CARTESIAN_POINT('',(-36.19319006079,-0.2,-0.910347348321));
|
||||
#131 = VECTOR('',#132,1.);
|
||||
#132 = DIRECTION('',(0.707106781187,0.,0.707106781187));
|
||||
#133 = ORIENTED_EDGE('',*,*,#134,.F.);
|
||||
#134 = EDGE_CURVE('',#135,#127,#137,.T.);
|
||||
#135 = VERTEX_POINT('',#136);
|
||||
#136 = CARTESIAN_POINT('',(-32.57517518159,-0.2,0.2));
|
||||
#137 = CIRCLE('',#138,6.054044962965);
|
||||
#138 = AXIS2_PLACEMENT_3D('',#139,#140,#141);
|
||||
#139 = CARTESIAN_POINT('',(-32.57517518159,-0.2,-5.854044962965));
|
||||
#140 = DIRECTION('',(0.,-1.,0.));
|
||||
#141 = DIRECTION('',(-1.,0.,0.));
|
||||
#142 = ORIENTED_EDGE('',*,*,#143,.T.);
|
||||
#143 = EDGE_CURVE('',#135,#144,#146,.T.);
|
||||
#144 = VERTEX_POINT('',#145);
|
||||
#145 = CARTESIAN_POINT('',(35.082842712475,-0.2,0.2));
|
||||
#146 = LINE('',#147,#148);
|
||||
#147 = CARTESIAN_POINT('',(-7.942265537672,-0.2,0.2));
|
||||
#148 = VECTOR('',#149,1.);
|
||||
#149 = DIRECTION('',(1.,0.,0.));
|
||||
#150 = ORIENTED_EDGE('',*,*,#151,.F.);
|
||||
#151 = EDGE_CURVE('',#152,#144,#154,.T.);
|
||||
#152 = VERTEX_POINT('',#153);
|
||||
#153 = CARTESIAN_POINT('',(42.298608189024,-0.2,-7.015765476549));
|
||||
#154 = LINE('',#155,#156);
|
||||
#155 = CARTESIAN_POINT('',(36.596246576251,-0.2,-1.313403863776));
|
||||
#156 = VECTOR('',#157,1.);
|
||||
#157 = DIRECTION('',(-0.707106781187,0.,0.707106781187));
|
||||
#158 = ORIENTED_EDGE('',*,*,#159,.F.);
|
||||
#159 = EDGE_CURVE('',#160,#152,#162,.T.);
|
||||
#160 = VERTEX_POINT('',#161);
|
||||
#161 = CARTESIAN_POINT('',(26.938753236523,-0.2,-55.20570756731));
|
||||
#162 = LINE('',#163,#164);
|
||||
#163 = CARTESIAN_POINT('',(32.699020781567,-0.2,-37.13346923684));
|
||||
#164 = VECTOR('',#165,1.);
|
||||
#165 = DIRECTION('',(0.30368282823,0.,0.952773183837));
|
||||
#166 = ORIENTED_EDGE('',*,*,#167,.F.);
|
||||
#167 = EDGE_CURVE('',#168,#160,#170,.T.);
|
||||
#168 = VERTEX_POINT('',#169);
|
||||
#169 = CARTESIAN_POINT('',(32.703857168398,-0.2,-60.78483712899));
|
||||
#170 = LINE('',#171,#172);
|
||||
#171 = CARTESIAN_POINT('',(16.008242280412,-0.2,-44.62779994931));
|
||||
#172 = VECTOR('',#173,1.);
|
||||
#173 = DIRECTION('',(-0.718602345805,0.,0.695421216677));
|
||||
#174 = ORIENTED_EDGE('',*,*,#175,.F.);
|
||||
#175 = EDGE_CURVE('',#176,#168,#178,.T.);
|
||||
#176 = VERTEX_POINT('',#177);
|
||||
#177 = CARTESIAN_POINT('',(26.715163243538,-0.2,-66.97315781796));
|
||||
#178 = LINE('',#179,#180);
|
||||
#179 = CARTESIAN_POINT('',(30.25240665457,-0.2,-63.31800414721));
|
||||
#180 = VECTOR('',#181,1.);
|
||||
#181 = DIRECTION('',(0.695421216677,0.,0.718602345805));
|
||||
#182 = ORIENTED_EDGE('',*,*,#183,.F.);
|
||||
#183 = EDGE_CURVE('',#184,#176,#186,.T.);
|
||||
#184 = VERTEX_POINT('',#185);
|
||||
#185 = CARTESIAN_POINT('',(20.532019001374,-0.2,-60.98947335481));
|
||||
#186 = LINE('',#187,#188);
|
||||
#187 = CARTESIAN_POINT('',(9.922784884512,-0.2,-50.72247862452));
|
||||
#188 = VECTOR('',#189,1.);
|
||||
#189 = DIRECTION('',(0.718602345805,0.,-0.695421216677));
|
||||
#190 = ORIENTED_EDGE('',*,*,#191,.F.);
|
||||
#191 = EDGE_CURVE('',#192,#184,#194,.T.);
|
||||
#192 = VERTEX_POINT('',#193);
|
||||
#193 = CARTESIAN_POINT('',(-20.53201900137,-0.2,-60.98947335481));
|
||||
#194 = LINE('',#195,#196);
|
||||
#195 = CARTESIAN_POINT('',(5.354765181569,-0.2,-60.98947335481));
|
||||
#196 = VECTOR('',#197,1.);
|
||||
#197 = DIRECTION('',(1.,0.,0.));
|
||||
#198 = ORIENTED_EDGE('',*,*,#199,.T.);
|
||||
#199 = EDGE_CURVE('',#192,#200,#202,.T.);
|
||||
#200 = VERTEX_POINT('',#201);
|
||||
#201 = CARTESIAN_POINT('',(-25.53052705686,-0.2,-65.82673637491));
|
||||
#202 = LINE('',#203,#204);
|
||||
#203 = CARTESIAN_POINT('',(-9.454421870603,-0.2,-50.26922436104));
|
||||
#204 = VECTOR('',#205,1.);
|
||||
#205 = DIRECTION('',(-0.718602345805,0.,-0.695421216677));
|
||||
#206 = ORIENTED_EDGE('',*,*,#207,.F.);
|
||||
#207 = EDGE_CURVE('',#83,#200,#208,.T.);
|
||||
#208 = CIRCLE('',#209,1.648528137424);
|
||||
#209 = AXIS2_PLACEMENT_3D('',#210,#211,#212);
|
||||
#210 = CARTESIAN_POINT('',(-26.67694849991,-0.2,-64.64210018823));
|
||||
#211 = DIRECTION('',(0.,-1.,0.));
|
||||
#212 = DIRECTION('',(-1.,0.,0.));
|
||||
#213 = FACE_BOUND('',#214,.F.);
|
||||
#214 = EDGE_LOOP('',(#215));
|
||||
#215 = ORIENTED_EDGE('',*,*,#216,.F.);
|
||||
#216 = EDGE_CURVE('',#217,#217,#219,.T.);
|
||||
#217 = VERTEX_POINT('',#218);
|
||||
#218 = CARTESIAN_POINT('',(-11.6927539352,-0.2,-48.00951684793));
|
||||
#219 = CIRCLE('',#220,4.735522705283);
|
||||
#220 = AXIS2_PLACEMENT_3D('',#221,#222,#223);
|
||||
#221 = CARTESIAN_POINT('',(-16.42827664048,-0.2,-48.00951684793));
|
||||
#222 = DIRECTION('',(-0.,1.,0.));
|
||||
#223 = DIRECTION('',(1.,0.,0.));
|
||||
#224 = PLANE('',#225);
|
||||
#225 = AXIS2_PLACEMENT_3D('',#226,#227,#228);
|
||||
#226 = CARTESIAN_POINT('',(0.403056515455,-0.2,-33.34517655273));
|
||||
#227 = DIRECTION('',(0.,1.,0.));
|
||||
#228 = DIRECTION('',(1.,0.,0.));
|
||||
#229 = ADVANCED_FACE('',(#230),#255,.F.);
|
||||
#230 = FACE_BOUND('',#231,.F.);
|
||||
#231 = EDGE_LOOP('',(#232,#240,#241,#249));
|
||||
#232 = ORIENTED_EDGE('',*,*,#233,.T.);
|
||||
#233 = EDGE_CURVE('',#234,#160,#236,.T.);
|
||||
#234 = VERTEX_POINT('',#235);
|
||||
#235 = CARTESIAN_POINT('',(26.938753236523,3.2,-55.20570756731));
|
||||
#236 = LINE('',#237,#238);
|
||||
#237 = CARTESIAN_POINT('',(26.938753236523,3.,-55.20570756731));
|
||||
#238 = VECTOR('',#239,1.);
|
||||
#239 = DIRECTION('',(0.,-1.,0.));
|
||||
#240 = ORIENTED_EDGE('',*,*,#159,.T.);
|
||||
#241 = ORIENTED_EDGE('',*,*,#242,.F.);
|
||||
#242 = EDGE_CURVE('',#243,#152,#245,.T.);
|
||||
#243 = VERTEX_POINT('',#244);
|
||||
#244 = CARTESIAN_POINT('',(42.298608189024,3.2,-7.015765476549));
|
||||
#245 = LINE('',#246,#247);
|
||||
#246 = CARTESIAN_POINT('',(42.298608189024,3.,-7.015765476549));
|
||||
#247 = VECTOR('',#248,1.);
|
||||
#248 = DIRECTION('',(0.,-1.,0.));
|
||||
#249 = ORIENTED_EDGE('',*,*,#250,.F.);
|
||||
#250 = EDGE_CURVE('',#234,#243,#251,.T.);
|
||||
#251 = LINE('',#252,#253);
|
||||
#252 = CARTESIAN_POINT('',(32.699020781567,3.2,-37.13346923684));
|
||||
#253 = VECTOR('',#254,1.);
|
||||
#254 = DIRECTION('',(0.30368282823,0.,0.952773183837));
|
||||
#255 = PLANE('',#256);
|
||||
#256 = AXIS2_PLACEMENT_3D('',#257,#258,#259);
|
||||
#257 = CARTESIAN_POINT('',(34.581352051556,3.,-31.22785130119));
|
||||
#258 = DIRECTION('',(-0.952773183837,0.,0.30368282823));
|
||||
#259 = DIRECTION('',(0.30368282823,0.,0.952773183837));
|
||||
#260 = ADVANCED_FACE('',(#261,#311,#322,#447,#481),#492,.T.);
|
||||
#261 = FACE_BOUND('',#262,.T.);
|
||||
#262 = EDGE_LOOP('',(#263,#273,#281,#289,#297,#305));
|
||||
#263 = ORIENTED_EDGE('',*,*,#264,.F.);
|
||||
#264 = EDGE_CURVE('',#265,#267,#269,.T.);
|
||||
#265 = VERTEX_POINT('',#266);
|
||||
#266 = CARTESIAN_POINT('',(16.626582997737,3.2,-8.940188245231));
|
||||
#267 = VERTEX_POINT('',#268);
|
||||
#268 = CARTESIAN_POINT('',(4.383041634064E-04,3.2,-8.903751615529));
|
||||
#269 = LINE('',#270,#271);
|
||||
#270 = CARTESIAN_POINT('',(4.347099726942,3.2,-8.913277437397));
|
||||
#271 = VECTOR('',#272,1.);
|
||||
#272 = DIRECTION('',(-0.999997598615,0.,2.191520817069E-03));
|
||||
#273 = ORIENTED_EDGE('',*,*,#274,.F.);
|
||||
#274 = EDGE_CURVE('',#275,#265,#277,.T.);
|
||||
#275 = VERTEX_POINT('',#276);
|
||||
#276 = CARTESIAN_POINT('',(19.029295926024,3.2,-17.63009960955));
|
||||
#277 = LINE('',#278,#279);
|
||||
#278 = CARTESIAN_POINT('',(19.849519003668,3.2,-20.59660707692));
|
||||
#279 = VECTOR('',#280,1.);
|
||||
#280 = DIRECTION('',(-0.26649542889,0.,0.963836182336));
|
||||
#281 = ORIENTED_EDGE('',*,*,#282,.F.);
|
||||
#282 = EDGE_CURVE('',#283,#275,#285,.T.);
|
||||
#283 = VERTEX_POINT('',#284);
|
||||
#284 = CARTESIAN_POINT('',(22.059435554995,3.2,-3.734519760785));
|
||||
#285 = LINE('',#286,#287);
|
||||
#286 = CARTESIAN_POINT('',(17.698510515043,3.2,-23.73280021221));
|
||||
#287 = VECTOR('',#288,1.);
|
||||
#288 = DIRECTION('',(-0.213058124893,0.,-0.977039526026));
|
||||
#289 = ORIENTED_EDGE('',*,*,#290,.F.);
|
||||
#290 = EDGE_CURVE('',#291,#283,#293,.T.);
|
||||
#291 = VERTEX_POINT('',#292);
|
||||
#292 = CARTESIAN_POINT('',(-21.72552223146,3.2,-3.734519760785));
|
||||
#293 = LINE('',#294,#295);
|
||||
#294 = CARTESIAN_POINT('',(0.297084840953,3.2,-3.734519760785));
|
||||
#295 = VECTOR('',#296,1.);
|
||||
#296 = DIRECTION('',(1.,0.,0.));
|
||||
#297 = ORIENTED_EDGE('',*,*,#298,.F.);
|
||||
#298 = EDGE_CURVE('',#299,#291,#301,.T.);
|
||||
#299 = VERTEX_POINT('',#300);
|
||||
#300 = CARTESIAN_POINT('',(-21.72552223146,3.2,-8.903751135252));
|
||||
#301 = LINE('',#302,#303);
|
||||
#302 = CARTESIAN_POINT('',(-21.72552223146,3.2,-19.83215600037));
|
||||
#303 = VECTOR('',#304,1.);
|
||||
#304 = DIRECTION('',(0.,0.,1.));
|
||||
#305 = ORIENTED_EDGE('',*,*,#306,.F.);
|
||||
#306 = EDGE_CURVE('',#267,#299,#307,.T.);
|
||||
#307 = LINE('',#308,#309);
|
||||
#308 = CARTESIAN_POINT('',(-5.279852300138,3.2,-8.903751135252));
|
||||
#309 = VECTOR('',#310,1.);
|
||||
#310 = DIRECTION('',(-1.,0.,0.));
|
||||
#311 = FACE_BOUND('',#312,.T.);
|
||||
#312 = EDGE_LOOP('',(#313));
|
||||
#313 = ORIENTED_EDGE('',*,*,#314,.F.);
|
||||
#314 = EDGE_CURVE('',#315,#315,#317,.T.);
|
||||
#315 = VERTEX_POINT('',#316);
|
||||
#316 = CARTESIAN_POINT('',(21.163799345768,3.2,-48.00951684793));
|
||||
#317 = CIRCLE('',#318,4.735522705283);
|
||||
#318 = AXIS2_PLACEMENT_3D('',#319,#320,#321);
|
||||
#319 = CARTESIAN_POINT('',(16.428276640485,3.2,-48.00951684793));
|
||||
#320 = DIRECTION('',(-0.,1.,0.));
|
||||
#321 = DIRECTION('',(1.,0.,0.));
|
||||
#322 = FACE_BOUND('',#323,.T.);
|
||||
#323 = EDGE_LOOP('',(#324,#334,#343,#351,#360,#368,#374,#375,#383,#391,
|
||||
#399,#407,#415,#424,#432,#441));
|
||||
#324 = ORIENTED_EDGE('',*,*,#325,.T.);
|
||||
#325 = EDGE_CURVE('',#326,#328,#330,.T.);
|
||||
#326 = VERTEX_POINT('',#327);
|
||||
#327 = CARTESIAN_POINT('',(-26.93875323652,3.2,-55.20570756731));
|
||||
#328 = VERTEX_POINT('',#329);
|
||||
#329 = CARTESIAN_POINT('',(-41.17904151244,3.2,-10.52828909594));
|
||||
#330 = LINE('',#331,#332);
|
||||
#331 = CARTESIAN_POINT('',(-32.39120436163,3.2,-38.09921113329));
|
||||
#332 = VECTOR('',#333,1.);
|
||||
#333 = DIRECTION('',(-0.30368282823,0.,0.952773183837));
|
||||
#334 = ORIENTED_EDGE('',*,*,#335,.F.);
|
||||
#335 = EDGE_CURVE('',#336,#328,#338,.T.);
|
||||
#336 = VERTEX_POINT('',#337);
|
||||
#337 = CARTESIAN_POINT('',(-39.69176606491,3.2,-4.408923352436));
|
||||
#338 = CIRCLE('',#339,6.054044962965);
|
||||
#339 = AXIS2_PLACEMENT_3D('',#340,#341,#342);
|
||||
#340 = CARTESIAN_POINT('',(-35.41090981799,3.2,-8.689779599357));
|
||||
#341 = DIRECTION('',(0.,-1.,0.));
|
||||
#342 = DIRECTION('',(-1.,0.,0.));
|
||||
#343 = ORIENTED_EDGE('',*,*,#344,.T.);
|
||||
#344 = EDGE_CURVE('',#336,#345,#347,.T.);
|
||||
#345 = VERTEX_POINT('',#346);
|
||||
#346 = CARTESIAN_POINT('',(-36.85603142851,3.2,-1.573188716044));
|
||||
#347 = LINE('',#348,#349);
|
||||
#348 = CARTESIAN_POINT('',(-36.19319006079,3.2,-0.910347348321));
|
||||
#349 = VECTOR('',#350,1.);
|
||||
#350 = DIRECTION('',(0.707106781187,0.,0.707106781187));
|
||||
#351 = ORIENTED_EDGE('',*,*,#352,.F.);
|
||||
#352 = EDGE_CURVE('',#353,#345,#355,.T.);
|
||||
#353 = VERTEX_POINT('',#354);
|
||||
#354 = CARTESIAN_POINT('',(-32.57517518159,3.2,0.2));
|
||||
#355 = CIRCLE('',#356,6.054044962965);
|
||||
#356 = AXIS2_PLACEMENT_3D('',#357,#358,#359);
|
||||
#357 = CARTESIAN_POINT('',(-32.57517518159,3.2,-5.854044962965));
|
||||
#358 = DIRECTION('',(0.,-1.,0.));
|
||||
#359 = DIRECTION('',(-1.,0.,0.));
|
||||
#360 = ORIENTED_EDGE('',*,*,#361,.T.);
|
||||
#361 = EDGE_CURVE('',#353,#362,#364,.T.);
|
||||
#362 = VERTEX_POINT('',#363);
|
||||
#363 = CARTESIAN_POINT('',(35.082842712475,3.2,0.2));
|
||||
#364 = LINE('',#365,#366);
|
||||
#365 = CARTESIAN_POINT('',(-7.942265537672,3.2,0.2));
|
||||
#366 = VECTOR('',#367,1.);
|
||||
#367 = DIRECTION('',(1.,0.,0.));
|
||||
#368 = ORIENTED_EDGE('',*,*,#369,.F.);
|
||||
#369 = EDGE_CURVE('',#243,#362,#370,.T.);
|
||||
#370 = LINE('',#371,#372);
|
||||
#371 = CARTESIAN_POINT('',(36.596246576251,3.2,-1.313403863776));
|
||||
#372 = VECTOR('',#373,1.);
|
||||
#373 = DIRECTION('',(-0.707106781187,0.,0.707106781187));
|
||||
#374 = ORIENTED_EDGE('',*,*,#250,.F.);
|
||||
#375 = ORIENTED_EDGE('',*,*,#376,.F.);
|
||||
#376 = EDGE_CURVE('',#377,#234,#379,.T.);
|
||||
#377 = VERTEX_POINT('',#378);
|
||||
#378 = CARTESIAN_POINT('',(32.703857168398,3.2,-60.78483712899));
|
||||
#379 = LINE('',#380,#381);
|
||||
#380 = CARTESIAN_POINT('',(16.008242280412,3.2,-44.62779994931));
|
||||
#381 = VECTOR('',#382,1.);
|
||||
#382 = DIRECTION('',(-0.718602345805,0.,0.695421216677));
|
||||
#383 = ORIENTED_EDGE('',*,*,#384,.F.);
|
||||
#384 = EDGE_CURVE('',#385,#377,#387,.T.);
|
||||
#385 = VERTEX_POINT('',#386);
|
||||
#386 = CARTESIAN_POINT('',(26.715163243538,3.2,-66.97315781796));
|
||||
#387 = LINE('',#388,#389);
|
||||
#388 = CARTESIAN_POINT('',(30.25240665457,3.2,-63.31800414721));
|
||||
#389 = VECTOR('',#390,1.);
|
||||
#390 = DIRECTION('',(0.695421216677,0.,0.718602345805));
|
||||
#391 = ORIENTED_EDGE('',*,*,#392,.F.);
|
||||
#392 = EDGE_CURVE('',#393,#385,#395,.T.);
|
||||
#393 = VERTEX_POINT('',#394);
|
||||
#394 = CARTESIAN_POINT('',(20.532019001374,3.2,-60.98947335481));
|
||||
#395 = LINE('',#396,#397);
|
||||
#396 = CARTESIAN_POINT('',(9.922784884512,3.2,-50.72247862452));
|
||||
#397 = VECTOR('',#398,1.);
|
||||
#398 = DIRECTION('',(0.718602345805,0.,-0.695421216677));
|
||||
#399 = ORIENTED_EDGE('',*,*,#400,.F.);
|
||||
#400 = EDGE_CURVE('',#401,#393,#403,.T.);
|
||||
#401 = VERTEX_POINT('',#402);
|
||||
#402 = CARTESIAN_POINT('',(-20.53201900137,3.2,-60.98947335481));
|
||||
#403 = LINE('',#404,#405);
|
||||
#404 = CARTESIAN_POINT('',(5.354765181569,3.2,-60.98947335481));
|
||||
#405 = VECTOR('',#406,1.);
|
||||
#406 = DIRECTION('',(1.,0.,0.));
|
||||
#407 = ORIENTED_EDGE('',*,*,#408,.T.);
|
||||
#408 = EDGE_CURVE('',#401,#409,#411,.T.);
|
||||
#409 = VERTEX_POINT('',#410);
|
||||
#410 = CARTESIAN_POINT('',(-25.53052705686,3.2,-65.82673637491));
|
||||
#411 = LINE('',#412,#413);
|
||||
#412 = CARTESIAN_POINT('',(-9.454421870603,3.2,-50.26922436104));
|
||||
#413 = VECTOR('',#414,1.);
|
||||
#414 = DIRECTION('',(-0.718602345805,0.,-0.695421216677));
|
||||
#415 = ORIENTED_EDGE('',*,*,#416,.F.);
|
||||
#416 = EDGE_CURVE('',#417,#409,#419,.T.);
|
||||
#417 = VERTEX_POINT('',#418);
|
||||
#418 = CARTESIAN_POINT('',(-27.86158468659,3.2,-65.78852163128));
|
||||
#419 = CIRCLE('',#420,1.648528137424);
|
||||
#420 = AXIS2_PLACEMENT_3D('',#421,#422,#423);
|
||||
#421 = CARTESIAN_POINT('',(-26.67694849991,3.2,-64.64210018823));
|
||||
#422 = DIRECTION('',(0.,-1.,0.));
|
||||
#423 = DIRECTION('',(-1.,0.,0.));
|
||||
#424 = ORIENTED_EDGE('',*,*,#425,.T.);
|
||||
#425 = EDGE_CURVE('',#417,#426,#428,.T.);
|
||||
#426 = VERTEX_POINT('',#427);
|
||||
#427 = CARTESIAN_POINT('',(-29.08766684168,3.2,-64.52156932717));
|
||||
#428 = LINE('',#429,#430);
|
||||
#429 = CARTESIAN_POINT('',(-29.44004200272,3.2,-64.15744811364));
|
||||
#430 = VECTOR('',#431,1.);
|
||||
#431 = DIRECTION('',(-0.695421216677,0.,0.718602345805));
|
||||
#432 = ORIENTED_EDGE('',*,*,#433,.F.);
|
||||
#433 = EDGE_CURVE('',#434,#426,#436,.T.);
|
||||
#434 = VERTEX_POINT('',#435);
|
||||
#435 = CARTESIAN_POINT('',(-28.96712497021,3.2,-57.16864680227));
|
||||
#436 = CIRCLE('',#437,5.2);
|
||||
#437 = AXIS2_PLACEMENT_3D('',#438,#439,#440);
|
||||
#438 = CARTESIAN_POINT('',(-25.35093464349,3.2,-60.90537900045));
|
||||
#439 = DIRECTION('',(0.,-1.,0.));
|
||||
#440 = DIRECTION('',(-1.,0.,0.));
|
||||
#441 = ORIENTED_EDGE('',*,*,#442,.T.);
|
||||
#442 = EDGE_CURVE('',#434,#326,#443,.T.);
|
||||
#443 = LINE('',#444,#445);
|
||||
#444 = CARTESIAN_POINT('',(-14.90185526362,3.2,-43.55710346492));
|
||||
#445 = VECTOR('',#446,1.);
|
||||
#446 = DIRECTION('',(0.718602345805,0.,0.695421216677));
|
||||
#447 = FACE_BOUND('',#448,.T.);
|
||||
#448 = EDGE_LOOP('',(#449,#459,#467,#475));
|
||||
#449 = ORIENTED_EDGE('',*,*,#450,.F.);
|
||||
#450 = EDGE_CURVE('',#451,#453,#455,.T.);
|
||||
#451 = VERTEX_POINT('',#452);
|
||||
#452 = CARTESIAN_POINT('',(5.809375885494,3.2,-13.06417917474));
|
||||
#453 = VERTEX_POINT('',#454);
|
||||
#454 = CARTESIAN_POINT('',(2.688069798796,3.2,-49.64588621989));
|
||||
#455 = LINE('',#456,#457);
|
||||
#456 = CARTESIAN_POINT('',(4.914157977861,3.2,-23.55613296875));
|
||||
#457 = VECTOR('',#458,1.);
|
||||
#458 = DIRECTION('',(-8.501532861635E-02,0.,-0.996379643459));
|
||||
#459 = ORIENTED_EDGE('',*,*,#460,.F.);
|
||||
#460 = EDGE_CURVE('',#461,#451,#463,.T.);
|
||||
#461 = VERTEX_POINT('',#462);
|
||||
#462 = CARTESIAN_POINT('',(-5.809375885494,3.2,-13.06417917474));
|
||||
#463 = LINE('',#464,#465);
|
||||
#464 = CARTESIAN_POINT('',(1.615747408047,3.2,-13.06417917474));
|
||||
#465 = VECTOR('',#466,1.);
|
||||
#466 = DIRECTION('',(1.,0.,-3.066574716487E-16));
|
||||
#467 = ORIENTED_EDGE('',*,*,#468,.F.);
|
||||
#468 = EDGE_CURVE('',#469,#461,#471,.T.);
|
||||
#469 = VERTEX_POINT('',#470);
|
||||
#470 = CARTESIAN_POINT('',(-2.688069798796,3.2,-49.64588621989));
|
||||
#471 = LINE('',#472,#473);
|
||||
#472 = CARTESIAN_POINT('',(-4.890802006217,3.2,-23.82986495424));
|
||||
#473 = VECTOR('',#474,1.);
|
||||
#474 = DIRECTION('',(-8.501532861635E-02,0.,0.996379643459));
|
||||
#475 = ORIENTED_EDGE('',*,*,#476,.F.);
|
||||
#476 = EDGE_CURVE('',#453,#469,#477,.T.);
|
||||
#477 = LINE('',#478,#479);
|
||||
#478 = CARTESIAN_POINT('',(1.615747408047,3.2,-49.64588621989));
|
||||
#479 = VECTOR('',#480,1.);
|
||||
#480 = DIRECTION('',(-1.,0.,0.));
|
||||
#481 = FACE_BOUND('',#482,.T.);
|
||||
#482 = EDGE_LOOP('',(#483));
|
||||
#483 = ORIENTED_EDGE('',*,*,#484,.F.);
|
||||
#484 = EDGE_CURVE('',#485,#485,#487,.T.);
|
||||
#485 = VERTEX_POINT('',#486);
|
||||
#486 = CARTESIAN_POINT('',(-11.6927539352,3.2,-48.00951684793));
|
||||
#487 = CIRCLE('',#488,4.735522705283);
|
||||
#488 = AXIS2_PLACEMENT_3D('',#489,#490,#491);
|
||||
#489 = CARTESIAN_POINT('',(-16.42827664048,3.2,-48.00951684793));
|
||||
#490 = DIRECTION('',(-0.,1.,0.));
|
||||
#491 = DIRECTION('',(1.,0.,0.));
|
||||
#492 = PLANE('',#493);
|
||||
#493 = AXIS2_PLACEMENT_3D('',#494,#495,#496);
|
||||
#494 = CARTESIAN_POINT('',(0.403056515455,3.2,-33.34517655273));
|
||||
#495 = DIRECTION('',(0.,1.,0.));
|
||||
#496 = DIRECTION('',(1.,0.,0.));
|
||||
#497 = ADVANCED_FACE('',(#498),#509,.F.);
|
||||
#498 = FACE_BOUND('',#499,.F.);
|
||||
#499 = EDGE_LOOP('',(#500,#506,#507,#508));
|
||||
#500 = ORIENTED_EDGE('',*,*,#501,.F.);
|
||||
#501 = EDGE_CURVE('',#168,#377,#502,.T.);
|
||||
#502 = LINE('',#503,#504);
|
||||
#503 = CARTESIAN_POINT('',(32.703857168398,3.,-60.78483712899));
|
||||
#504 = VECTOR('',#505,1.);
|
||||
#505 = DIRECTION('',(0.,1.,0.));
|
||||
#506 = ORIENTED_EDGE('',*,*,#167,.T.);
|
||||
#507 = ORIENTED_EDGE('',*,*,#233,.F.);
|
||||
#508 = ORIENTED_EDGE('',*,*,#376,.F.);
|
||||
#509 = PLANE('',#510);
|
||||
#510 = AXIS2_PLACEMENT_3D('',#511,#512,#513);
|
||||
#511 = CARTESIAN_POINT('',(29.704873980143,3.,-57.88259703786));
|
||||
#512 = DIRECTION('',(-0.695421216677,0.,-0.718602345805));
|
||||
#513 = DIRECTION('',(-0.718602345805,0.,0.695421216677));
|
||||
#514 = ADVANCED_FACE('',(#515),#526,.F.);
|
||||
#515 = FACE_BOUND('',#516,.F.);
|
||||
#516 = EDGE_LOOP('',(#517,#523,#524,#525));
|
||||
#517 = ORIENTED_EDGE('',*,*,#518,.F.);
|
||||
#518 = EDGE_CURVE('',#176,#385,#519,.T.);
|
||||
#519 = LINE('',#520,#521);
|
||||
#520 = CARTESIAN_POINT('',(26.715163243538,3.,-66.97315781796));
|
||||
#521 = VECTOR('',#522,1.);
|
||||
#522 = DIRECTION('',(0.,1.,0.));
|
||||
#523 = ORIENTED_EDGE('',*,*,#175,.T.);
|
||||
#524 = ORIENTED_EDGE('',*,*,#501,.T.);
|
||||
#525 = ORIENTED_EDGE('',*,*,#384,.F.);
|
||||
#526 = PLANE('',#527);
|
||||
#527 = AXIS2_PLACEMENT_3D('',#528,#529,#530);
|
||||
#528 = CARTESIAN_POINT('',(29.709510205968,3.,-63.87899747347));
|
||||
#529 = DIRECTION('',(-0.718602345805,0.,0.695421216677));
|
||||
#530 = DIRECTION('',(0.695421216677,0.,0.718602345805));
|
||||
#531 = ADVANCED_FACE('',(#532),#543,.F.);
|
||||
#532 = FACE_BOUND('',#533,.F.);
|
||||
#533 = EDGE_LOOP('',(#534,#540,#541,#542));
|
||||
#534 = ORIENTED_EDGE('',*,*,#535,.T.);
|
||||
#535 = EDGE_CURVE('',#393,#184,#536,.T.);
|
||||
#536 = LINE('',#537,#538);
|
||||
#537 = CARTESIAN_POINT('',(20.532019001374,3.,-60.98947335481));
|
||||
#538 = VECTOR('',#539,1.);
|
||||
#539 = DIRECTION('',(0.,-1.,0.));
|
||||
#540 = ORIENTED_EDGE('',*,*,#183,.T.);
|
||||
#541 = ORIENTED_EDGE('',*,*,#518,.T.);
|
||||
#542 = ORIENTED_EDGE('',*,*,#392,.F.);
|
||||
#543 = PLANE('',#544);
|
||||
#544 = AXIS2_PLACEMENT_3D('',#545,#546,#547);
|
||||
#545 = CARTESIAN_POINT('',(23.522653113203,3.,-63.8836336993));
|
||||
#546 = DIRECTION('',(0.695421216677,0.,0.718602345805));
|
||||
#547 = DIRECTION('',(0.718602345805,0.,-0.695421216677));
|
||||
#548 = ADVANCED_FACE('',(#549),#560,.F.);
|
||||
#549 = FACE_BOUND('',#550,.F.);
|
||||
#550 = EDGE_LOOP('',(#551,#557,#558,#559));
|
||||
#551 = ORIENTED_EDGE('',*,*,#552,.F.);
|
||||
#552 = EDGE_CURVE('',#192,#401,#553,.T.);
|
||||
#553 = LINE('',#554,#555);
|
||||
#554 = CARTESIAN_POINT('',(-20.53201900137,3.,-60.98947335481));
|
||||
#555 = VECTOR('',#556,1.);
|
||||
#556 = DIRECTION('',(0.,1.,0.));
|
||||
#557 = ORIENTED_EDGE('',*,*,#191,.T.);
|
||||
#558 = ORIENTED_EDGE('',*,*,#535,.F.);
|
||||
#559 = ORIENTED_EDGE('',*,*,#400,.F.);
|
||||
#560 = PLANE('',#561);
|
||||
#561 = AXIS2_PLACEMENT_3D('',#562,#563,#564);
|
||||
#562 = CARTESIAN_POINT('',(10.306473847682,3.,-60.98947335481));
|
||||
#563 = DIRECTION('',(0.,0.,1.));
|
||||
#564 = DIRECTION('',(0.,-1.,0.));
|
||||
#565 = ADVANCED_FACE('',(#566),#577,.T.);
|
||||
#566 = FACE_BOUND('',#567,.T.);
|
||||
#567 = EDGE_LOOP('',(#568,#574,#575,#576));
|
||||
#568 = ORIENTED_EDGE('',*,*,#569,.F.);
|
||||
#569 = EDGE_CURVE('',#409,#200,#570,.T.);
|
||||
#570 = LINE('',#571,#572);
|
||||
#571 = CARTESIAN_POINT('',(-25.53052705686,3.,-65.82673637491));
|
||||
#572 = VECTOR('',#573,1.);
|
||||
#573 = DIRECTION('',(0.,-1.,0.));
|
||||
#574 = ORIENTED_EDGE('',*,*,#408,.F.);
|
||||
#575 = ORIENTED_EDGE('',*,*,#552,.F.);
|
||||
#576 = ORIENTED_EDGE('',*,*,#199,.T.);
|
||||
#577 = PLANE('',#578);
|
||||
#578 = AXIS2_PLACEMENT_3D('',#579,#580,#581);
|
||||
#579 = CARTESIAN_POINT('',(-23.00219525444,3.,-63.37996509944));
|
||||
#580 = DIRECTION('',(0.695421216677,0.,-0.718602345805));
|
||||
#581 = DIRECTION('',(-0.718602345805,0.,-0.695421216677));
|
||||
#582 = ADVANCED_FACE('',(#583),#594,.T.);
|
||||
#583 = FACE_BOUND('',#584,.T.);
|
||||
#584 = EDGE_LOOP('',(#585,#591,#592,#593));
|
||||
#585 = ORIENTED_EDGE('',*,*,#586,.F.);
|
||||
#586 = EDGE_CURVE('',#417,#83,#587,.T.);
|
||||
#587 = LINE('',#588,#589);
|
||||
#588 = CARTESIAN_POINT('',(-27.86158468659,3.,-65.78852163128));
|
||||
#589 = VECTOR('',#590,1.);
|
||||
#590 = DIRECTION('',(0.,-1.,0.));
|
||||
#591 = ORIENTED_EDGE('',*,*,#416,.T.);
|
||||
#592 = ORIENTED_EDGE('',*,*,#569,.T.);
|
||||
#593 = ORIENTED_EDGE('',*,*,#207,.F.);
|
||||
#594 = CYLINDRICAL_SURFACE('',#595,1.648528137424);
|
||||
#595 = AXIS2_PLACEMENT_3D('',#596,#597,#598);
|
||||
#596 = CARTESIAN_POINT('',(-26.67694849991,3.,-64.64210018823));
|
||||
#597 = DIRECTION('',(0.,-1.,0.));
|
||||
#598 = DIRECTION('',(-1.,0.,0.));
|
||||
#599 = ADVANCED_FACE('',(#600),#611,.T.);
|
||||
#600 = FACE_BOUND('',#601,.T.);
|
||||
#601 = EDGE_LOOP('',(#602,#608,#609,#610));
|
||||
#602 = ORIENTED_EDGE('',*,*,#603,.F.);
|
||||
#603 = EDGE_CURVE('',#426,#85,#604,.T.);
|
||||
#604 = LINE('',#605,#606);
|
||||
#605 = CARTESIAN_POINT('',(-29.08766684168,3.,-64.52156932717));
|
||||
#606 = VECTOR('',#607,1.);
|
||||
#607 = DIRECTION('',(0.,-1.,0.));
|
||||
#608 = ORIENTED_EDGE('',*,*,#425,.F.);
|
||||
#609 = ORIENTED_EDGE('',*,*,#586,.T.);
|
||||
#610 = ORIENTED_EDGE('',*,*,#82,.T.);
|
||||
#611 = PLANE('',#612);
|
||||
#612 = AXIS2_PLACEMENT_3D('',#613,#614,#615);
|
||||
#613 = CARTESIAN_POINT('',(-28.47462576413,3.,-65.15504547923));
|
||||
#614 = DIRECTION('',(-0.718602345805,0.,-0.695421216677));
|
||||
#615 = DIRECTION('',(-0.695421216677,0.,0.718602345805));
|
||||
#616 = ADVANCED_FACE('',(#617),#628,.T.);
|
||||
#617 = FACE_BOUND('',#618,.T.);
|
||||
#618 = EDGE_LOOP('',(#619,#625,#626,#627));
|
||||
#619 = ORIENTED_EDGE('',*,*,#620,.F.);
|
||||
#620 = EDGE_CURVE('',#434,#93,#621,.T.);
|
||||
#621 = LINE('',#622,#623);
|
||||
#622 = CARTESIAN_POINT('',(-28.96712497021,3.,-57.16864680227));
|
||||
#623 = VECTOR('',#624,1.);
|
||||
#624 = DIRECTION('',(0.,-1.,0.));
|
||||
#625 = ORIENTED_EDGE('',*,*,#433,.T.);
|
||||
#626 = ORIENTED_EDGE('',*,*,#603,.T.);
|
||||
#627 = ORIENTED_EDGE('',*,*,#92,.F.);
|
||||
#628 = CYLINDRICAL_SURFACE('',#629,5.2);
|
||||
#629 = AXIS2_PLACEMENT_3D('',#630,#631,#632);
|
||||
#630 = CARTESIAN_POINT('',(-25.35093464349,3.,-60.90537900045));
|
||||
#631 = DIRECTION('',(0.,-1.,0.));
|
||||
#632 = DIRECTION('',(-1.,0.,0.));
|
||||
#633 = ADVANCED_FACE('',(#634),#645,.T.);
|
||||
#634 = FACE_BOUND('',#635,.T.);
|
||||
#635 = EDGE_LOOP('',(#636,#642,#643,#644));
|
||||
#636 = ORIENTED_EDGE('',*,*,#637,.F.);
|
||||
#637 = EDGE_CURVE('',#326,#102,#638,.T.);
|
||||
#638 = LINE('',#639,#640);
|
||||
#639 = CARTESIAN_POINT('',(-26.93875323652,3.,-55.20570756731));
|
||||
#640 = VECTOR('',#641,1.);
|
||||
#641 = DIRECTION('',(0.,-1.,0.));
|
||||
#642 = ORIENTED_EDGE('',*,*,#442,.F.);
|
||||
#643 = ORIENTED_EDGE('',*,*,#620,.T.);
|
||||
#644 = ORIENTED_EDGE('',*,*,#101,.T.);
|
||||
#645 = PLANE('',#646);
|
||||
#646 = AXIS2_PLACEMENT_3D('',#647,#648,#649);
|
||||
#647 = CARTESIAN_POINT('',(-27.90836811563,3.,-56.14404399617));
|
||||
#648 = DIRECTION('',(-0.695421216677,0.,0.718602345805));
|
||||
#649 = DIRECTION('',(0.718602345805,0.,0.695421216677));
|
||||
#650 = ADVANCED_FACE('',(#651),#662,.T.);
|
||||
#651 = FACE_BOUND('',#652,.T.);
|
||||
#652 = EDGE_LOOP('',(#653,#659,#660,#661));
|
||||
#653 = ORIENTED_EDGE('',*,*,#654,.F.);
|
||||
#654 = EDGE_CURVE('',#328,#110,#655,.T.);
|
||||
#655 = LINE('',#656,#657);
|
||||
#656 = CARTESIAN_POINT('',(-41.17904151244,3.,-10.52828909594));
|
||||
#657 = VECTOR('',#658,1.);
|
||||
#658 = DIRECTION('',(0.,-1.,0.));
|
||||
#659 = ORIENTED_EDGE('',*,*,#325,.F.);
|
||||
#660 = ORIENTED_EDGE('',*,*,#637,.T.);
|
||||
#661 = ORIENTED_EDGE('',*,*,#109,.T.);
|
||||
#662 = PLANE('',#663);
|
||||
#663 = AXIS2_PLACEMENT_3D('',#664,#665,#666);
|
||||
#664 = CARTESIAN_POINT('',(-34.04006158346,3.,-32.92609366041));
|
||||
#665 = DIRECTION('',(-0.952773183837,0.,-0.30368282823));
|
||||
#666 = DIRECTION('',(-0.30368282823,0.,0.952773183837));
|
||||
#667 = ADVANCED_FACE('',(#668),#679,.T.);
|
||||
#668 = FACE_BOUND('',#669,.T.);
|
||||
#669 = EDGE_LOOP('',(#670,#676,#677,#678));
|
||||
#670 = ORIENTED_EDGE('',*,*,#671,.F.);
|
||||
#671 = EDGE_CURVE('',#336,#118,#672,.T.);
|
||||
#672 = LINE('',#673,#674);
|
||||
#673 = CARTESIAN_POINT('',(-39.69176606491,3.,-4.408923352436));
|
||||
#674 = VECTOR('',#675,1.);
|
||||
#675 = DIRECTION('',(0.,-1.,0.));
|
||||
#676 = ORIENTED_EDGE('',*,*,#335,.T.);
|
||||
#677 = ORIENTED_EDGE('',*,*,#654,.T.);
|
||||
#678 = ORIENTED_EDGE('',*,*,#117,.F.);
|
||||
#679 = CYLINDRICAL_SURFACE('',#680,6.054044962965);
|
||||
#680 = AXIS2_PLACEMENT_3D('',#681,#682,#683);
|
||||
#681 = CARTESIAN_POINT('',(-35.41090981799,3.,-8.689779599357));
|
||||
#682 = DIRECTION('',(0.,-1.,0.));
|
||||
#683 = DIRECTION('',(-1.,0.,0.));
|
||||
#684 = ADVANCED_FACE('',(#685),#696,.T.);
|
||||
#685 = FACE_BOUND('',#686,.T.);
|
||||
#686 = EDGE_LOOP('',(#687,#693,#694,#695));
|
||||
#687 = ORIENTED_EDGE('',*,*,#688,.F.);
|
||||
#688 = EDGE_CURVE('',#345,#127,#689,.T.);
|
||||
#689 = LINE('',#690,#691);
|
||||
#690 = CARTESIAN_POINT('',(-36.85603142851,3.,-1.573188716044));
|
||||
#691 = VECTOR('',#692,1.);
|
||||
#692 = DIRECTION('',(0.,-1.,0.));
|
||||
#693 = ORIENTED_EDGE('',*,*,#344,.F.);
|
||||
#694 = ORIENTED_EDGE('',*,*,#671,.T.);
|
||||
#695 = ORIENTED_EDGE('',*,*,#126,.T.);
|
||||
#696 = PLANE('',#697);
|
||||
#697 = AXIS2_PLACEMENT_3D('',#698,#699,#700);
|
||||
#698 = CARTESIAN_POINT('',(-38.27389874671,3.,-2.99105603424));
|
||||
#699 = DIRECTION('',(-0.707106781187,0.,0.707106781187));
|
||||
#700 = DIRECTION('',(0.707106781187,0.,0.707106781187));
|
||||
#701 = ADVANCED_FACE('',(#702),#713,.T.);
|
||||
#702 = FACE_BOUND('',#703,.T.);
|
||||
#703 = EDGE_LOOP('',(#704,#710,#711,#712));
|
||||
#704 = ORIENTED_EDGE('',*,*,#705,.F.);
|
||||
#705 = EDGE_CURVE('',#353,#135,#706,.T.);
|
||||
#706 = LINE('',#707,#708);
|
||||
#707 = CARTESIAN_POINT('',(-32.57517518159,3.,0.2));
|
||||
#708 = VECTOR('',#709,1.);
|
||||
#709 = DIRECTION('',(0.,-1.,0.));
|
||||
#710 = ORIENTED_EDGE('',*,*,#352,.T.);
|
||||
#711 = ORIENTED_EDGE('',*,*,#688,.T.);
|
||||
#712 = ORIENTED_EDGE('',*,*,#134,.F.);
|
||||
#713 = CYLINDRICAL_SURFACE('',#714,6.054044962965);
|
||||
#714 = AXIS2_PLACEMENT_3D('',#715,#716,#717);
|
||||
#715 = CARTESIAN_POINT('',(-32.57517518159,3.,-5.854044962965));
|
||||
#716 = DIRECTION('',(0.,-1.,0.));
|
||||
#717 = DIRECTION('',(-1.,0.,0.));
|
||||
#718 = ADVANCED_FACE('',(#719),#730,.T.);
|
||||
#719 = FACE_BOUND('',#720,.T.);
|
||||
#720 = EDGE_LOOP('',(#721,#727,#728,#729));
|
||||
#721 = ORIENTED_EDGE('',*,*,#722,.F.);
|
||||
#722 = EDGE_CURVE('',#362,#144,#723,.T.);
|
||||
#723 = LINE('',#724,#725);
|
||||
#724 = CARTESIAN_POINT('',(35.082842712475,3.,0.2));
|
||||
#725 = VECTOR('',#726,1.);
|
||||
#726 = DIRECTION('',(0.,-1.,0.));
|
||||
#727 = ORIENTED_EDGE('',*,*,#361,.F.);
|
||||
#728 = ORIENTED_EDGE('',*,*,#705,.T.);
|
||||
#729 = ORIENTED_EDGE('',*,*,#143,.T.);
|
||||
#730 = PLANE('',#731);
|
||||
#731 = AXIS2_PLACEMENT_3D('',#732,#733,#734);
|
||||
#732 = CARTESIAN_POINT('',(-16.28758759079,3.,0.2));
|
||||
#733 = DIRECTION('',(0.,0.,1.));
|
||||
#734 = DIRECTION('',(0.,-1.,0.));
|
||||
#735 = ADVANCED_FACE('',(#736),#742,.F.);
|
||||
#736 = FACE_BOUND('',#737,.F.);
|
||||
#737 = EDGE_LOOP('',(#738,#739,#740,#741));
|
||||
#738 = ORIENTED_EDGE('',*,*,#151,.T.);
|
||||
#739 = ORIENTED_EDGE('',*,*,#722,.F.);
|
||||
#740 = ORIENTED_EDGE('',*,*,#369,.F.);
|
||||
#741 = ORIENTED_EDGE('',*,*,#242,.T.);
|
||||
#742 = PLANE('',#743);
|
||||
#743 = AXIS2_PLACEMENT_3D('',#744,#745,#746);
|
||||
#744 = CARTESIAN_POINT('',(38.67695526217,3.,-3.394112549695));
|
||||
#745 = DIRECTION('',(-0.707106781187,0.,-0.707106781187));
|
||||
#746 = DIRECTION('',(-0.707106781187,0.,0.707106781187));
|
||||
#747 = ADVANCED_FACE('',(#748),#765,.T.);
|
||||
#748 = FACE_BOUND('',#749,.T.);
|
||||
#749 = EDGE_LOOP('',(#750,#758,#764));
|
||||
#750 = ORIENTED_EDGE('',*,*,#751,.T.);
|
||||
#751 = EDGE_CURVE('',#451,#752,#754,.T.);
|
||||
#752 = VERTEX_POINT('',#753);
|
||||
#753 = CARTESIAN_POINT('',(3.256654205567E-15,17.8572529153,
|
||||
-18.39898295202));
|
||||
#754 = LINE('',#755,#756);
|
||||
#755 = CARTESIAN_POINT('',(5.649679875255,3.602918464526,-13.21082950266
|
||||
));
|
||||
#756 = VECTOR('',#757,1.);
|
||||
#757 = DIRECTION('',(-0.349023821871,0.880598971639,-0.320511814002));
|
||||
#758 = ORIENTED_EDGE('',*,*,#759,.T.);
|
||||
#759 = EDGE_CURVE('',#752,#461,#760,.T.);
|
||||
#760 = LINE('',#761,#762);
|
||||
#761 = CARTESIAN_POINT('',(-5.275833888477,4.546144602338,
|
||||
-13.55413574101));
|
||||
#762 = VECTOR('',#763,1.);
|
||||
#763 = DIRECTION('',(-0.349023821871,-0.880598971639,0.320511814002));
|
||||
#764 = ORIENTED_EDGE('',*,*,#460,.T.);
|
||||
#765 = PLANE('',#766);
|
||||
#766 = AXIS2_PLACEMENT_3D('',#767,#768,#769);
|
||||
#767 = CARTESIAN_POINT('',(2.828438300639,3.068404028665,-13.01628215822
|
||||
));
|
||||
#768 = DIRECTION('',(2.881637632171E-16,0.342020143326,0.939692620786));
|
||||
#769 = DIRECTION('',(-1.048830324052E-16,0.939692620786,-0.342020143326)
|
||||
);
|
||||
#770 = ADVANCED_FACE('',(#771),#789,.T.);
|
||||
#771 = FACE_BOUND('',#772,.T.);
|
||||
#772 = EDGE_LOOP('',(#773,#781,#782,#783));
|
||||
#773 = ORIENTED_EDGE('',*,*,#774,.T.);
|
||||
#774 = EDGE_CURVE('',#775,#752,#777,.T.);
|
||||
#775 = VERTEX_POINT('',#776);
|
||||
#776 = CARTESIAN_POINT('',(1.480297366167E-15,11.242400581089,
|
||||
-46.71869179633));
|
||||
#777 = LINE('',#778,#779);
|
||||
#778 = CARTESIAN_POINT('',(2.6645352591E-15,18.133069549222,
|
||||
-17.21814831317));
|
||||
#779 = VECTOR('',#780,1.);
|
||||
#780 = DIRECTION('',(5.275122655166E-17,0.227455280238,0.97378852709));
|
||||
#781 = ORIENTED_EDGE('',*,*,#751,.F.);
|
||||
#782 = ORIENTED_EDGE('',*,*,#450,.T.);
|
||||
#783 = ORIENTED_EDGE('',*,*,#784,.T.);
|
||||
#784 = EDGE_CURVE('',#453,#775,#785,.T.);
|
||||
#785 = LINE('',#786,#787);
|
||||
#786 = CARTESIAN_POINT('',(1.103762571829,7.940067898719,-47.92064259636
|
||||
));
|
||||
#787 = VECTOR('',#788,1.);
|
||||
#788 = DIRECTION('',(-0.299648208284,0.896513522642,0.326304236859));
|
||||
#789 = PLANE('',#790);
|
||||
#790 = AXIS2_PLACEMENT_3D('',#791,#792,#793);
|
||||
#791 = CARTESIAN_POINT('',(5.823691883056,3.068404028665,-13.45978839622
|
||||
));
|
||||
#792 = DIRECTION('',(0.93629059846,0.342020143326,-7.988827695448E-02));
|
||||
#793 = DIRECTION('',(-0.340781908463,0.939692620786,2.907695487824E-02)
|
||||
);
|
||||
#794 = ADVANCED_FACE('',(#795),#805,.T.);
|
||||
#795 = FACE_BOUND('',#796,.T.);
|
||||
#796 = EDGE_LOOP('',(#797,#803,#804));
|
||||
#797 = ORIENTED_EDGE('',*,*,#798,.T.);
|
||||
#798 = EDGE_CURVE('',#469,#775,#799,.T.);
|
||||
#799 = LINE('',#800,#801);
|
||||
#800 = CARTESIAN_POINT('',(-0.871390517001,8.635298785064,
|
||||
-47.66759924779));
|
||||
#801 = VECTOR('',#802,1.);
|
||||
#802 = DIRECTION('',(0.299648208284,0.896513522642,0.326304236859));
|
||||
#803 = ORIENTED_EDGE('',*,*,#784,.F.);
|
||||
#804 = ORIENTED_EDGE('',*,*,#476,.T.);
|
||||
#805 = PLANE('',#806);
|
||||
#806 = AXIS2_PLACEMENT_3D('',#807,#808,#809);
|
||||
#807 = CARTESIAN_POINT('',(2.828438300639,3.068404028665,-49.69378323641
|
||||
));
|
||||
#808 = DIRECTION('',(0.,0.342020143326,-0.939692620786));
|
||||
#809 = DIRECTION('',(0.,0.939692620786,0.342020143326));
|
||||
#810 = ADVANCED_FACE('',(#811),#817,.T.);
|
||||
#811 = FACE_BOUND('',#812,.T.);
|
||||
#812 = EDGE_LOOP('',(#813,#814,#815,#816));
|
||||
#813 = ORIENTED_EDGE('',*,*,#468,.T.);
|
||||
#814 = ORIENTED_EDGE('',*,*,#759,.F.);
|
||||
#815 = ORIENTED_EDGE('',*,*,#774,.F.);
|
||||
#816 = ORIENTED_EDGE('',*,*,#798,.F.);
|
||||
#817 = PLANE('',#818);
|
||||
#818 = AXIS2_PLACEMENT_3D('',#819,#820,#821);
|
||||
#819 = CARTESIAN_POINT('',(-5.782806207227,3.068404028665,
|
||||
-13.93896851312));
|
||||
#820 = DIRECTION('',(-0.93629059846,0.342020143326,-7.988827695448E-02)
|
||||
);
|
||||
#821 = DIRECTION('',(0.340781908463,0.939692620786,2.907695487824E-02));
|
||||
#822 = ADVANCED_FACE('',(#823),#834,.F.);
|
||||
#823 = FACE_BOUND('',#824,.F.);
|
||||
#824 = EDGE_LOOP('',(#825,#831,#832,#833));
|
||||
#825 = ORIENTED_EDGE('',*,*,#826,.F.);
|
||||
#826 = EDGE_CURVE('',#217,#485,#827,.T.);
|
||||
#827 = LINE('',#828,#829);
|
||||
#828 = CARTESIAN_POINT('',(-11.6927539352,-22.,-48.00951684793));
|
||||
#829 = VECTOR('',#830,1.);
|
||||
#830 = DIRECTION('',(0.,1.,0.));
|
||||
#831 = ORIENTED_EDGE('',*,*,#216,.T.);
|
||||
#832 = ORIENTED_EDGE('',*,*,#826,.T.);
|
||||
#833 = ORIENTED_EDGE('',*,*,#484,.F.);
|
||||
#834 = CYLINDRICAL_SURFACE('',#835,4.735522705283);
|
||||
#835 = AXIS2_PLACEMENT_3D('',#836,#837,#838);
|
||||
#836 = CARTESIAN_POINT('',(-16.42827664048,-22.,-48.00951684793));
|
||||
#837 = DIRECTION('',(0.,1.,0.));
|
||||
#838 = DIRECTION('',(1.,0.,0.));
|
||||
#839 = ADVANCED_FACE('',(#840),#851,.F.);
|
||||
#840 = FACE_BOUND('',#841,.F.);
|
||||
#841 = EDGE_LOOP('',(#842,#848,#849,#850));
|
||||
#842 = ORIENTED_EDGE('',*,*,#843,.F.);
|
||||
#843 = EDGE_CURVE('',#72,#315,#844,.T.);
|
||||
#844 = LINE('',#845,#846);
|
||||
#845 = CARTESIAN_POINT('',(21.163799345768,-22.,-48.00951684793));
|
||||
#846 = VECTOR('',#847,1.);
|
||||
#847 = DIRECTION('',(0.,1.,0.));
|
||||
#848 = ORIENTED_EDGE('',*,*,#71,.T.);
|
||||
#849 = ORIENTED_EDGE('',*,*,#843,.T.);
|
||||
#850 = ORIENTED_EDGE('',*,*,#314,.F.);
|
||||
#851 = CYLINDRICAL_SURFACE('',#852,4.735522705283);
|
||||
#852 = AXIS2_PLACEMENT_3D('',#853,#854,#855);
|
||||
#853 = CARTESIAN_POINT('',(16.428276640485,-22.,-48.00951684793));
|
||||
#854 = DIRECTION('',(0.,1.,0.));
|
||||
#855 = DIRECTION('',(1.,0.,0.));
|
||||
#856 = ADVANCED_FACE('',(#857),#873,.F.);
|
||||
#857 = FACE_BOUND('',#858,.F.);
|
||||
#858 = EDGE_LOOP('',(#859,#865,#866,#872));
|
||||
#859 = ORIENTED_EDGE('',*,*,#860,.F.);
|
||||
#860 = EDGE_CURVE('',#24,#265,#861,.T.);
|
||||
#861 = LINE('',#862,#863);
|
||||
#862 = CARTESIAN_POINT('',(16.626582997737,-22.,-8.940188245231));
|
||||
#863 = VECTOR('',#864,1.);
|
||||
#864 = DIRECTION('',(0.,1.,0.));
|
||||
#865 = ORIENTED_EDGE('',*,*,#63,.T.);
|
||||
#866 = ORIENTED_EDGE('',*,*,#867,.T.);
|
||||
#867 = EDGE_CURVE('',#56,#267,#868,.T.);
|
||||
#868 = LINE('',#869,#870);
|
||||
#869 = CARTESIAN_POINT('',(-5.329070518201E-15,-22.,-8.903751135252));
|
||||
#870 = VECTOR('',#871,1.);
|
||||
#871 = DIRECTION('',(0.,1.,0.));
|
||||
#872 = ORIENTED_EDGE('',*,*,#264,.F.);
|
||||
#873 = PLANE('',#874);
|
||||
#874 = AXIS2_PLACEMENT_3D('',#875,#876,#877);
|
||||
#875 = CARTESIAN_POINT('',(8.237581109188,-22.,-8.921803528809));
|
||||
#876 = DIRECTION('',(-2.191520817069E-03,0.,-0.999997598615));
|
||||
#877 = DIRECTION('',(-0.999997598615,0.,2.191520817069E-03));
|
||||
#878 = ADVANCED_FACE('',(#879),#890,.F.);
|
||||
#879 = FACE_BOUND('',#880,.F.);
|
||||
#880 = EDGE_LOOP('',(#881,#887,#888,#889));
|
||||
#881 = ORIENTED_EDGE('',*,*,#882,.T.);
|
||||
#882 = EDGE_CURVE('',#48,#299,#883,.T.);
|
||||
#883 = LINE('',#884,#885);
|
||||
#884 = CARTESIAN_POINT('',(-21.72552223146,-22.,-8.903751135252));
|
||||
#885 = VECTOR('',#886,1.);
|
||||
#886 = DIRECTION('',(0.,1.,0.));
|
||||
#887 = ORIENTED_EDGE('',*,*,#306,.F.);
|
||||
#888 = ORIENTED_EDGE('',*,*,#867,.F.);
|
||||
#889 = ORIENTED_EDGE('',*,*,#55,.T.);
|
||||
#890 = PLANE('',#891);
|
||||
#891 = AXIS2_PLACEMENT_3D('',#892,#893,#894);
|
||||
#892 = CARTESIAN_POINT('',(-10.96276111573,-22.,-8.903751135252));
|
||||
#893 = DIRECTION('',(0.,0.,-1.));
|
||||
#894 = DIRECTION('',(0.,1.,0.));
|
||||
#895 = ADVANCED_FACE('',(#896),#907,.F.);
|
||||
#896 = FACE_BOUND('',#897,.F.);
|
||||
#897 = EDGE_LOOP('',(#898,#904,#905,#906));
|
||||
#898 = ORIENTED_EDGE('',*,*,#899,.T.);
|
||||
#899 = EDGE_CURVE('',#40,#291,#900,.T.);
|
||||
#900 = LINE('',#901,#902);
|
||||
#901 = CARTESIAN_POINT('',(-21.72552223146,-22.,-3.734519760785));
|
||||
#902 = VECTOR('',#903,1.);
|
||||
#903 = DIRECTION('',(0.,1.,0.));
|
||||
#904 = ORIENTED_EDGE('',*,*,#298,.F.);
|
||||
#905 = ORIENTED_EDGE('',*,*,#882,.F.);
|
||||
#906 = ORIENTED_EDGE('',*,*,#47,.T.);
|
||||
#907 = PLANE('',#908);
|
||||
#908 = AXIS2_PLACEMENT_3D('',#909,#910,#911);
|
||||
#909 = CARTESIAN_POINT('',(-21.72552223146,-22.,-6.319135448019));
|
||||
#910 = DIRECTION('',(-1.,0.,0.));
|
||||
#911 = DIRECTION('',(0.,1.,0.));
|
||||
#912 = ADVANCED_FACE('',(#913),#924,.F.);
|
||||
#913 = FACE_BOUND('',#914,.F.);
|
||||
#914 = EDGE_LOOP('',(#915,#921,#922,#923));
|
||||
#915 = ORIENTED_EDGE('',*,*,#916,.T.);
|
||||
#916 = EDGE_CURVE('',#32,#283,#917,.T.);
|
||||
#917 = LINE('',#918,#919);
|
||||
#918 = CARTESIAN_POINT('',(22.059435554995,-22.,-3.734519760785));
|
||||
#919 = VECTOR('',#920,1.);
|
||||
#920 = DIRECTION('',(0.,1.,0.));
|
||||
#921 = ORIENTED_EDGE('',*,*,#290,.F.);
|
||||
#922 = ORIENTED_EDGE('',*,*,#899,.F.);
|
||||
#923 = ORIENTED_EDGE('',*,*,#39,.T.);
|
||||
#924 = PLANE('',#925);
|
||||
#925 = AXIS2_PLACEMENT_3D('',#926,#927,#928);
|
||||
#926 = CARTESIAN_POINT('',(0.19111316645,-22.,-3.734519760785));
|
||||
#927 = DIRECTION('',(0.,0.,1.));
|
||||
#928 = DIRECTION('',(0.,-1.,0.));
|
||||
#929 = ADVANCED_FACE('',(#930),#941,.F.);
|
||||
#930 = FACE_BOUND('',#931,.F.);
|
||||
#931 = EDGE_LOOP('',(#932,#938,#939,#940));
|
||||
#932 = ORIENTED_EDGE('',*,*,#933,.T.);
|
||||
#933 = EDGE_CURVE('',#22,#275,#934,.T.);
|
||||
#934 = LINE('',#935,#936);
|
||||
#935 = CARTESIAN_POINT('',(19.029295926024,-22.,-17.63009960955));
|
||||
#936 = VECTOR('',#937,1.);
|
||||
#937 = DIRECTION('',(0.,1.,0.));
|
||||
#938 = ORIENTED_EDGE('',*,*,#282,.F.);
|
||||
#939 = ORIENTED_EDGE('',*,*,#916,.F.);
|
||||
#940 = ORIENTED_EDGE('',*,*,#31,.T.);
|
||||
#941 = PLANE('',#942);
|
||||
#942 = AXIS2_PLACEMENT_3D('',#943,#944,#945);
|
||||
#943 = CARTESIAN_POINT('',(20.484588228021,-22.,-10.95643672209));
|
||||
#944 = DIRECTION('',(0.977039526026,0.,-0.213058124893));
|
||||
#945 = DIRECTION('',(-0.213058124893,0.,-0.977039526026));
|
||||
#946 = ADVANCED_FACE('',(#947),#953,.F.);
|
||||
#947 = FACE_BOUND('',#948,.F.);
|
||||
#948 = EDGE_LOOP('',(#949,#950,#951,#952));
|
||||
#949 = ORIENTED_EDGE('',*,*,#21,.T.);
|
||||
#950 = ORIENTED_EDGE('',*,*,#860,.T.);
|
||||
#951 = ORIENTED_EDGE('',*,*,#274,.F.);
|
||||
#952 = ORIENTED_EDGE('',*,*,#933,.F.);
|
||||
#953 = PLANE('',#954);
|
||||
#954 = AXIS2_PLACEMENT_3D('',#955,#956,#957);
|
||||
#955 = CARTESIAN_POINT('',(17.956031892536,-22.,-13.7484168618));
|
||||
#956 = DIRECTION('',(-0.963836182336,0.,-0.26649542889));
|
||||
#957 = DIRECTION('',(-0.26649542889,0.,0.963836182336));
|
||||
#958 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3)
|
||||
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#962)) GLOBAL_UNIT_ASSIGNED_CONTEXT
|
||||
((#959,#960,#961)) REPRESENTATION_CONTEXT('Context #1',
|
||||
'3D Context with UNIT and UNCERTAINTY') );
|
||||
#959 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
||||
#960 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
||||
#961 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
||||
#962 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-05),#959,
|
||||
'distance_accuracy_value','confusion accuracy');
|
||||
#963 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7));
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -1,922 +0,0 @@
|
||||
# Mate connectors: aligning with the mainstream CAD systems
|
||||
|
||||
Research date: 2026-08-05. Written against `orca_cad` / `Snapmaker` at the M8 state
|
||||
(`CadDocument.{hpp,cpp}`, `apply_mate`, `datum_frame`, the `Mate` card in `DesignPanel.cpp`).
|
||||
|
||||
**Brief:** align with the mate-connector concept as the main CAD programs actually implement it,
|
||||
and be simple, unequivocal, unconfusing. Alignment is the organising principle of this document:
|
||||
every recommendation is labelled either **[INDUSTRY]** — do what they all do — or **[DEVIATION]** —
|
||||
we would be departing, here is why and what it costs.
|
||||
|
||||
---
|
||||
|
||||
## 0. The answer in ten lines
|
||||
|
||||
1. Seven systems surveyed. **Five of the seven use the same model**; two are the old world.
|
||||
2. The model: a joint is defined between **two local coordinate frames**, one rigidly attached to
|
||||
each part, plus **one type** naming which DOF stay free.
|
||||
3. The frame is called a mate connector (Onshape), a **joint origin** (Fusion, Inventor), a joint
|
||||
connector (FreeCAD 1.0). Same object, three names.
|
||||
4. **Every one of them expresses every DOF about the frame's Z axis.** One axis, one convention.
|
||||
5. **Five types appear in every frame-based system with identical names and identical DOF**:
|
||||
Fastened/Rigid, Revolute, Slider, Cylindrical, Planar. Ball is in four of five.
|
||||
6. That is not fashion — those are the classical **lower kinematic pairs**. The vocabulary converged
|
||||
because the mechanics converged.
|
||||
7. Our kernel is already on the right side of the line: frame-based, five types, Z-relative,
|
||||
superimpose-then-relax. **The architecture needs no revisiting.**
|
||||
8. Where we are out of step: connectors that are not attached to a body; an origin that can only be
|
||||
a face centroid; no live preview of the two Z arrows; a mate card of abstract dropdowns.
|
||||
9. Where we would knowingly deviate: refusing a second mate per body (no vendor does this — it is
|
||||
forced on us by having no solver) and possibly inverting the default mate direction.
|
||||
10. Biggest single win for the stated goal, and it costs no kernel work: **draw both frames and
|
||||
ghost the result before Confirm.** The convention stops needing to be remembered.
|
||||
|
||||
---
|
||||
|
||||
## 1. The two families
|
||||
|
||||
**Constraint-based ("old CAD").** The user states pairwise *geometric relations* between raw
|
||||
topology — this face coincident with that face, this axis concentric with that axis, this plane
|
||||
parallel at 12 mm. Each relation removes some DOF; a numerical solver satisfies all of them at once.
|
||||
Fully positioning one part typically takes **three or more mates**, and the set can be
|
||||
over-constrained, under-constrained, or satisfiable in several configurations.
|
||||
|
||||
**Frame-based ("mate connectors").** The user places a *local coordinate system* on each part and
|
||||
states **one** relation between the two frames. The relation is not "these surfaces touch" but
|
||||
"these frames coincide, except for the following DOF, which stay free."
|
||||
|
||||
Onshape's help page opens by drawing exactly this line:
|
||||
|
||||
> *"Mates in Onshape are different than mates in old CAD systems. Many assemblies require only one
|
||||
> Onshape Mate between any two instances, as the movement (degrees of freedom) between those two
|
||||
> instances is embedded in the Mate."*
|
||||
|
||||
The frame-based model won for three reasons, all of which matter here:
|
||||
|
||||
- **One mate per pair.** No mental arithmetic about which three constraints add up to a hinge.
|
||||
- **The DOF are declared, not deduced.** A revolute mate *is* one rotation. You do not discover the
|
||||
remaining freedom by dragging.
|
||||
- **It needs no simultaneous solver for the common case.** Frame-to-frame alignment is a matrix
|
||||
composition — precisely what `apply_mate` already does.
|
||||
|
||||
> **Caveat — several vendors ship both, and "align with X" is therefore ambiguous.** **Inventor**
|
||||
> kept its legacy constraints *and* added frame-based Joints in 2012; many Inventor users still build
|
||||
> assemblies entirely with the old constraint stack. **Creo** has placement constraints *and*
|
||||
> Mechanism connections. **FreeCAD** had constraint-based Assembly2/3 add-ons before the frame-based
|
||||
> Assembly workbench shipped in 1.0. So copying "what Inventor does" means copying **one of two
|
||||
> coexisting workflows**. **Onshape and Fusion 360 are the only pure frame-based examples**, and they
|
||||
> are the ones to weight most heavily when the evidence conflicts.
|
||||
|
||||
---
|
||||
|
||||
## 2. Field survey — seven systems
|
||||
|
||||
| | Onshape | Fusion 360 | Inventor | FreeCAD 1.0 | Creo | Siemens NX | SOLIDWORKS |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| **Family** | Frame | Frame | Frame (+ legacy constraints) | Frame (+ legacy add-ons) | Both | Constraint | Constraint |
|
||||
| **Frame object** | Mate connector | Joint origin | Joint origin | Joint connector (`Placement1/2`) | CSYS on `Weld`/`6DOF` | — | — (nearest: **mate reference**) |
|
||||
| **Where it lives** | Part Studio **and** Assembly; in the feature list | Component, inside the joint | Component / inside the joint | Inside the Joint object | Part | — | Part (up to 3 named entities) |
|
||||
| **Origin placement** | Inferred family on hover; `Shift` locks | Discrete **snap points**; `Ctrl` cycles | Snap points + explicit origins | Inferred, previewed on hover | Picked CSYS | Picked entities | Picked entities |
|
||||
| **Orientation control** | Primary axis (Z) + secondary axis; flip + 90° reorient | Flip, angle, offsets | Flip, angle, offsets | `Placement1/2` + `Offset1/2` | CSYS + offset | — | — |
|
||||
| **Type inference** | No — explicit | No — explicit | **Yes — "Automatic"** from picked geometry | No | No | No | Partial (mate reference type) |
|
||||
| **Solver** | Yes, simultaneous — *"order won't affect a Mate"* | Yes | Yes | Yes (Ondsel) | Yes | Yes | Yes |
|
||||
| **Reuse across instances** | **Yes** — a Part Studio connector exists on every instance | Weak | Partial | Per-joint | Interfaces | Product Interface | Mate references auto-mate on insert |
|
||||
|
||||
Three observations that shape everything below.
|
||||
|
||||
- **Every frame-based system reduced the type list by an order of magnitude** relative to SOLIDWORKS
|
||||
(7–13 vs ~25) and lost nothing. That is not simplification-by-omission; it is what happens when the
|
||||
DOF live in the mate instead of being assembled from constraints.
|
||||
- **Every one of them defines its types relative to a single axis.** Slider translates along Z,
|
||||
Revolute rotates about Z, Cylindrical does both, Planar translates in X/Y and rotates about Z.
|
||||
One axis carries the whole vocabulary.
|
||||
- **Onshape alone treats the connector as a first-class, reusable, named object** — and that is also
|
||||
where its worst usability complaints come from (§4).
|
||||
|
||||
---
|
||||
|
||||
## 3. The type vocabulary — cross-system table
|
||||
|
||||
DOF = degrees of freedom left **free**, stated about/along the connector Z.
|
||||
|
||||
| DOF | Onshape | Fusion 360 | Inventor | FreeCAD 1.0 | Creo | **Ours today** |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 0 | Fastened | Rigid | Rigid | Fixed | Rigid / Weld | **Fastened** ✅ |
|
||||
| 1 — rot Z | Revolute | Revolute | Rotational | Revolute | Pin | **Revolute** ✅ |
|
||||
| 1 — trans Z | Slider | Slider | Slider | Slider | Slider | **Slider** ✅ |
|
||||
| 2 — rot + trans Z | Cylindrical | Cylindrical | Cylindrical | Cylindrical | Cylinder | **Cylindrical** ✅ |
|
||||
| 3 — trans XY + rot Z | Planar | Planar | Planar | *(Parallel+Distance)* | Planar | **Planar** ✅ |
|
||||
| 3 — rot XYZ | Ball | Ball | Ball | Ball | Ball | — |
|
||||
| 2 — different axes | Pin slot | Pin-Slot | — | — | Slot / Bearing | — |
|
||||
| 1 — coupled | Screw | — | — | Screw | — | — |
|
||||
| 4 | Parallel | — | — | Parallel | — | — |
|
||||
| other | Tangent, Width, Group | As-built | Automatic | Perpendicular, Angle, Distance, Gears, Belt, RackPinion | General, 6DOF | — |
|
||||
|
||||
**Five types appear in every frame-based system, with the same name and the same DOF.** Those five
|
||||
are the industry's common denominator, and they are exactly `mate_kind` 0–4 as already implemented.
|
||||
Ball is in four of five. Everything past that is a long tail no two vendors agree on.
|
||||
|
||||
### Why the convergence is a fact, not a fashion
|
||||
|
||||
A rigid-body placement is an element of SE(3). A mate leaves some set of relative motions free. For
|
||||
the mate to behave the same throughout its range — for a hinge to be a hinge at every angle — that
|
||||
free set must be **closed under composition**: two allowed motions must compose to an allowed motion.
|
||||
A closed set of motions is a **subgroup** of SE(3).
|
||||
|
||||
The subgroups corresponding to physical surface-on-surface contact are the classical **six lower
|
||||
pairs** (Reuleaux):
|
||||
|
||||
| Pair | Free motion relative to Z | DOF |
|
||||
|---|---|---|
|
||||
| Revolute (R) | rotation about Z | 1 |
|
||||
| Prismatic / slider (P) | translation along Z | 1 |
|
||||
| Helical / screw (H) | coupled rotation + translation | 1 |
|
||||
| Cylindrical (C) | rotation about **and** translation along Z | 2 |
|
||||
| Planar (E/G) | translation in X,Y + rotation about Z | 3 |
|
||||
| Spherical / ball (S) | rotation about X, Y, Z | 3 |
|
||||
|
||||
Plus the two trivial ends: identity (0 DOF — **fastened**) and all of SE(3) (6 DOF — floating, i.e.
|
||||
no mate). Hervé's Lie-subgroup analysis of the displacement group is the standard reference for
|
||||
treating these as the algebraic building blocks of mechanism synthesis.
|
||||
|
||||
**Consequence.** Anything outside this table is either (a) a *composition* needing a solver, or
|
||||
(b) not a joint at all but a *measurement*:
|
||||
|
||||
- Onshape's **Parallel** (4 DOF), **Tangent**, **Width**, **Pin slot**, and FreeCAD's **Distance /
|
||||
Angle / Perpendicular** are constraints, not pairs — their free set is not a subgroup, so they only
|
||||
make sense alongside a simultaneous solver.
|
||||
- **Gear, Belt, Rack-and-pinion** are *relations between two mates*, a different object entirely.
|
||||
- **Screw (H)** is a legitimate lower pair but needs a pitch parameter and is rare in printed parts.
|
||||
|
||||
So the vendors' shared five, the lower pairs, and our `mate_kind` 0–4 are the same list arrived at
|
||||
three ways. **[INDUSTRY] Stop looking for missing types and spend the budget on the connector.**
|
||||
|
||||
---
|
||||
|
||||
## 4. What they all agree on — adopt verbatim
|
||||
|
||||
Deviating from any of these makes an experienced user's intuition *wrong*, which is the operational
|
||||
definition of "confusing".
|
||||
|
||||
**A1 [INDUSTRY] — The connector is a full right-handed frame.**
|
||||
Origin + Z (primary) + X (secondary). Onshape and Fusion expose exactly these two axis controls and
|
||||
nothing else. A point cannot express spin; an axis cannot express clocking.
|
||||
*Status: we comply* — `DatumCoordSys` carries origin/x/y and derives Z.
|
||||
|
||||
**A2 [INDUSTRY] — Z is the joint axis; every DOF is about or along Z.**
|
||||
Revolute rotates about Z. Slider translates along Z. Planar's free plane is normal to Z. Offsets run
|
||||
along Z. This single rule is what makes the system learnable: **one axis to look at, and its meaning
|
||||
never changes.**
|
||||
*Status: we comply* — `mate_offset` along A's z, `mate_angle` about A's z.
|
||||
|
||||
**A3 [INDUSTRY] — Mating superimposes the two frames; the type then relaxes specific DOF.**
|
||||
FreeCAD states it most plainly: *"the second connector is superimposed on the first connector by
|
||||
default and may change its position according to the joint type."* Fastened is not a special case —
|
||||
it is the base case with nothing relaxed.
|
||||
*Status: we comply* — `T = M_A · Rz · Tz · F · M_B⁻¹`, looser kinds relaxing from there.
|
||||
|
||||
**A4 [INDUSTRY] — The connector belongs to a part and moves with it.**
|
||||
Onshape: a connector defined in a Part Studio *"is available for reuse on every instance of that part
|
||||
in every assembly in which it is instanced."* It is part geometry, not assembly geometry.
|
||||
*Status: **violated**.* `CoordSysType::PointWorld` is a bare world XYZ with `X = world X` and no
|
||||
`coordsys_body`. Such a connector does not follow its part. See §6 G1.
|
||||
|
||||
**A5 [INDUSTRY] — Selection order is meaningful and must be visible.**
|
||||
One connector is the reference; the other is driven onto it. Onshape spells out that offsets are
|
||||
measured *"from the second Mate connector selected to the first"*, and that reversing the order
|
||||
flips the sign.
|
||||
*Status: complied with in the data model* (`mate_cs_a` fixed, `mate_cs_b` moves) *but not in the UI* —
|
||||
two dropdowns labelled A and B do not tell the user which part is about to jump.
|
||||
|
||||
**A6 [INDUSTRY] — Flip and re-clock live in the mate dialog, always.**
|
||||
Onshape: *"Click the arrow icon to flip the direction of the primary axis. Click the Reorient
|
||||
secondary axis icon to rotate the secondary axis in 90-degree increments."*
|
||||
*Status: partial.* We have `mate_flip` (Z reversal). We have `mate_angle` as a free number — strictly
|
||||
more powerful than 90° steps, and much worse to *use*: the common case is "it came in a quarter turn
|
||||
out", and typing 90 is a worse gesture than pressing a button.
|
||||
|
||||
**A7 [INDUSTRY] — DOF are shown, not inferred by the user.**
|
||||
Onshape animates each mate's remaining DOF on demand; Fusion and Inventor name the DOF in the type
|
||||
list. Our dropdown text already does this in words ("free spin + axial slide"). Keep it.
|
||||
|
||||
**A8 [INDUSTRY] — Free DOF are preserved from the current placement, not zeroed.**
|
||||
Onshape: a Planar mate aligns the frames *"but they are not restricted to this location with respect
|
||||
to their degrees of freedom."*
|
||||
*Status: we comply* — and it must be *said*, because a Planar mate that leaves the part where it was
|
||||
looks like a mate that did nothing.
|
||||
|
||||
---
|
||||
|
||||
## 5. Where they diverge — who to copy, and why
|
||||
|
||||
### D1 — Where the connector's origin comes from
|
||||
|
||||
| | Behaviour |
|
||||
|---|---|
|
||||
| **Fusion 360** | Discrete **snap points** only: vertex, edge midpoint, face centre, arc centre. `Ctrl` cycles the candidates under the cursor. A circle icon denotes a vertex, a triangle a midpoint. "Between two faces" is a separate explicit option. |
|
||||
| **Onshape** | Infers a *family* on hover — centroid, every vertex, every edge midpoint, every arc centre, the centroids of interior regions (holes, slots), and the virtual sharps of conical faces. `Shift` locks the current candidate. |
|
||||
| **Inventor** | Snap points, plus explicit joint origins for awkward cases. |
|
||||
| **FreeCAD 1.0** | Hovering previews where the connector will land before you commit. |
|
||||
| **Ours** | Always the **face centroid**. No alternative exists. |
|
||||
|
||||
Onshape's richness has a cost its own documentation admits: *"The suggested locations are based on
|
||||
the underlying geometry of the part and changing the geometry will change the location of the Mate.
|
||||
This can be undesirable in certain situations."* On the forum this shows up as connectors that move
|
||||
or break on edit — the classic topological-naming failure. Fusion's discrete set is poorer and far
|
||||
more predictable.
|
||||
|
||||
> **[INDUSTRY] Copy Fusion's candidate *set*.** A small, closed, enumerable set — **face centroid,
|
||||
> vertex, edge midpoint, arc/circle centre** — each drawn before commit, with the card naming which is
|
||||
> in use ("Origin: edge midpoint"). This is our largest expressiveness gap: a face centroid alone
|
||||
> cannot place a hinge pin on a corner boss. It is also the one place where copying the *simpler*
|
||||
> vendor is clearly right.
|
||||
>
|
||||
> **Open sub-choice — how the candidate is chosen.** Three options, in increasing order of magic:
|
||||
> (1) **explicit dropdown** in the card after picking the face — no hover behaviour at all;
|
||||
> (2) **Fusion's `Ctrl` cycling** through candidates under the cursor; (3) **Onshape's hover
|
||||
> inference**. Kimi's independent review argued for (1) on the grounds that hover is exactly where
|
||||
> both vendors' instability complaints originate, and that a dropdown gets ~90% of the expressiveness
|
||||
> with none of the hover-guess debugging. That is a fair reading and (1) is the cheapest to build and
|
||||
> the easiest to make unequivocal. **Recommendation: build (1) first; if hover is added later, let it
|
||||
> *pre-fill the dropdown* rather than silently create an implicit connector** — which also keeps R2
|
||||
> (one kind of connector) intact.
|
||||
|
||||
### D2 — Explicit type, or inferred from the geometry?
|
||||
|
||||
Inventor is the only surveyed system that infers: *"Rotational is selected if the two selected
|
||||
origins are circular. Cylindrical if the two selected origins are points on a cylinder. Ball if
|
||||
points on a sphere. Rigid for all other origin selections."* Onshape and Fusion require an explicit
|
||||
choice.
|
||||
|
||||
> **[INDUSTRY, Inventor] Do both, in Inventor's order.** Infer a *default* type from what was picked,
|
||||
> then show it in an editable control. Inference is what makes the tool feel like it understands the
|
||||
> geometry; the visible, editable result is what keeps it unequivocal. Pure inference with no visible
|
||||
> type is the confusing option; a pure dropdown with no default is the tedious one. This also fits
|
||||
> the Design tab's geometry-first charter exactly: point at a bore, get Revolute offered.
|
||||
|
||||
### D3 — How the Z-direction ambiguity is resolved
|
||||
|
||||
This is the specific failure the brief is aimed at. A former IT trainer stated it precisely on the
|
||||
Onshape forum:
|
||||
|
||||
> *"There is always the risk that users will build their own conceptual models of how software works
|
||||
> which may not match the designer's concept. The result is usually a poor user experience and many
|
||||
> mistakes… for a good (say) Fixed mate to occur do the Z axes of the two mates have to be pointing
|
||||
> in the same direction… Alternatively, should they be facing each other?"*
|
||||
|
||||
He is asking the right question and **no vendor's documentation answers it.** Onshape's own advice —
|
||||
*"if the behavior is not what you expected, try flipping the primary and/or secondary axis"* — is
|
||||
trial and error. This is a gap in the industry, not a convention to copy.
|
||||
|
||||
> **[INDUSTRY, method] Resolve it with live preview, not documentation.** FreeCAD previews the
|
||||
> connector on hover; Onshape and Fusion both draw the frames. Draw **both** Z arrows the moment the
|
||||
> second connector is picked, and ghost the resulting placement *before* Confirm. The convention then
|
||||
> never has to be remembered because it is on screen.
|
||||
>
|
||||
> **[DEVIATION, optional] Name the two cases in the user's words** rather than in axis-speak:
|
||||
> "the two faces come together" vs "the axes run the same way". No surveyed vendor does this — they
|
||||
> all ship a flip arrow. It is a small, low-risk improvement on the state of the art, and it is
|
||||
> separable from the default-direction question in §8 D1.
|
||||
|
||||
### D4 — Named, reusable connectors on the part
|
||||
|
||||
Onshape: connectors created in the Part Studio are reused on every instance in every assembly.
|
||||
SOLIDWORKS' **mate reference** reaches the same end by another route: up to three named entities
|
||||
(primary/secondary/tertiary) baked into the part so it auto-mates on drag-and-drop — and a *named*
|
||||
mate reference seeks out a matching name on insertion. That naming trick is how a library of
|
||||
fasteners assembles itself.
|
||||
|
||||
> **[INDUSTRY] Out of scope now, but do not preclude it.** Give connectors a stable, user-visible
|
||||
> name at creation. One string today; expensive to add once documents exist in the wild.
|
||||
|
||||
---
|
||||
|
||||
## 6. Confusion catalogue
|
||||
|
||||
Documented ways real implementations confuse people. Each is a requirement in disguise.
|
||||
|
||||
**C1 — Which way does Z point?** See D3. If a user has to ask once, they will mis-predict a hundred
|
||||
times.
|
||||
|
||||
**C2 — The roll is unspecified.** Aligning Z leaves one rotation about Z undetermined. Something must
|
||||
pin it, and if that something is world-derived, the frame does not rotate with its part. **This
|
||||
codebase shipped exactly this bug** (`en4`): a face-only connector took Z from the face
|
||||
normal but X from `coordsys_x_hint`, a world constant, so Fastened and Slider claimed to lock an
|
||||
orientation the frame could not see. Fixed 2026-07-26 by deriving X from the face's own first usable
|
||||
edge — but note the fix's own caveat: *"replaying an older document whose face-only connector fed a
|
||||
mate can now place that body differently."* Roll conventions are load-bearing, and changing one is a
|
||||
document-format change.
|
||||
|
||||
**C3 — The origin drifts.** See D1.
|
||||
|
||||
**C4 — Implicit and explicit connectors are not the same thing.** On the Onshape forum, implicit
|
||||
connectors are reported to change their query structure when a feature is edited and re-accepted, and
|
||||
are unusable in places explicit ones work. Two things called by one name that behave differently is a
|
||||
permanent tax.
|
||||
|
||||
**C5 — Which part moves?** A frame alignment is asymmetric. If the UI does not say which frame is
|
||||
driven, the user finds out by watching the wrong part jump.
|
||||
|
||||
**C6 — Which direction is a positive offset?** Onshape measures *"from the second Mate connector
|
||||
selected to the first"* — the sign depends on pick order, and swapping the picks flips it. Documented
|
||||
behaviour, documented surprise.
|
||||
|
||||
**C7 — One intent, several mates.** The SOLIDWORKS failure: expressing "this shaft is in this hole,
|
||||
resting on this shoulder" as three constraints, then discovering the solver picked the mirror
|
||||
configuration. Frame-based systems fix this by construction; the requirement is not to reintroduce it.
|
||||
|
||||
**C8 — Degenerate frames.** A circular face has no usable in-plane edge direction; a cylinder seam
|
||||
projects to nothing; a picked edge parallel to Z gives a zero cross product. `datum_frame` handles all
|
||||
three with fallbacks — the requirement is that a fallback be *visible*, because a silent fallback is
|
||||
C2 wearing a different hat.
|
||||
|
||||
**C9 — Order dependence without a solver.** Onshape can say *"Onshape solves Mates simultaneously so
|
||||
order won't affect a Mate."* A system that composes transforms in tree order cannot say that. Two
|
||||
mates driving one body means the second wins and the first is a lie on screen.
|
||||
|
||||
**C10 — Mirrors and patterns.** A mirrored instance has a left-handed frame. Blindly mirroring a
|
||||
connector gives a frame whose Z still points "out" but whose handedness flipped, so every rotation
|
||||
runs backwards. Cheap to handle now, miserable to retrofit.
|
||||
|
||||
---
|
||||
|
||||
## 7. Requirements
|
||||
|
||||
Labelled **[INDUSTRY]** (what the frame-based systems do) or **[DEVIATION]** (we would depart).
|
||||
|
||||
### Definition
|
||||
|
||||
**R1 [INDUSTRY] — A mate connector is a frame attached to exactly one body.** No body, no connector.
|
||||
*Test:* creating a connector without a body is rejected at creation, not at mate time.
|
||||
→ **`CoordSysType::PointWorld` violates this.** It is a datum wearing a connector's name.
|
||||
|
||||
**R2 [INDUSTRY] — One kind of connector, not two.** No "implicit" connector that behaves differently
|
||||
from an explicit one. If hover inference is offered, hovering *creates* an ordinary connector.
|
||||
*Why:* C4. *Test:* everything that accepts a connector accepts any connector.
|
||||
|
||||
**R3 [INDUSTRY] — A mate names exactly one subgroup of free motion.** Fastened (0), Revolute (1),
|
||||
Slider (1), Cylindrical (2), Planar (3), optionally Ball (3). *Why:* §3. *Test:* every type's free
|
||||
set is closed; no type is "A and also B".
|
||||
|
||||
### Orientation
|
||||
|
||||
**R4 [INDUSTRY] — Everything is about Z. Say so once, in the UI.** *Test:* no mate parameter refers
|
||||
to any other axis.
|
||||
|
||||
**R5 [DEVIATION] — Z is the outward material direction, and mates default to FACING.**
|
||||
A mate would drive B's Z onto **−A's Z** by default, so picking two faces that should touch makes
|
||||
them touch with no options changed. *Why:* it is the whole of C1.
|
||||
**Cost and caveat:** this inverts today's default (`mate_flip=false` currently *aligns*), and I could
|
||||
not establish from any vendor's documentation what their default actually is — the forum question in
|
||||
D3 went unanswered precisely because it is undocumented. So this is marked a deviation on the honest
|
||||
grounds that **I cannot prove the industry agrees with it.** If D3's live preview lands first, the
|
||||
default matters much less, because the user sees the outcome before committing. See §9 D1.
|
||||
|
||||
**R6 [DEVIATION] — Name the two directions; do not ship a boolean called "flip".**
|
||||
`Direction: Facing | Aligned`. Every surveyed vendor ships a flip arrow instead. A boolean requires
|
||||
remembering what unticked means; two named values do not. Low risk, small improvement on the state of
|
||||
the art.
|
||||
|
||||
**R7 [INDUSTRY] — Roll is picked, or a stored quarter turn. Never world-derived.**
|
||||
X from a referenced edge or in-plane direction; failing that, a deterministic body-attached seed, with
|
||||
**Rotate 90°** offered as a stored integer 0–3 on top (this is Onshape's "reorient secondary axis",
|
||||
A6). *Why:* C2 and the world-constant bug this project already shipped. *Test:* rotate the parent
|
||||
body by any angle; the connector's X rotates with it — *this test already exists* ("a face-only frame
|
||||
rotates with its body").
|
||||
|
||||
**R8 [INDUSTRY] — A degenerate roll is reported, not absorbed.** *Test:* a connector on a full
|
||||
cylindrical face reports "roll undefined — pick a direction" rather than silently taking a fallback.
|
||||
|
||||
### Placement
|
||||
|
||||
**R9 [INDUSTRY, Fusion] — Origin comes from a small closed set of named candidates.**
|
||||
**Face centroid, arc/circle centre, edge midpoint, vertex.** Four. Each stored as
|
||||
`(kind, topological reference)` and resolved at rebuild. *Why:* D1. *Test:* the stored kind is visible
|
||||
in the card; a rebuild either resolves it or raises an error.
|
||||
|
||||
**R10 [INDUSTRY] — An unresolvable reference is an error, never a silent relocation.**
|
||||
*Test:* delete the referenced face; the mate reports "connector A: face not found" and the body stays
|
||||
where it was.
|
||||
|
||||
### Semantics without a solver
|
||||
|
||||
**R11 [DEVIATION] — A body is driven by at most one mate. The second is refused.**
|
||||
**No surveyed system does this** — they all have solvers and all accept many mates per body. It is
|
||||
forced on us by tree-order composition: a second mate on the same body silently overrides the first
|
||||
and the screen shows a configuration satisfying only one stated intent (C9). *Test:* creating a
|
||||
second mate whose moving body already has one is rejected, naming the existing mate.
|
||||
This is the single largest departure in this document. See §9 D4.
|
||||
|
||||
> **A tempting misreading, checked and rejected.** It is easy to find the claim that Onshape mandates
|
||||
> *"exactly one Mate between any two instances"*, which would make R11 an industry agreement rather
|
||||
> than a deviation. **The Onshape page does not say that.** It says *"**Many assemblies require only**
|
||||
> one Onshape Mate between any two instances"* and then lists, as an explicit remedy, *"**Use more
|
||||
> than one Mate if necessary.**"* One mate per pair is Onshape's *typical case*, not its rule. R11
|
||||
> remains a deviation and must be justified on our own architecture, not on theirs.
|
||||
|
||||
**R11a [DEVIATION] — The refusal list.** With no solver, these are unsupportable and must be refused
|
||||
rather than half-done: a second mate on an already-driven body; cycles (A→B, B→A); closed loops
|
||||
(A→B, A→C, B→C); relations *between* mates (gear, belt, rack-and-pinion, screw coupling); **joint
|
||||
limits**, which nothing can enforce without a solver; and **dragging a body to exercise a free DOF**,
|
||||
which requires keeping the body on the allowed manifold. Motion analysis and animation follow from the
|
||||
same lack. *Requirement:* none of these may appear in the UI as something that half-works.
|
||||
|
||||
**R12 [DEVIATION] — The mate graph is an acyclic forest rooted at fixed bodies.** A body reached by
|
||||
no mate is fixed; cycles are refused. Same root cause as R11. *Test:* A→B, B→A rejected at creation.
|
||||
|
||||
**R13 [INDUSTRY] — Free DOF are preserved from the current placement, and the user is told.**
|
||||
Behaviour already matches Onshape (A8); the telling does not. *Test:* the card for any type with
|
||||
DOF > 0 says which motions remain and that dragging exercises them.
|
||||
|
||||
**R14 [INDUSTRY] — State what mirroring does to a connector.**
|
||||
*Checked in the code:* `datum_frame` ends with a Gram-Schmidt forcing a right-handed frame
|
||||
(`ds.x = Y.cross(Z)`), so a connector resolved on a mirrored body comes out **right-handed, not
|
||||
mirror-imaged**. Z follows the mirrored face's outward normal, X follows a mirrored edge, handedness
|
||||
is re-imposed. Defensible — a mate on the mirrored part still turns the way its type says — but it
|
||||
means a mirrored sub-assembly is *not* the mirror image of the original in its rotation sense.
|
||||
*Requirement:* document it and pin it with a test. *Why:* C10.
|
||||
|
||||
### Feedback — the part that actually removes confusion
|
||||
|
||||
**R15 [INDUSTRY] — Before Confirm, the card answers four questions in words.** Which body moves;
|
||||
which way Z points on each connector; how many DOF remain; what the offset is measured from.
|
||||
|
||||
**R16 [INDUSTRY] — Draw both frames live, with Z distinguishable, and ghost the result.**
|
||||
Two triads with Z rendered differently from X/Y (length, arrowhead, colour). *Why:* D3 — the fastest
|
||||
way to make a convention unequivocal is to show it. *Test:* both Z directions are readable in a
|
||||
screenshot.
|
||||
|
||||
**R17 [INDUSTRY] — Show the DOF budget per body.** "Body 2: 1 of 6 DOF free (rotation about Z)."
|
||||
The most educational readout in any assembly system, and free to compute here — the type *is* the DOF
|
||||
count. *Test:* the number changes when the type changes.
|
||||
|
||||
**R18 [DEVIATION] — Refuse loudly and name the alternative.** Where something is out of scope (a
|
||||
second mate, a tangency, a gear ratio), say what is unsupported and what to do instead. Vendors do not
|
||||
need this because their solvers accept the input. *Test:* no refusal message ends without a suggested
|
||||
next action.
|
||||
|
||||
---
|
||||
|
||||
## 8. Minimal specification, and gap analysis
|
||||
|
||||
### The connector
|
||||
|
||||
```
|
||||
MateConnector
|
||||
body int required, ≥ 0 (R1)
|
||||
origin_kind enum FaceCentroid | ArcCentre | EdgeMidpoint | Vertex (R9)
|
||||
origin_ref topo ref face / edge / vertex index on that body
|
||||
z_source implied by origin_kind: face normal, arc axis, edge tangent
|
||||
roll_ref topo ref optional in-plane edge; else deterministic seed (R7)
|
||||
roll_quarters int 0..3 stored quarter turns on top of the seed (R7, A6)
|
||||
flip_z bool reverse Z at the connector
|
||||
name string stable, user-visible (D4)
|
||||
```
|
||||
|
||||
`flip_z` is a property of the **connector**, chosen once when it is made — not a per-mate
|
||||
afterthought. Keeping connector-flip and mate-direction separate is what stops the "which flip do I
|
||||
tick?" question.
|
||||
|
||||
### The mate
|
||||
|
||||
```
|
||||
Mate
|
||||
kind enum Fastened | Revolute | Slider | Cylindrical | Planar [| Ball] (R3)
|
||||
fixed connector A — its body does not move
|
||||
moving connector B — its body is driven (A5, C5)
|
||||
direction enum Facing | Aligned (R5, R6)
|
||||
offset mm along A's Z, measured A → B — state this in the label (C6)
|
||||
angle deg about A's Z (R4)
|
||||
```
|
||||
|
||||
Within one field of what exists.
|
||||
|
||||
### Gaps against today
|
||||
|
||||
Source of record: `CadDocument.hpp:26,247-252,298-310`; `CadDocument.cpp:1669` (`datum_frame`),
|
||||
`:2961` (`apply_mate`), `:1302` (`add_mate`); `DesignPanel.cpp:2671-2709` (the Mate card).
|
||||
|
||||
| # | Gap | Severity | Ref |
|
||||
|---|---|---|---|
|
||||
| G1 | `PointWorld` connectors are not attached to a body and their X is a world constant | **High — data model** | A4/R1 |
|
||||
| G2 | Origin is always the face centroid; no vertex / edge-midpoint / arc-centre snap | **High — expressiveness** | D1/R9 |
|
||||
| G3 | No live preview of the two Z arrows or of the resulting placement | **High — this is the brief** | D3/R16 |
|
||||
| G4 | Mate card is two abstract dropdowns; nothing says which body moves | High — charter + A5 | R15 |
|
||||
| G5 | No joint-type inference from the picked geometry | Medium — feel | D2 |
|
||||
| G6 | `add_mate` validates nothing — no one-mate-per-body, no cycle check | Medium | R11/R12 |
|
||||
| G7 | No `Ball` type | Low | §3 |
|
||||
| G8 | Re-clocking needs a typed angle; no 90° step control | Low, cheap | A6/R7 |
|
||||
| G9 | Degenerate roll falls back silently | Low | C8/R8 |
|
||||
| G10 | Connectors have no stable user-facing name | Low now, expensive later | D4 |
|
||||
|
||||
**Already aligned — do not "fix" these:** the five types and their DOF; the frame definition (A1);
|
||||
Z as the joint axis (A2); superimpose-then-relax (A3); the fixed/moving asymmetry in the data model
|
||||
(A5); DOF wording in the type list (A7); free-DOF preservation (A8); right-handed frames under mirror
|
||||
(R14); and `en4`'s fix, which put roll derivation on the body where it belongs (C2).
|
||||
|
||||
**The pattern worth naming: the kernel is in good shape and the concept is under-explained.** Half the
|
||||
requirements here are wording and drawing, not geometry. The two real engineering items are R9 (origin
|
||||
candidates) and R11/R12 (the mate-graph rules).
|
||||
|
||||
### Expensive-to-retrofit decisions — get these right in the data model now
|
||||
|
||||
Changing any of these after documents exist in the wild costs a migration, not an edit.
|
||||
|
||||
1. **Topological reference stability.** Storing raw face/edge indices is brittle — editing a body
|
||||
renumbers faces. Either persistent topology IDs, or store the named origin *kind* plus a
|
||||
deterministic search that re-finds the same geometric intent on rebuild. The latter is cheaper and
|
||||
probably sufficient here; it is also what makes R10's "error, never silent relocation" enforceable.
|
||||
2. **Connector ownership** (R1). Remove `PointWorld` or bind it to a body. Do this first.
|
||||
3. **Mate direction semantics** (R5/D1). Inverting the default rewrites the meaning of every saved
|
||||
mate.
|
||||
4. **Roll representation** (R7). "First usable edge" is better than world-X but still fragile. Store
|
||||
an explicit roll reference plus quarter turns.
|
||||
5. **Coordinate convention** — Z = joint axis, X = roll reference. Changing this after release
|
||||
invalidates every mate.
|
||||
6. **Units** — offset in mm, angle in degrees. Never change.
|
||||
7. **Mirror handedness** (R14) — document the decision, do not let it stay an accident.
|
||||
8. **Flat body index vs. a component tree.** Mates currently reference bodies in a flat vector. If
|
||||
**sub-assemblies** are ever in scope, mates must reference nodes in a tree instead. Retrofitting
|
||||
this is painful and it is the one item on this list not already implied elsewhere in the document —
|
||||
**decide now whether nested assemblies are in scope.**
|
||||
9. **Serialization field semantics.** Adding fields is easy; redefining `mate_flip` or
|
||||
`coordsys_x_hint` is not.
|
||||
10. **The one-mate-per-body rule** (R11). Enforce at creation. Relaxing it later by adding a solver is
|
||||
straightforward; allowing many mates now and discovering later that they silently conflict is not.
|
||||
|
||||
---
|
||||
|
||||
## 8b. The visual shape of the connector — polarity and verse
|
||||
|
||||
Researched separately (2026-08-05) by downloading and **looking at** the vendors' own figures, not
|
||||
by reading their prose. Files kept alongside this document in `doc/design/mate-connectors/`.
|
||||
|
||||
### What the systems actually draw
|
||||
|
||||
**Onshape** — verified from `planarfacemateconnectors.png`, `cylindricalmateconnectors.png`,
|
||||
`linearedgemateconnectors.png`, `mateconnector-planarpoints.png`, `matepointiconLG.png`:
|
||||
|
||||
> **A small circle with one quadrant filled, plus three short coloured axis arms (X red, Y green,
|
||||
> Z blue).**
|
||||
|
||||
Three parts, each doing one job:
|
||||
|
||||
| Element | What it says |
|
||||
|---|---|
|
||||
| The **circle** | "I am a frame, and this is my XY plane." |
|
||||
| The **filled quadrant** | **The roll.** The shaded sector is the +X/+Y quadrant. |
|
||||
| The **coloured arms** | The three axis directions, Z distinguished by colour. |
|
||||
|
||||
The quadrant is the cleverest part of the whole design and it is easy to miss. The figure
|
||||
`matepointreorientsecondaryaxis.png` shows three connectors side by side with the quadrant in three
|
||||
different rotations — **it is the live readout of "reorient secondary axis in 90° increments" (A6).**
|
||||
One glyph element makes the otherwise-invisible clocking visible, and makes the 90° button's effect
|
||||
legible before you commit. The toolbar icon `matepointiconLG.png` is that same circle-with-a-quadrant,
|
||||
so the symbol is consistent from toolbar to viewport.
|
||||
|
||||
Candidate snap points, before you choose one, are drawn as **plain small white dots** on the model
|
||||
(clear in `mateconnector-planarpoints.png`: dots at every corner and edge midpoint). Candidate and
|
||||
committed are deliberately different weights — dots propose, the circle-and-triad commits.
|
||||
|
||||
**FreeCAD 1.0** — verbatim from the wiki: *"Connectors are local coordinate systems and are marked by
|
||||
a symbol with three axes (X, Y, Z) and a circle representing the XY-plane."* Same core as Onshape —
|
||||
circle plus triad — **without** the quadrant.
|
||||
|
||||
**Fusion 360** — the joint origin glyph, plus a documented icon language for *candidates*: *"A circle
|
||||
denotes a vertex, and a triangle denotes a midpoint."* Shape encodes what kind of point it is.
|
||||
|
||||
**Convergent core:** *circle for the XY plane + coloured triad*. Onshape alone adds the roll quadrant.
|
||||
|
||||
### What none of them draw — and it is exactly what was asked for
|
||||
|
||||
**Nothing in any vendor's glyph says which connector is the reference and which one is about to
|
||||
move.** Both ends of a mate are drawn identically. That is confusion C5 ("which part moves?") left
|
||||
unsolved in the visual language, and it is why the honest recommendation earlier was a live ghost —
|
||||
the ghost compensates for a glyph that does not carry the information.
|
||||
|
||||
So the two things asked for split cleanly, and only one of them is solved upstream:
|
||||
|
||||
- **Verse** (*verso* — which way it points): **solved**. Z has a colour and a direction.
|
||||
- **Polarity** (which end receives, which end inserts; who is anchored, who travels): **unsolved
|
||||
everywhere.** This is open ground, and getting it right is a genuine improvement rather than a
|
||||
deviation to justify.
|
||||
|
||||
### Our starting point
|
||||
|
||||
**We draw nothing.** `resolve_datum_coordsys()` (`CadDocument.cpp:1749`) has exactly one consumer in
|
||||
the entire tree — `McpControl.cpp:1310`, the agent socket. A mate connector is today visible only to
|
||||
a program. The glyph is unbuilt, so there is no migration cost to designing it properly now.
|
||||
|
||||
### Proposed glyph: the magnet
|
||||
|
||||
Adopt Onshape's proven core, then add the missing polarity with a metaphor that carries its own
|
||||
instructions.
|
||||
|
||||
```
|
||||
▲ solid cone on +Z ONLY ← verse
|
||||
|
|
||||
────●──── ← the disc = XY plane, ● = exact origin
|
||||
▨ quadrant filled ← roll / clocking, steps 90°
|
||||
```
|
||||
|
||||
**Rule 1 — verse: draw +Z and never −Z.** A single stem with a cone head, on the positive side only.
|
||||
No stem below the disc. A double-headed axis is the one thing that guarantees the question gets asked;
|
||||
an arrow that exists on one side only cannot be misread. Length is asymmetric on purpose.
|
||||
|
||||
**Rule 2 — roll: keep Onshape's quadrant.** Filled sector = the +X/+Y quadrant. It rotates in 90°
|
||||
steps with the reorient control (A6/R7). This is aligned *and* it is the only in-glyph answer to
|
||||
"where is X?", which matters because Fastened and Slider lock the clocking.
|
||||
|
||||
**Rule 3 — polarity: solid cone travels, open collar receives.**
|
||||
- The **driven** connector (B, on the body that will move) draws a **solid filled cone** — the plug.
|
||||
- The **fixed** connector (A) draws an **open ring / hollow cone outline** — the socket.
|
||||
|
||||
Same silhouette, so they read as a matched pair; opposite fill, so which one is about to jump is
|
||||
answerable at a glance and without a legend. Plug-into-socket is the one mechanical metaphor every
|
||||
user of this tool already has in their hands.
|
||||
|
||||
**Rule 4 — the pair reads as a magnet.** Draw a dashed line joining the two origins the moment both
|
||||
are picked. Two poles, one field line. And because a magnet's north seeks a south, **"facing" becomes
|
||||
the self-evident default** — which quietly settles open decision D1 (§9) on visual grounds rather than
|
||||
on a convention nobody can look up. If the glyph looks like a magnet, nobody has to be told that two
|
||||
faces which touch have opposed normals.
|
||||
|
||||
**Rule 5 — three states, three weights.**
|
||||
|
||||
| State | Drawing |
|
||||
|---|---|
|
||||
| **Candidate** (hover) | small dot only — Onshape's white dots; shape may encode kind, Fusion-style |
|
||||
| **Picked** | full glyph: disc + quadrant + cone |
|
||||
| **Degenerate roll** (C8/R8) | the quadrant is drawn **hollow/hatched** — "roll undefined, pick a direction" |
|
||||
|
||||
That last row is worth the trouble: it turns R8 from a message nobody reads into a mark you cannot
|
||||
miss, and it costs one branch in the renderer.
|
||||
|
||||
**Rule 6 — do not reuse the existing triad.** The bed-centre world triad
|
||||
(`DesignCanvas.cpp:65`, `set_axes_at_bed_center`) and the move gizmo are already three-coloured arrows.
|
||||
The connector must not be a fourth set of RGB arrows or the viewport becomes unreadable. The disc and
|
||||
the quadrant are what distinguish it; keep the arms short, and consider drawing only Z on the
|
||||
committed glyph, with X/Y implied by the quadrant.
|
||||
|
||||
### Built and judged in the viewport, not in a mock
|
||||
|
||||
The browser mock that first accompanied this section was the wrong instrument and its proportions
|
||||
were meaningless: **every gizmo in this codebase is sized in SCREEN PIXELS** via `upp = 1/zoom`
|
||||
(`render_shell_gizmo` uses `15.0 * upp`, `render_hole_gizmo` `9.0 * upp` for its cube). A connector
|
||||
is a symbol, not a part — it must not shrink with the model. Nothing about that is visible in SVG.
|
||||
|
||||
The glyph was therefore implemented and driven on the rig. Screenshots: `g-0*.png`, left in the workspace `artifacts/shots/` and not moved into the repo.
|
||||
Five findings, none of which a mock could have produced:
|
||||
|
||||
**F1 — Three axis arms lose to one.** Rendered side by side (`ORCA_CAD_GLYPH=A` vs default), the
|
||||
Onshape-style RGB trio crowds a 22 px disc: the arrowheads are as large as the disc, they bury the
|
||||
gold quadrant, and at an oblique angle the three heads pile into a coloured smudge. Worse, **it is
|
||||
indistinguishable from the move gizmo and the bed triad**, which are already RGB arrow trios in this
|
||||
viewport. One-sided Z wins on evidence, not taste. (`g-01-zoom.png` vs `g-02-zoom.png`.)
|
||||
|
||||
**F2 — Polarity works, and colour does more of the work than fill.** A filled blue head against an
|
||||
open grey outline head is readable instantly at 22 px (`g-03-zoom.png`). But the fill difference is
|
||||
the *second* cue; the colour split carries it. Keep both — fill survives greyscale and colour-blind
|
||||
palettes, colour survives small size.
|
||||
|
||||
**F3 — Depth off floats, depth on tears.** With `GL_DEPTH_TEST` off, connectors on faces pointing
|
||||
*away* from the camera still drew their discs over the solid, so the part looked covered in frames
|
||||
that were really on its back. Turning depth on fixed that and immediately caused **z-fighting**: the
|
||||
disc is exactly coplanar with its face, and came out as a broken dotted arc. The fix is depth **on**
|
||||
plus a sub-pixel lift along Z (`0.7 * upp`), scaled by `upp` so it never becomes a visible gap on
|
||||
zoom-in. Both failure modes are in the images (`g-03` torn, `g-04` clean).
|
||||
|
||||
**F4 — The quadrant is the first thing to die at a grazing angle.** On a face seen nearly edge-on the
|
||||
disc foreshortens to a sliver and the fan collapses into a blob (`g-01-zoom.png`, lower-right glyph).
|
||||
The roll is exactly the information that is hardest to read when you most need it. Not yet solved —
|
||||
see the open item below.
|
||||
|
||||
**F5 — Roll-undefined in red is too loud.** It works, but it makes the *least* important connector
|
||||
the most eye-catching thing on screen. Amber, or the same grey with a hatched quadrant, is enough.
|
||||
|
||||
Also surfaced while testing, and unrelated to the glyph: `add_mate` accepted a mate between two
|
||||
connectors **on the same body**, which is meaningless, and duly transformed the body relative to
|
||||
itself. Concrete instance of gap G6.
|
||||
|
||||
**Still untested:** a true grazing view (the view-cube click missed), a connector on a curved face,
|
||||
and behaviour when a connector overlaps the move gizmo. F4 is the open design question — the disc may
|
||||
need to billboard its *quadrant* while keeping the disc in-plane, which is a compromise no surveyed
|
||||
vendor makes and which should be tried before being adopted.
|
||||
|
||||
### What this costs
|
||||
|
||||
A renderer for `resolve_datum_coordsys()` — which does not exist and has to be written whatever glyph
|
||||
is chosen — plus one dashed line and three fill states. No kernel work. It is the same piece of work
|
||||
as G3 (live preview), and doing them together is what makes the mate card honest.
|
||||
|
||||
---
|
||||
|
||||
## 8c. The "faceted ridge dome" proposal — built, rendered, judged
|
||||
|
||||
A colleague proposed replacing the flat disc with an **asymmetric low-poly solid**: a faceted
|
||||
prismatic wedge with a dominant longitudinal ridge that **slopes** from a tall steep back to a long
|
||||
shallow front, plus a male protrusion / female pocket pair with a 0.2 mm clearance.
|
||||
|
||||
It was built rather than discussed. `faceted_ridge_key.scad` (this folder) (6 vertices, 7 faces),
|
||||
verified as a closed manifold, exported through OpenSCAD, and flat-shaded from five directions with
|
||||
`render_key.py` / `render_stl.py`. Sheets: `rk-sheet.png`, `cmp-sheet.png`.
|
||||
|
||||
### The verdict: the shape is right, the male/female polarity cue is not
|
||||
|
||||
**It solves F4, decisively.** The grazing view — where the flat disc dies, its quadrant collapsing to
|
||||
a blob — is the view where this shape is *most* legible: the tall back and long shallow front are
|
||||
unmistakable in silhouette. At a grazing angle the silhouette IS the information, and this solid's
|
||||
silhouette is maximally informative there. That is a real, evidence-backed win over what is currently
|
||||
in the code.
|
||||
|
||||
**Down the mating axis (+Z) it also reads well**, which matters because that is the natural viewing
|
||||
direction when you are looking at a face you intend to mate.
|
||||
|
||||
**One degenerate view, and it is not the one I predicted.** I expected the ±X views (along the ridge)
|
||||
to be silhouette-ambiguous, resolved only by shading. Wrong: front and back are clearly *different* —
|
||||
the front shows several facets, the back is a **single flat featureless triangle**. So they are not
|
||||
confusable, but the view from directly behind the tall end tells you nothing about roll or slope.
|
||||
A second blind spot remains untested: from below the base, where the protrusion is hidden behind its
|
||||
own face.
|
||||
|
||||
**The female half fails, and much harder than expected.** Rendered with flat shading and no outlines —
|
||||
the honest test, since a viewport draws no black edges — a recessed pocket is *invisible*: iso and
|
||||
grazing show a plain block with a hairline; straight down the axis shows a **completely blank
|
||||
rectangle**. The interior faces are lit almost identically to the top face and are occluded by the rim
|
||||
from most angles. As a polarity cue, male/female therefore works in exactly one direction and returns
|
||||
nothing in the other.
|
||||
|
||||
> **Conclusion: do not overload shape with all three jobs.** Let the solid carry **verse and roll**,
|
||||
> where it is excellent, and carry **polarity on a second channel** — colour plus the filled/open head
|
||||
> that already tested well at 22 px (F2). Drawing the fixed connector as an outline/wireframe of the
|
||||
> same solid is the variant worth trying; drawing it as a pocket is not.
|
||||
|
||||
### Two premises in the brief are wrong
|
||||
|
||||
**"Avoid curved surfaces to optimise rendering computations / rapid mesh processing."** Not a reason
|
||||
for a viewport glyph. There are 2–20 connectors on screen, the renderer pushes `GLModel` triangles
|
||||
directly, and it performs no CSG or mesh processing at all. **The real argument for flat facets is
|
||||
legibility**: hard normals give distinct value steps between adjacent facets, and the renders confirm
|
||||
that is exactly what makes the shape readable from an arbitrary angle. Keep the constraint, fix the
|
||||
justification. (For a *printed* part the original justification is sound for a different reason: flat
|
||||
facets slice without the stair-stepping a tessellated curve produces.)
|
||||
|
||||
**"0.2 mm clearance for smooth mechanical mating."** Meaningless for a glyph. A symbol mates with
|
||||
nothing, and every gizmo here is sized in screen pixels via `upp`, so a millimetre tolerance has no
|
||||
referent. This is the strongest signal that **the brief was written for a physical printed part**,
|
||||
not for a viewport symbol — as are "scannable" and "mechanical mating". See the open question below.
|
||||
|
||||
### Two defects the build caught that discussion would not have
|
||||
|
||||
1. **The flank quads are not planar.** Written as `[0,3,5,4]` and `[1,4,5,2]` the base edge and the
|
||||
ridge edge are skew, so the four corners do not share a plane — my own first draft asserted the
|
||||
opposite in a comment. Left as quads, the tessellator picks the fold direction, the "flat facet"
|
||||
promise is broken by an unspecified crease, and two exporters can disagree about the shape. Fixed
|
||||
by triangulating explicitly (7 faces, Euler 6 − 11 + 7 = 2).
|
||||
2. **The pocket punched through its own plate.** A 4.5 mm key against a 3 mm demo plate gives a
|
||||
through-hole, not a pocket. Minimum stock = height + clearance + pocket depth + a wall.
|
||||
|
||||
Also worth recording: the first female render was misleading because the debug renderer outlined
|
||||
*every* triangle, so a flat top face triangulated by CGAL looked like a faceted dome. The instrument
|
||||
lied before the geometry did. Conclusions were only drawn after outlines were removed.
|
||||
|
||||
### Second opinion, and the one disagreement worth resolving
|
||||
|
||||
Kimi reviewed the proposal independently and **rejected it for the viewport**. It agreed on the two
|
||||
wrong premises, agreed the female pocket is unreadable, and added the useful framing that a
|
||||
screen-constant symbol and a model-constant part feature are two different design spaces that cannot
|
||||
be served by one geometry. It also noted correctly that there is **no single scalar** that removes
|
||||
ambiguity from every view: you need one asymmetry in the base plane (for top-down roll) and one out
|
||||
of plane (the ridge slope, for front/back). Our base is scalene, so it has both.
|
||||
|
||||
Its central objection was numeric and testable: *"at 22 px with 6–8 facets each facet is 3–7 px wide,
|
||||
that is at the aliasing limit … minimum useful size is roughly 32–48 px, which is not compatible with
|
||||
a 22 px screen-constant symbol."* My own renders were ~300 px, so the claim was unaddressed by my
|
||||
evidence and would have killed the concept if true.
|
||||
|
||||
**Rendered at 22, 32 and 48 px (`size-test.png`), it is false for this shape.** At 22 px all three
|
||||
views still read: the grazing view shows the tall back and shallow front unmistakably, and the
|
||||
down-axis view keeps a strong dark/light split. The reason Kimi's arithmetic does not apply is that
|
||||
this solid presents only **four or five large facets with high value contrast**, not eight small ones —
|
||||
the silhouette does most of the work, and silhouettes survive downsampling far better than facet
|
||||
detail does.
|
||||
|
||||
*Honest limit on that result:* the test renderer has no anti-aliasing, no perspective, one directional
|
||||
light, and no background. Readable at 22 px against white is not the same as readable at 22 px on top
|
||||
of a shaded gold part next to the move gizmo. That case still needs the rig.
|
||||
|
||||
**Where I do not follow Kimi:** its recommendation is to **billboard** the existing flat glyph so it
|
||||
never turns edge-on. That kills F4 by construction, but a billboarded frame cannot show the frame's
|
||||
orientation *in place* — which is the entire reason the disc is a disc and not a dot — and it is what
|
||||
no surveyed CAD system does; Onshape, Fusion and FreeCAD all draw the frame in the geometry. Worth
|
||||
prototyping as an option, not worth adopting on argument.
|
||||
|
||||
### Open question for Tommaso
|
||||
|
||||
**Is this a viewport glyph or a printable alignment feature?** The vertex logic is identical either
|
||||
way; only the units and the clearance change, and the `.scad` file states both readings. But the
|
||||
answer decides whether `clr`/`depth` are real millimetres or meaningless, and whether the geometry
|
||||
scales with the model or stays screen-constant. The brief's own language points at "physical", the
|
||||
conversation it arrived in points at "glyph".
|
||||
|
||||
---
|
||||
|
||||
## 9. Decisions for you
|
||||
|
||||
**D1 — Invert the default direction to Facing?** [DEVIATION, R5]
|
||||
It changes the meaning of every stored document containing a mate. Options: (a) invert and migrate,
|
||||
writing `direction=Aligned` where `mate_flip` was false; (b) invert only for new mates and store
|
||||
`direction` explicitly from now on. (b) is safer and costs one field. Note this project has taken one
|
||||
such semantic hit knowingly before — the `en4` fix — and the golden fixture survived, so the
|
||||
migration path is a known quantity. **If G3 (live preview) lands first, this matters much less.**
|
||||
|
||||
**D2 — How far to take origin candidates?** [R9]
|
||||
Four kinds is the Fusion-aligned recommendation. Two (face centroid + arc centre) would cover "sit on
|
||||
a face" and "go down a hole" — most printed-part assembly — at a third of the work. Where do you want
|
||||
to stop?
|
||||
|
||||
**D3 — Ball mate: in or out?**
|
||||
In four of five frame-based systems, so including it is the aligned choice. Out is defensible for
|
||||
printable mechanical parts. Cheap either way — align origins, leave orientation free. Kimi's review
|
||||
argued **out**: a true ball joint is hard to print and hard to use without a roll reference, and a
|
||||
Fastened connector at the ball centre approximates it.
|
||||
|
||||
**D3a — Should Planar be dropped?** [dissent worth recording]
|
||||
Kimi's independent review recommended **removing Planar** and shipping four types, on the grounds that
|
||||
"slide on a flat surface" is rarely how printed mechanisms work — you usually want a rail or a hinge —
|
||||
and that Planar is the type most likely to confuse a user who expected "put this flat on that" and got
|
||||
a part free to slide. It further ranked the honest minimum as **three**: Fastened, Revolute, Slider,
|
||||
with Cylindrical useful and decomposable.
|
||||
**I do not agree, and the reason is alignment.** Planar appears in every frame-based system surveyed,
|
||||
it is a genuine lower pair, it is already implemented and tested, and removing it is a document-format
|
||||
change made in exchange for nothing. The confusion Kimi names is real but it is a *feedback* problem —
|
||||
it is exactly what R17 (show the DOF budget) and R13 (say that free DOF are preserved) exist to fix.
|
||||
Recorded here because it is a legitimate reading of the same evidence and the call is yours.
|
||||
|
||||
**D4 — Is refusing a second mate per body acceptable?** [DEVIATION, R11 — the big one]
|
||||
It is the honest consequence of having no solver, and it is what makes the tool predictable. But **no
|
||||
mainstream system behaves this way**, so it is the point where an experienced user's intuition will
|
||||
break. It means a part cannot be constrained by two independent relationships — "in this hole *and*
|
||||
resting on this shoulder" must be expressed by placing one connector correctly rather than by two
|
||||
mates. If that trade is unacceptable, the answer is a solver, and the scope of this document changes
|
||||
entirely.
|
||||
|
||||
There is a strong argument that the trade is not merely acceptable but *correct for this product*:
|
||||
the Design tab lives inside a slicer, and most of its users are positioning parts for printing rather
|
||||
than building working mechanisms. For layout-and-export, tree-order composition is genuinely enough,
|
||||
and adding a solver to look like Onshape would buy complexity nobody asked for. The rule to publish is
|
||||
then simple and defensible: **one mate per moving body, acyclic, no relations between mates** — with
|
||||
R18's loud refusals carrying the honesty.
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
**Onshape** — [Mate Connector](https://cad.onshape.com/help/Content/PartStudio/mate_connector.htm) ·
|
||||
[Mates](https://cad.onshape.com/help/Content/Assembly/mates.htm) ·
|
||||
[Fastened](https://cad.onshape.com/help/Content/Assembly/fastened_mate.htm) ·
|
||||
[Revolute](https://cad.onshape.com/help/Content/Assembly/revolute_mate.htm) ·
|
||||
[Slider](https://cad.onshape.com/help/Content/Assembly/slider_mate.htm) ·
|
||||
[Cylindrical](https://cad.onshape.com/help/Content/Assembly/cylindrical_mate.htm) ·
|
||||
[Planar](https://cad.onshape.com/help/Content/Assembly/planar_mate.htm) ·
|
||||
[Ball](https://cad.onshape.com/help/Content/Assembly/ball_mate.htm) ·
|
||||
[Parallel](https://cad.onshape.com/help/Content/Assembly/parallel_mate.htm) ·
|
||||
[Tangent](https://cad.onshape.com/help/Content/Assembly/tangent_mate.htm) ·
|
||||
[Pin Slot](https://cad.onshape.com/help/Content/Assembly/pin_slot_mate.htm) ·
|
||||
[5 things you can do with mate connectors in Part Studios](https://www.onshape.com/en/resource-center/tech-tips/tech-tip-5-things-you-can-do-with-mate-connectors-in-onshape-part-studios)
|
||||
|
||||
**Onshape forum** — [The concept behind Mates Z Axes](https://forum.onshape.com/discussion/22828/the-concept-behind-mates-z-axes) (C1/D3) ·
|
||||
[Implicit mate connectors act differently than explicit ones](https://forum.onshape.com/discussion/15736/implicit-mate-connectors-act-differently-than-explicit-ones) (C4) ·
|
||||
[Efficiently set mate connectors](https://forum.onshape.com/discussion/13133/efficiently-set-mate-connectors)
|
||||
|
||||
**Fusion 360** — [Joint types](https://help.autodesk.com/cloudhelp/ENU/Fusion-Assemble/files/GUID-8818AE31-958A-4A59-989B-9875A174C67A.htm) ·
|
||||
[Joint origins](https://help.autodesk.com/view/fusion360/ENU/?guid=ASM-JOINT-ORIGIN) ·
|
||||
[Joints vs. Mates in Fusion](https://www.autodesk.com/products/fusion-360/blog/joints-mates-moving-fusion/) ·
|
||||
[Joint tips — snap points and Ctrl cycling](https://mgfx.co.za/blog/engineering-manufacturing-design/fusion-360-joint-tips/)
|
||||
|
||||
**Inventor** — [Create Joints Reference](https://help.autodesk.com/cloudhelp/2026/ENU/Inventor-Help/files/GUID-6AA68E8F-7C97-4806-8483-3941DE915E70.htm) ·
|
||||
[Use Joint to define and manage relationships](https://knowledge.autodesk.com/support/inventor-products/learn-explore/caas/CloudHelp/cloudhelp/2014/ENU/Inventor/files/GUID-21DC3336-5C51-42C1-90FB-4299CD66E0C6-htm.html) (type inference, D2)
|
||||
|
||||
**FreeCAD 1.0** — [Assembly Workbench](https://wiki.freecad.org/Assembly_Workbench) ·
|
||||
[Fixed Joint properties](https://wiki.freecad.org/Assembly_CreateJointFixed)
|
||||
|
||||
**Creo** — [About Predefined Constraint Sets](https://support.ptc.com/help/creo/creo_pma/r12/usascii/assembly/asm/About_Predefined_Constraint_Sets.html)
|
||||
|
||||
**Siemens NX** — [Assembly constraints](https://learnnx.com/lesson/siemens-nx-assemblies-assembly-constraints/)
|
||||
|
||||
**SOLIDWORKS** — [Mate References](https://help.solidworks.com/2025/English/SolidWorks/sldworks/c_Mate_References_Overview_SWassy.htm) ·
|
||||
[Creating and using mate references](https://blogs.solidworks.com/tech/2019/07/creating-and-using-mate-references.html)
|
||||
|
||||
**Theory** — [Hervé, The Lie group of rigid body displacements, a fundamental tool for mechanism design](https://www.sciencedirect.com/science/article/abs/pii/S0094114X98000512) ·
|
||||
[Joint kinematics — the six lower pairs and their DOF](https://erc-bpgc.github.io/handbook/mechanical/Joint%20Kinematics/) ·
|
||||
[ISO 10303-105 — Kinematics (STEP integrated resource)](https://www.iso.org/standard/78589.html)
|
||||
|
||||
**Internal** — `en4` (closed 2026-07-26, fixes C2 here) · `CadDocument.cpp:1669`
|
||||
`datum_frame` · `CadDocument.cpp:2961` `apply_mate` · `CadDocument.cpp:1302` `add_mate`
|
||||
|
||||
**Second opinion** — an independent review by Kimi Code (2026-08-05) contributed the
|
||||
vendors-ship-both caveat (§1), the explicit-dropdown option for origin choice (D1), the expanded
|
||||
refusal list (R11a), the retrofit list (§8), and the dissents recorded at D3/D3a. One of its claims —
|
||||
that Onshape mandates *"exactly one Mate between any two instances"* — **was checked against the
|
||||
source and is wrong**; the correction is recorded at R11 because it is a misreading that would
|
||||
otherwise turn our largest deviation into a false agreement.
|
||||
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,30 +0,0 @@
|
||||
// Emitted by doc/design/mate-connectors/emit_glyph_table.py from bear.step — do not hand-edit.
|
||||
// Normalised to the part's bounding span and centred: the renderer scales by one radius.
|
||||
static const Vec2d kBearOutline[] = { // 12 verts, RDP eps 0.030, CCW
|
||||
{+0.3842, +0.3294}, {+0.3156, +0.4002}, {+0.2424, +0.3294},
|
||||
{-0.2524, +0.3294}, {-0.3377, +0.3877}, {-0.3693, +0.3298},
|
||||
{-0.3256, +0.2631}, {-0.4893, -0.3337}, {-0.3960, -0.4002},
|
||||
{+0.4151, -0.4002}, {+0.5000, -0.3154}, {+0.3156, +0.2631},
|
||||
};
|
||||
static const Vec2d kBearChin[] = { // the CHIN BAR, flat. The muzzle is relief — see kBearCrest.
|
||||
{-0.2682, -0.3578}, {+0.2628, -0.3578}, {+0.2237, -0.1786},
|
||||
};
|
||||
// {cx, cy, r}: two eyes, then the cheek dot that carries handedness (wi3z).
|
||||
static const Vec3d kBearMarks[] = {
|
||||
{-0.1997, +0.1760, +0.0590},
|
||||
{+0.1947, +0.1760, +0.0590},
|
||||
{+0.2797, +0.0760, +0.0380},
|
||||
};
|
||||
// THE MUZZLE, lifted off the mesh: a tapered wedge, base quad + crest edge, 6 facets.
|
||||
// This is the only feature standing along +Z and the only one still legible edge-on.
|
||||
static const double kBearPlateZ = +0.0360;
|
||||
static const Vec2d kBearSnoutBase[] = { // CCW from the nose end
|
||||
{-0.0727, -0.2417},
|
||||
{+0.0630, -0.2417},
|
||||
{+0.0259, +0.1939},
|
||||
{-0.0356, +0.1939},
|
||||
};
|
||||
static const Vec3d kBearCrest[] = { // nose (tall) -> tail (short)
|
||||
{-0.0048, -0.1793, +0.2073},
|
||||
{-0.0048, +0.1605, +0.1279},
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
{"outer": [[26.711, -55.263], [42.071, -7.071], [35.0, -0.0], [-32.575, 0.0], [-33.717, -0.112], [-34.815, -0.446], [-35.828, -0.987], [-36.715, -1.715], [-39.55, -4.55], [-40.35, -5.547], [-40.914, -6.694], [-41.216, -7.937], [-41.241, -9.215], [-40.988, -10.468], [-26.711, -55.263], [-28.828, -57.312], [-29.64, -58.335], [-30.159, -59.532], [-30.35, -60.823], [-30.201, -62.12], [-29.721, -63.334], [-28.944, -64.382], [-27.718, -65.649], [-27.075, -66.035], [-26.325, -66.047], [-25.67, -65.683], [-20.613, -60.789], [20.613, -60.789], [26.711, -66.69], [32.421, -60.789], [26.711, -55.263]], "holes": [{"pts": [[19.052, -18.464], [16.474, -9.14], [-0.0, -9.104], [-21.926, -9.104], [-21.926, -3.535], [22.308, -3.535], [19.052, -18.464]], "cx": 4.719, "cz": -10.192, "d": 44.234}, {"pts": [[-11.493, -48.01], [-11.676, -49.341], [-12.211, -50.574], [-13.06, -51.617], [-14.158, -52.392], [-15.424, -52.842], [-16.765, -52.934], [-18.081, -52.66], [-19.274, -52.042], [-20.257, -51.124], [-20.955, -49.976], [-21.318, -48.682], [-21.318, -47.337], [-20.955, -46.043], [-20.257, -44.895], [-19.274, -43.977], [-18.081, -43.359], [-16.765, -43.085], [-15.424, -43.177], [-14.158, -43.627], [-13.06, -44.402], [-12.211, -45.445], [-11.676, -46.678], [-11.493, -48.01]], "cx": -16.223, "cz": -48.01, "d": 9.825}, {"pts": [[21.364, -48.01], [21.181, -49.341], [20.645, -50.574], [19.797, -51.617], [18.699, -52.392], [17.432, -52.842], [16.091, -52.934], [14.775, -52.66], [13.582, -52.042], [12.6, -51.124], [11.901, -49.976], [11.539, -48.682], [11.539, -47.337], [11.901, -46.043], [12.6, -44.895], [13.582, -43.977], [14.775, -43.359], [16.091, -43.085], [17.432, -43.177], [18.699, -43.627], [19.797, -44.402], [20.645, -45.445], [21.181, -46.678], [21.364, -48.01]], "cx": 16.634, "cz": -48.01, "d": 9.825}]}
|
||||
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 22 KiB |
@@ -1,299 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Mate connector glyph — polarity and verse</title>
|
||||
<style>
|
||||
:root {
|
||||
--ground: #eceef1;
|
||||
--panel: #f8f9fb;
|
||||
--panel-edge: #d3d8df;
|
||||
--ink: #171a1f;
|
||||
--ink-soft: #5a626e;
|
||||
--ink-faint: #8b93a0;
|
||||
--viewport: #9aa0a8; /* the grey a CAD viewport actually is */
|
||||
--viewport-2: #7f858d;
|
||||
--axis-z: #2f6fed;
|
||||
--axis-x: #d94a3d;
|
||||
--axis-y: #3aa757;
|
||||
--quadrant: #e8a317;
|
||||
--anchor: #6b7280;
|
||||
--driven: #2f6fed;
|
||||
--warn: #c2410c;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--ground: #14171c;
|
||||
--panel: #1b1f26;
|
||||
--panel-edge: #2b313a;
|
||||
--ink: #e8eaee;
|
||||
--ink-soft: #a6aeba;
|
||||
--ink-faint: #6e7784;
|
||||
--viewport: #4a5058;
|
||||
--viewport-2: #3a3f46;
|
||||
--axis-z: #6ea2ff;
|
||||
--axis-x: #ff7a6d;
|
||||
--axis-y: #5fd07f;
|
||||
--quadrant: #ffc247;
|
||||
--anchor: #9aa3b0;
|
||||
--driven: #6ea2ff;
|
||||
--warn: #fb923c;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"] {
|
||||
--ground:#14171c; --panel:#1b1f26; --panel-edge:#2b313a; --ink:#e8eaee;
|
||||
--ink-soft:#a6aeba; --ink-faint:#6e7784; --viewport:#4a5058; --viewport-2:#3a3f46;
|
||||
--axis-z:#6ea2ff; --axis-x:#ff7a6d; --axis-y:#5fd07f; --quadrant:#ffc247;
|
||||
--anchor:#9aa3b0; --driven:#6ea2ff; --warn:#fb923c;
|
||||
}
|
||||
:root[data-theme="light"] {
|
||||
--ground:#eceef1; --panel:#f8f9fb; --panel-edge:#d3d8df; --ink:#171a1f;
|
||||
--ink-soft:#5a626e; --ink-faint:#8b93a0; --viewport:#9aa0a8; --viewport-2:#7f858d;
|
||||
--axis-z:#2f6fed; --axis-x:#d94a3d; --axis-y:#3aa757; --quadrant:#e8a317;
|
||||
--anchor:#6b7280; --driven:#2f6fed; --warn:#c2410c;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; padding: 40px 24px 72px;
|
||||
background: var(--ground); color: var(--ink);
|
||||
font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
.wrap { max-width: 1000px; margin: 0 auto; display: flex; flex-direction: column; gap: 28px; }
|
||||
header { display: flex; flex-direction: column; gap: 6px; }
|
||||
h1 { font-size: 26px; line-height: 1.25; margin: 0; letter-spacing: -0.01em; text-wrap: balance; }
|
||||
.sub { color: var(--ink-soft); max-width: 62ch; margin: 0; }
|
||||
.eyebrow {
|
||||
font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase;
|
||||
color: var(--ink-faint); font-weight: 600;
|
||||
}
|
||||
h2 {
|
||||
font-size: 13px; letter-spacing: 0.1em; text-transform: uppercase;
|
||||
color: var(--ink-faint); margin: 16px 0 0; font-weight: 600;
|
||||
}
|
||||
.row { display: flex; flex-wrap: wrap; gap: 16px; }
|
||||
.card {
|
||||
background: var(--panel); border: 1px solid var(--panel-edge);
|
||||
border-radius: 10px; padding: 18px; flex: 1 1 220px; min-width: 220px;
|
||||
display: flex; flex-direction: column; gap: 10px;
|
||||
}
|
||||
.card.wide { flex: 1 1 100%; }
|
||||
.stage { display: flex; align-items: center; justify-content: center; padding: 4px 0; }
|
||||
.name { font-weight: 650; font-size: 15px; }
|
||||
.note { color: var(--ink-soft); font-size: 13.5px; margin: 0; }
|
||||
.k { color: var(--ink); font-weight: 600; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 14px; }
|
||||
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--panel-edge); vertical-align: top; }
|
||||
th { color: var(--ink-faint); font-weight: 600; font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase; }
|
||||
code { font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--ink-soft); }
|
||||
.legend { display: flex; flex-wrap: wrap; gap: 14px; font-size: 13px; color: var(--ink-soft); }
|
||||
.swatch { display: inline-flex; align-items: center; gap: 7px; }
|
||||
.dot { width: 11px; height: 11px; border-radius: 50%; display: inline-block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<header>
|
||||
<div class="eyebrow">Orca Design · assembly</div>
|
||||
<h1>Mate connector glyph — polarity and verse</h1>
|
||||
<p class="sub">
|
||||
Onshape's core (disc + roll quadrant + Z arrow) is adopted unchanged because it is proven and
|
||||
aligned. The addition is <span class="k">polarity</span> — which connector is anchored and
|
||||
which one travels — which no surveyed CAD system encodes in its glyph.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<h2>The three jobs of the glyph</h2>
|
||||
<div class="row">
|
||||
<div class="card">
|
||||
<div class="stage">
|
||||
<svg width="150" height="130" viewBox="-75 -95 150 130" aria-label="Disc with origin dot">
|
||||
<ellipse cx="0" cy="0" rx="42" ry="17" fill="none" stroke="var(--ink-soft)" stroke-width="2.5"/>
|
||||
<circle cx="0" cy="0" r="3.6" fill="var(--ink)"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="name">Disc — the XY plane</div>
|
||||
<p class="note">Says “I am a frame, and this is the plane I sit in.” The dot is the exact origin.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="stage">
|
||||
<svg width="150" height="130" viewBox="-75 -95 150 130" aria-label="Disc with one quadrant filled">
|
||||
<path d="M0,0 L42,0 A42,17 0 0 1 0,17 Z" fill="var(--quadrant)" opacity="0.9"/>
|
||||
<ellipse cx="0" cy="0" rx="42" ry="17" fill="none" stroke="var(--ink-soft)" stroke-width="2.5"/>
|
||||
<circle cx="0" cy="0" r="3.6" fill="var(--ink)"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="name">Quadrant — the roll</div>
|
||||
<p class="note">
|
||||
The filled sector is the +X/+Y quadrant. It steps 90° with the reorient control, so the
|
||||
clocking that Fastened and Slider lock is <em>visible</em> before you commit.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="stage">
|
||||
<svg width="150" height="130" viewBox="-75 -95 150 130" aria-label="Z arrow drawn only upward">
|
||||
<path d="M0,0 L42,0 A42,17 0 0 1 0,17 Z" fill="var(--quadrant)" opacity="0.9"/>
|
||||
<ellipse cx="0" cy="0" rx="42" ry="17" fill="none" stroke="var(--ink-soft)" stroke-width="2.5"/>
|
||||
<line x1="0" y1="0" x2="0" y2="-58" stroke="var(--axis-z)" stroke-width="3.5" stroke-linecap="round"/>
|
||||
<polygon points="0,-80 -9.5,-56 9.5,-56" fill="var(--axis-z)"/>
|
||||
<circle cx="0" cy="0" r="3.6" fill="var(--ink)"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="name">Arrow — the verse</div>
|
||||
<p class="note">
|
||||
Drawn on <span class="k">+Z only</span>. Nothing below the disc. A double-headed axis is what
|
||||
makes people ask which way it points; a one-sided arrow cannot be misread.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Polarity — the part nobody else draws</h2>
|
||||
<div class="row">
|
||||
<div class="card">
|
||||
<div class="stage">
|
||||
<svg width="170" height="150" viewBox="-85 -105 170 150" aria-label="Fixed connector, open collar">
|
||||
<path d="M0,0 L42,0 A42,17 0 0 1 0,17 Z" fill="var(--quadrant)" opacity="0.55"/>
|
||||
<ellipse cx="0" cy="0" rx="42" ry="17" fill="none" stroke="var(--anchor)" stroke-width="2.5"/>
|
||||
<line x1="0" y1="0" x2="0" y2="-56" stroke="var(--anchor)" stroke-width="3" stroke-linecap="round"/>
|
||||
<polygon points="0,-80 -9.5,-56 9.5,-56" fill="none" stroke="var(--anchor)" stroke-width="3" stroke-linejoin="round"/>
|
||||
<ellipse cx="0" cy="-56" rx="9.5" ry="3.6" fill="none" stroke="var(--anchor)" stroke-width="2.2"/>
|
||||
<circle cx="0" cy="0" r="3.6" fill="var(--anchor)"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="name">Fixed — the socket</div>
|
||||
<p class="note">
|
||||
Hollow head, muted colour. This body <span class="k">does not move</span>. It receives.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="stage">
|
||||
<svg width="170" height="150" viewBox="-85 -105 170 150" aria-label="Driven connector, solid cone">
|
||||
<path d="M0,0 L42,0 A42,17 0 0 1 0,17 Z" fill="var(--quadrant)" opacity="0.95"/>
|
||||
<ellipse cx="0" cy="0" rx="42" ry="17" fill="none" stroke="var(--driven)" stroke-width="2.5"/>
|
||||
<line x1="0" y1="0" x2="0" y2="-58" stroke="var(--driven)" stroke-width="3.5" stroke-linecap="round"/>
|
||||
<polygon points="0,-80 -9.5,-56 9.5,-56" fill="var(--driven)"/>
|
||||
<circle cx="0" cy="0" r="3.6" fill="var(--driven)"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="name">Driven — the plug</div>
|
||||
<p class="note">
|
||||
Solid head, active colour. This body <span class="k">is the one that jumps</span>. It inserts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="stage">
|
||||
<svg width="170" height="150" viewBox="-85 -105 170 150" aria-label="Degenerate roll, hatched quadrant">
|
||||
<defs>
|
||||
<pattern id="hatch" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
|
||||
<line x1="0" y1="0" x2="0" y2="6" stroke="var(--warn)" stroke-width="2"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<path d="M0,0 L42,0 A42,17 0 0 1 0,17 Z" fill="url(#hatch)" opacity="0.85"/>
|
||||
<ellipse cx="0" cy="0" rx="42" ry="17" fill="none" stroke="var(--warn)" stroke-width="2.5" stroke-dasharray="5 4"/>
|
||||
<line x1="0" y1="0" x2="0" y2="-58" stroke="var(--axis-z)" stroke-width="3.5" stroke-linecap="round"/>
|
||||
<polygon points="0,-80 -9.5,-56 9.5,-56" fill="var(--axis-z)"/>
|
||||
<circle cx="0" cy="0" r="3.6" fill="var(--ink)"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="name">Roll undefined</div>
|
||||
<p class="note">
|
||||
Hatched quadrant, dashed disc: a circular face or a seam gave no usable direction. Says
|
||||
“pick a direction” without a dialog.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>The pair reads as a magnet</h2>
|
||||
<div class="card wide">
|
||||
<div class="stage">
|
||||
<svg width="620" height="230" viewBox="-310 -120 620 230" aria-label="Two connectors facing each other on two plates">
|
||||
<!-- lower plate (fixed) -->
|
||||
<path d="M-260,52 L-60,10 L60,44 L-140,86 Z" fill="var(--viewport)" stroke="var(--viewport-2)" stroke-width="1.5"/>
|
||||
<!-- upper plate (driven) -->
|
||||
<path d="M-60,-96 L140,-138 L260,-104 L60,-62 Z" fill="var(--viewport)" stroke="var(--viewport-2)" stroke-width="1.5" opacity="0.55"/>
|
||||
|
||||
<!-- dashed field line between origins -->
|
||||
<line x1="-100" y1="48" x2="100" y2="-79" stroke="var(--ink-faint)" stroke-width="2" stroke-dasharray="7 6"/>
|
||||
|
||||
<!-- FIXED connector, pointing up (+Z out of the lower plate) -->
|
||||
<g transform="translate(-100,48)">
|
||||
<path d="M0,0 L38,0 A38,15 0 0 1 0,15 Z" fill="var(--quadrant)" opacity="0.5"/>
|
||||
<ellipse cx="0" cy="0" rx="38" ry="15" fill="none" stroke="var(--anchor)" stroke-width="2.4"/>
|
||||
<line x1="0" y1="0" x2="0" y2="-48" stroke="var(--anchor)" stroke-width="3" stroke-linecap="round"/>
|
||||
<polygon points="0,-70 -9,-48 9,-48" fill="none" stroke="var(--anchor)" stroke-width="3" stroke-linejoin="round"/>
|
||||
<ellipse cx="0" cy="-48" rx="9" ry="3.4" fill="none" stroke="var(--anchor)" stroke-width="2"/>
|
||||
<circle cx="0" cy="0" r="3.4" fill="var(--anchor)"/>
|
||||
</g>
|
||||
|
||||
<!-- DRIVEN connector, pointing down (+Z out of the upper plate's underside) -->
|
||||
<g transform="translate(100,-79) rotate(180)">
|
||||
<path d="M0,0 L38,0 A38,15 0 0 1 0,15 Z" fill="var(--quadrant)" opacity="0.9"/>
|
||||
<ellipse cx="0" cy="0" rx="38" ry="15" fill="none" stroke="var(--driven)" stroke-width="2.4"/>
|
||||
<line x1="0" y1="0" x2="0" y2="-50" stroke="var(--driven)" stroke-width="3.4" stroke-linecap="round"/>
|
||||
<polygon points="0,-70 -9,-48 9,-48" fill="var(--driven)"/>
|
||||
<circle cx="0" cy="0" r="3.4" fill="var(--driven)"/>
|
||||
</g>
|
||||
|
||||
<text x="-100" y="102" text-anchor="middle" font-size="13" fill="var(--ink-soft)">fixed · receives</text>
|
||||
<text x="100" y="-100" text-anchor="middle" font-size="13" fill="var(--ink-soft)">driven · inserts</text>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="note">
|
||||
Two arrows nose to nose. Because a magnet's north seeks a south, <span class="k">“facing” is the
|
||||
self-evident default</span> — which settles open decision D1 on visual grounds instead of a
|
||||
convention nobody can look up. Nothing has to be remembered: the picture is the rule.
|
||||
The dashed line is what makes the two glyphs read as one object.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2>States</h2>
|
||||
<div class="card wide">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>State</th><th>Drawing</th><th>Why</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="k">Candidate</span> (hover)</td>
|
||||
<td>small dot only</td>
|
||||
<td>Onshape draws plain white dots at every corner and midpoint. Dots propose; the full glyph commits.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="k">Picked</span></td>
|
||||
<td>disc + quadrant + cone</td>
|
||||
<td>The committed frame, with roll and verse both readable.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="k">Roll undefined</span></td>
|
||||
<td>hatched quadrant, dashed disc</td>
|
||||
<td>Turns requirement R8 from a message nobody reads into a mark you cannot miss.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2>Constraints on the drawing</h2>
|
||||
<div class="card wide">
|
||||
<p class="note">
|
||||
<span class="k">Do not make it a fourth RGB triad.</span> The bed-centre world triad
|
||||
(<code>DesignCanvas.cpp:65</code>) and the move gizmo are already three coloured arrows. The disc
|
||||
and the quadrant are what tell a connector apart from those — keep the arms short, and consider
|
||||
drawing only Z on the committed glyph, with X and Y implied by the quadrant.
|
||||
</p>
|
||||
<div class="legend">
|
||||
<span class="swatch"><i class="dot" style="background:var(--quadrant)"></i> roll quadrant</span>
|
||||
<span class="swatch"><i class="dot" style="background:var(--axis-z)"></i> Z / driven</span>
|
||||
<span class="swatch"><i class="dot" style="background:var(--anchor)"></i> fixed</span>
|
||||
<span class="swatch"><i class="dot" style="background:var(--warn)"></i> roll undefined</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,68 +0,0 @@
|
||||
# Does the connector pair let two hosts sit COPLANAR, or does it hold them apart?
|
||||
#
|
||||
# The male's flat back is the plane Y=0 and all its relief rises to +Y. So Y=0 is the natural
|
||||
# mating datum: everything the male adds lives on one side of it. The test below builds two dummy
|
||||
# host plates that meet on that plane -- one with the male FUSED on, one with the cavity CUT in --
|
||||
# and measures whether they touch, interfere, or stand apart.
|
||||
#
|
||||
# It also emits the artifact that makes this work in practice: a CUTTER solid (the male grown by
|
||||
# the clearance) that you subtract from any host. A standalone female block cannot keep two hosts
|
||||
# coplanar, because its own floor material stands between them; a cavity can.
|
||||
#
|
||||
# Run: /snap/bin/freecad.cmd coplanar_test.py
|
||||
|
||||
import os
|
||||
import FreeCAD as App
|
||||
import Part
|
||||
from FreeCAD import Vector
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
MALE = os.path.join(HERE, "bear.step")
|
||||
CLEAR = 0.20
|
||||
|
||||
male = Part.Shape(); male.read(MALE); male = male.Solids[0]
|
||||
bb = male.BoundBox
|
||||
print(f"male relief: Y {bb.YMin:.3f} .. {bb.YMax:.3f} -> datum plane Y=0, all relief on +Y")
|
||||
|
||||
# the flat back face, and proof it is the whole silhouette sitting on Y=0
|
||||
back = max((f for f in male.Faces
|
||||
if abs(f.CenterOfMass.y) < 1e-6 and abs(abs(f.normalAt(0, 0).y) - 1) < 1e-6),
|
||||
key=lambda f: f.Area)
|
||||
print(f"back face : {back.Area:.1f} mm2 on Y=0 -- this is the contact surface")
|
||||
|
||||
# ---- the cutter: the male grown by the clearance, poking 0.2 mm proud so the boolean is clean
|
||||
cutter = male.makeOffsetShape(CLEAR, 1e-6, False, False, 0, 2, False).Solids[0]
|
||||
cb = cutter.BoundBox
|
||||
print(f"cutter : Y {cb.YMin:.3f} .. {cb.YMax:.3f}, {cutter.Volume/1000:.2f} cm3")
|
||||
|
||||
# ---- two dummy hosts meeting on Y = 0
|
||||
W, H = 120.0, 100.0
|
||||
hostA = Part.makeBox(W, 10.0, H, Vector(-W/2, -10.0, -15.0)) # occupies Y -10..0
|
||||
hostB = Part.makeBox(W, 30.0, H, Vector(-W/2, 0.0, -15.0)) # occupies Y 0..30
|
||||
|
||||
partA = hostA.fuse(male) # male stands proud of A's face
|
||||
partB = hostB.cut(cutter) # cavity sunk into B from its face
|
||||
|
||||
print(f"\npart A (host + male) : {partA.Volume/1000:.2f} cm3")
|
||||
print(f"part B (host - cutter) : {partB.Volume/1000:.2f} cm3")
|
||||
|
||||
# ---- the question ------------------------------------------------------------------
|
||||
inter = partA.common(partB)
|
||||
iv = inter.Volume if inter.Solids else 0.0
|
||||
gap = partA.distToShape(partB)[0]
|
||||
print(f"\nRESULT interference A vs B : {iv:.6f} mm3 (0 = they do not collide)")
|
||||
print(f"RESULT closest approach : {gap:.4f} mm (0 = the host faces are touching)")
|
||||
|
||||
# are the two host faces actually on the same plane?
|
||||
fa = [f for f in partA.Faces if abs(f.CenterOfMass.y) < 1e-9 and abs(abs(f.normalAt(0,0).y)-1) < 1e-6]
|
||||
fb = [f for f in partB.Faces if abs(f.CenterOfMass.y) < 1e-9 and abs(abs(f.normalAt(0,0).y)-1) < 1e-6]
|
||||
print(f"RESULT A has {len(fa)} face(s) lying exactly on Y=0, total {sum(f.Area for f in fa):.1f} mm2")
|
||||
print(f"RESULT B has {len(fb)} face(s) lying exactly on Y=0, total {sum(f.Area for f in fb):.1f} mm2")
|
||||
print("RESULT -> the hosts meet on Y=0: COPLANAR" if fa and fb and iv < 1e-3
|
||||
else "RESULT -> NOT coplanar")
|
||||
|
||||
doc = App.newDocument("Cutter")
|
||||
o = doc.addObject("Part::Feature", "BearConnector_Cutter"); o.Shape = cutter
|
||||
doc.recompute()
|
||||
Part.export([o], os.path.join(HERE, "BearConnector_Cutter.step"))
|
||||
print(f"\nwrote BearConnector_Cutter.step -- subtract this from any host to get the socket")
|
||||
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,97 +0,0 @@
|
||||
"""Emit the simplified bear as a C++ table for the viewport glyph — wi3z.
|
||||
|
||||
Everything is normalised to the part's own bounding span and centred, so the renderer scales by
|
||||
one radius R in screen pixels and nothing here carries millimetres. Emitting rather than
|
||||
hand-authoring keeps the glyph and the printed part from drifting apart: rerun this and the table
|
||||
follows the STEP.
|
||||
"""
|
||||
import json, math, os
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
D = json.load(open(os.path.join(HERE, "bear_outline.json")))
|
||||
|
||||
def unit_frame(pts_sets):
|
||||
allp=[p for s in pts_sets for p in s]
|
||||
xs=[p[0] for p in allp]; ys=[p[1] for p in allp]
|
||||
cx,cy=(min(xs)+max(xs))/2,(min(ys)+max(ys))/2
|
||||
span=max(max(xs)-min(xs), max(ys)-min(ys))
|
||||
return cx,cy,span
|
||||
|
||||
outer=[(x,-z) for x,z in D["outer"]]
|
||||
holes=[[(x,-z) for x,z in h["pts"]] for h in D["holes"]]
|
||||
CX,CY,SPAN = unit_frame([outer]+holes)
|
||||
U=lambda pts:[((x-CX)/SPAN,(y-CY)/SPAN) for x,y in pts]
|
||||
OUT=U(outer)
|
||||
EYES=[U(h) for h,m in zip(holes,D["holes"]) if m["d"]<20]
|
||||
MUZ =U([h for h,m in zip(holes,D["holes"]) if m["d"]>=20][0])
|
||||
|
||||
def rdp(p,eps):
|
||||
if len(p)<3: return p
|
||||
ax,ay=p[0]; bx,by=p[-1]; dx,dy=bx-ax,by-ay; n=math.hypot(dx,dy)
|
||||
best,bi=-1.0,0
|
||||
for i in range(1,len(p)-1):
|
||||
px,py=p[i]
|
||||
d=abs(dx*(ay-py)-(ax-px)*dy)/n if n>1e-12 else math.hypot(px-ax,py-ay)
|
||||
if d>best: best,bi=d,i
|
||||
if best<=eps: return [p[0],p[-1]]
|
||||
return rdp(p[:bi+1],eps)[:-1]+rdp(p[bi:],eps)
|
||||
def simp(p,eps):
|
||||
r=rdp(p+[p[0]],eps); return r[:-1]
|
||||
|
||||
OUT_S = simp(OUT,.030) # 22 verts, the size the study settled on
|
||||
# wind counter-clockwise so the renderer's normals come out facing +Z
|
||||
def area2(p): return sum(p[i][0]*p[(i+1)%len(p)][1]-p[(i+1)%len(p)][0]*p[i][1] for i in range(len(p)))
|
||||
if area2(OUT_S) < 0: OUT_S = OUT_S[::-1]
|
||||
|
||||
def centroid(p): return (sum(q[0] for q in p)/len(p), sum(q[1] for q in p)/len(p))
|
||||
E=[]
|
||||
for e in EYES:
|
||||
c=centroid(e); r=(max(p[0] for p in e)-min(p[0] for p in e))/2
|
||||
E.append((c[0],c[1],r))
|
||||
E.sort()
|
||||
|
||||
lo=min(p[1] for p in MUZ); hi=max(p[1] for p in MUZ)
|
||||
bottom=[p for p in MUZ if p[1] < lo+0.06*(hi-lo)]
|
||||
apex=max(MUZ,key=lambda p:p[1])
|
||||
TRI=[min(bottom),max(bottom),apex]
|
||||
if area2(TRI)<0: TRI=TRI[::-1]
|
||||
|
||||
# the cheek dot: the handedness mark adopted after the mirror-difference study
|
||||
DOT=(E[1][0]+0.085, E[1][1]-0.10, 0.038)
|
||||
|
||||
# THE MUZZLE. Six facets lifted straight off the mesh -- every facet touching anything above the
|
||||
# 3 mm plate. Do NOT recompute the base from height*tan(draft): the first version did and produced
|
||||
# a needle, because the real base OVERHANGS the crest at both ends (0.062 at the nose, 0.034 at the
|
||||
# tail) and it is that overhang that makes it a tapered wedge instead of a blade.
|
||||
PLATE = 0.036 # 3.00 / 83.34
|
||||
SNOUT_BASE = ((-0.0727, -0.2417), (+0.0630, -0.2417), # nose end, 0.136 wide
|
||||
(+0.0259, +0.1939), (-0.0356, +0.1939)) # tail end, 0.062 wide
|
||||
CREST = ((-0.0048, -0.1793, 0.2073), (-0.0048, +0.1605, 0.1279))
|
||||
|
||||
def fmt(v): return f"{v:+.4f}"
|
||||
L=[]
|
||||
L.append(f"// Emitted by doc/design/mate-connectors/emit_glyph_table.py from bear.step — do not hand-edit.")
|
||||
L.append(f"// Normalised to the part's bounding span and centred: the renderer scales by one radius.")
|
||||
L.append(f"static const Vec2d kBearOutline[] = {{ // {len(OUT_S)} verts, RDP eps 0.030, CCW")
|
||||
for i in range(0,len(OUT_S),3):
|
||||
row=", ".join(f"{{{fmt(x)}, {fmt(y)}}}" for x,y in OUT_S[i:i+3])
|
||||
L.append(" "+row+",")
|
||||
L.append("};")
|
||||
L.append(f"static const Vec2d kBearChin[] = {{ // the CHIN BAR, flat. The muzzle is relief — see kBearCrest.")
|
||||
L.append(" "+", ".join(f"{{{fmt(x)}, {fmt(y)}}}" for x,y in TRI)+",")
|
||||
L.append("};")
|
||||
L.append("// {cx, cy, r}: two eyes, then the cheek dot that carries handedness (wi3z).")
|
||||
L.append("static const Vec3d kBearMarks[] = {")
|
||||
for cx,cy,r in E: L.append(f" {{{fmt(cx)}, {fmt(cy)}, {fmt(r)}}},")
|
||||
L.append(f" {{{fmt(DOT[0])}, {fmt(DOT[1])}, {fmt(DOT[2])}}},")
|
||||
L.append("};")
|
||||
L.append("// THE MUZZLE, lifted off the mesh: a tapered wedge, base quad + crest edge, 6 facets.")
|
||||
L.append("// This is the only feature standing along +Z and the only one still legible edge-on.")
|
||||
L.append(f"static const double kBearPlateZ = {PLATE:+.4f};")
|
||||
L.append("static const Vec2d kBearSnoutBase[] = { // CCW from the nose end")
|
||||
for x,y in SNOUT_BASE: L.append(f" {{{fmt(x)}, {fmt(y)}}},")
|
||||
L.append("};")
|
||||
L.append("static const Vec3d kBearCrest[] = { // nose (tall) -> tail (short)")
|
||||
for x,y,z in CREST: L.append(f" {{{fmt(x)}, {fmt(y)}, {fmt(z)}}},")
|
||||
L.append("};")
|
||||
open(os.path.join(HERE,"bear_glyph_table.h"),"w").write("\n".join(L)+"\n")
|
||||
print("\n".join(L))
|
||||
@@ -1,65 +0,0 @@
|
||||
# Pull the bear's true silhouette and feature positions out of the supplied male B-rep, so the
|
||||
# simplification study starts from measured geometry instead of a tracing of the flat drawing.
|
||||
#
|
||||
# The part's native frame (make_female.py): flat back on Y=0, relief rising to Y=+17.27, the FACE
|
||||
# carried by X and Z. So the face plane is XZ and the silhouette is the outline projected along Y.
|
||||
import os, json
|
||||
import Part
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
s = Part.Shape(); s.read(os.path.join(HERE, "bear.step"))
|
||||
sol = s.Solids[0]
|
||||
bb = sol.BoundBox
|
||||
print(f"bbox X {bb.XMin:.2f}..{bb.XMax:.2f} Y {bb.YMin:.2f}..{bb.YMax:.2f} Z {bb.ZMin:.2f}..{bb.ZMax:.2f}")
|
||||
|
||||
# The back plate face: the planar face whose normal is -Y and which sits at Y=YMin. Its outer wire
|
||||
# IS the silhouette; its inner wires are the eye holes.
|
||||
best = None
|
||||
for f in sol.Faces:
|
||||
if f.Surface.__class__.__name__ != "Plane":
|
||||
continue
|
||||
n = f.Surface.Axis
|
||||
if abs(abs(n.y) - 1.0) > 1e-6:
|
||||
continue
|
||||
c = f.CenterOfMass
|
||||
if best is None or c.y < best[0]:
|
||||
best = (c.y, f)
|
||||
y, face = best
|
||||
print(f"back plate at Y={y:.3f} wires={len(face.Wires)} area={face.Area:.1f} mm2")
|
||||
|
||||
def wire_pts(w, tol=0.05):
|
||||
# ORDER MATTERS and w.Edges does not carry it: OCC hands the edges back in whatever order the
|
||||
# face stored them, so concatenating their discretisations gives a scrambled ring. The first
|
||||
# version of this script did exactly that and emitted an outline with 7 duplicated points and
|
||||
# twice the perimeter it should have. OrderedEdges walks the wire, and each edge is reversed
|
||||
# when its own orientation runs against the walk.
|
||||
pts = []
|
||||
for e in w.OrderedEdges:
|
||||
d = e.discretize(Deflection=tol)
|
||||
if e.Orientation == "Reversed":
|
||||
d = list(reversed(d))
|
||||
for p in d:
|
||||
pts.append((round(p.x, 3), round(p.z, 3)))
|
||||
# drop consecutive duplicates
|
||||
out = [pts[0]]
|
||||
for p in pts[1:]:
|
||||
if abs(p[0]-out[-1][0]) > 1e-4 or abs(p[1]-out[-1][1]) > 1e-4:
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
data = {"outer": None, "holes": []}
|
||||
outer = face.OuterWire
|
||||
data["outer"] = wire_pts(outer)
|
||||
for w in face.Wires:
|
||||
if w.isSame(outer):
|
||||
continue
|
||||
pts = wire_pts(w)
|
||||
xs = [p[0] for p in pts]; zs = [p[1] for p in pts]
|
||||
data["holes"].append({"pts": pts,
|
||||
"cx": round(sum(xs)/len(xs), 3), "cz": round(sum(zs)/len(zs), 3),
|
||||
"d": round(max(xs)-min(xs), 3)})
|
||||
print(f" hole: centre ({data['holes'][-1]['cx']}, {data['holes'][-1]['cz']}) dia {data['holes'][-1]['d']}")
|
||||
|
||||
print(f"outer wire: {len(data['outer'])} points")
|
||||
json.dump(data, open(os.path.join(HERE, "bear_outline.json"), "w"))
|
||||
print("WROTE bear_outline.json")
|
||||
@@ -1,140 +0,0 @@
|
||||
// Faceted ridge key — asymmetric male/female alignment feature, flat facets only.
|
||||
//
|
||||
// 6 vertices, 7 faces, one closed manifold. Euler check: V - E + F = 6 - 11 + 7 = 2.
|
||||
// No spheres, no cylinders, no splines, no fillets.
|
||||
//
|
||||
// THE FLANKS ARE TRIANGULATED EXPLICITLY, and that is not cosmetic. Written as quads
|
||||
// [0,3,5,4] and [1,4,5,2] they are NOT planar — the base edge and the ridge edge are
|
||||
// skew, so the four corners do not share a plane. A checker caught this after the first
|
||||
// draft claimed the opposite. Left as quads, the tessellator picks the fold direction for
|
||||
// you, which means the "flat facet" promise is broken by an unspecified crease and two
|
||||
// exporters can disagree about the shape. Splitting them here fixes the crease at
|
||||
// back-bottom -> front-ridge, which keeps the rear peak's triangle large and clean.
|
||||
//
|
||||
// FRAME CONVENTION (matches the CAD mate connector it is derived from):
|
||||
// +Z the mating axis — the feature protrudes along it
|
||||
// +X the roll reference — the ridge runs along it, low end forward
|
||||
// +Y completes the right-handed frame
|
||||
//
|
||||
// WHAT BREAKS WHICH SYMMETRY
|
||||
// rotational about Z ....... the ridge (elongation along X)
|
||||
// 180 deg about Z .......... the ridge SLOPE: tall steep back, long shallow front
|
||||
// mirror across XZ ......... deliberately NOT broken. Handedness is fixed by convention,
|
||||
// so +Y is implied once Z and X are known. Breaking it would
|
||||
// add a facet and buy nothing.
|
||||
//
|
||||
// KNOWN AMBIGUITY, stated rather than hidden: viewed exactly ALONG the ridge (+/-X,
|
||||
// orthographic), the silhouette is the same isoceles triangle from front and back. Front
|
||||
// and back are then distinguished by SHADING only — the long shallow front face catches
|
||||
// light differently from the steep back face. If the target renderer is flat-shaded with a
|
||||
// single headlight, verify this case before committing to the shape.
|
||||
|
||||
// ---------------------------------------------------------------- parameters
|
||||
L = 12.0; // overall length along the ridge (X)
|
||||
W = 4.0; // half-width at the BACK
|
||||
tf = 0.45; // front taper: front half-width = W * tf
|
||||
H = 4.5; // peak height at the rear <-- the single dimension controlling asymmetry
|
||||
pr = 0.22; // rear ridge position, fraction of L from the back
|
||||
pf = 0.62; // front ridge position, fraction of L from the back
|
||||
hf = 0.35; // front ridge height, fraction of H
|
||||
|
||||
// Clearance is a PHYSICAL quantity and only means anything if this is a printed part.
|
||||
// See the note at the bottom: for a viewport glyph it is meaningless.
|
||||
clr = 0.20; // per-face clearance, mm
|
||||
depth = 0.40; // extra pocket depth so the male never bottoms out before it seats
|
||||
|
||||
Wf = W * tf;
|
||||
xr0 = -L/2 + L * pr;
|
||||
xr1 = -L/2 + L * pf;
|
||||
Hf = H * hf;
|
||||
|
||||
// ---------------------------------------------------------------- geometry
|
||||
// Vertex order is fixed and referenced by the face table; do not reorder.
|
||||
// 0 back-left 1 back-right 2 front-right 3 front-left
|
||||
// 4 REAR PEAK (tall) 5 front ridge (low)
|
||||
function ridge_pts(l, w, wf, h, hfr, x0, x1) = [
|
||||
[-l/2, -w, 0 ], // 0
|
||||
[-l/2, w, 0 ], // 1
|
||||
[ l/2, wf, 0 ], // 2
|
||||
[ l/2, -wf, 0 ], // 3
|
||||
[ x0, 0, h ], // 4 rear peak
|
||||
[ x1, 0, hfr] // 5 front ridge, low
|
||||
];
|
||||
|
||||
// OpenSCAD wants each face wound CLOCKWISE seen from OUTSIDE. The right-hand-rule
|
||||
// outward-normal (CCW) form is given in the comment for anyone porting to STL/OCC,
|
||||
// where the opposite convention is the usual one.
|
||||
RIDGE_FACES = [
|
||||
[3, 2, 1, 0], // base (CCW-outward: [0,1,2,3]) planar, all z=0
|
||||
[1, 4, 0], // back (CCW-outward: [0,4,1]) steep
|
||||
[5, 3, 0], // flank -Y a (CCW-outward: [0,3,5])
|
||||
[4, 5, 0], // flank -Y b (CCW-outward: [0,5,4])
|
||||
[5, 4, 1], // flank +Y a (CCW-outward: [1,4,5])
|
||||
[2, 5, 1], // flank +Y b (CCW-outward: [1,5,2])
|
||||
[5, 2, 3] // front (CCW-outward: [3,2,5]) long, shallow
|
||||
];
|
||||
|
||||
module ridge_key(l = L, w = W, wf = Wf, h = H, hfr = Hf, x0 = xr0, x1 = xr1) {
|
||||
polyhedron(points = ridge_pts(l, w, wf, h, hfr, x0, x1),
|
||||
faces = RIDGE_FACES,
|
||||
convexity = 3);
|
||||
}
|
||||
|
||||
// MALE: the protrusion, nominal size.
|
||||
module ridge_key_male() { ridge_key(); }
|
||||
|
||||
// FEMALE: the pocket. Grown by `clr` on every side and sunk `depth` deeper.
|
||||
//
|
||||
// HONEST LIMITATION: this grows the key by scaling its defining dimensions, which is NOT a
|
||||
// true uniform surface offset — on the shallow front face the normal clearance comes out
|
||||
// smaller than `clr`, because that face is far from perpendicular to every axis it is
|
||||
// scaled along. A true offset needs minkowski() with a small cube, which is exact and slow,
|
||||
// or an explicit per-face plane push, which is exact and fiddly. For a keying feature whose
|
||||
// job is angular registration rather than a press fit, the approximation is the right trade
|
||||
// — but do not quote this pocket as holding 0.2 mm everywhere, because it does not.
|
||||
module ridge_key_female() {
|
||||
translate([0, 0, -depth])
|
||||
ridge_key(l = L + 2*clr,
|
||||
w = W + clr,
|
||||
wf = Wf + clr,
|
||||
h = H + clr + depth,
|
||||
hfr = Hf + clr + depth,
|
||||
x0 = xr0,
|
||||
x1 = xr1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- demo
|
||||
// Left: the male key on its plate. Right: the plate with the pocket cut.
|
||||
PLATE = [30, 18, 3];
|
||||
|
||||
module plate_with_male() {
|
||||
translate([-PLATE[0]/2, -PLATE[1]/2, -PLATE[2]]) cube(PLATE);
|
||||
ridge_key_male();
|
||||
}
|
||||
|
||||
module plate_with_female() {
|
||||
difference() {
|
||||
translate([-PLATE[0]/2, -PLATE[1]/2, -PLATE[2]]) cube(PLATE);
|
||||
ridge_key_female();
|
||||
}
|
||||
}
|
||||
|
||||
translate([-20, 0, 0]) plate_with_male();
|
||||
translate([ 20, 0, 0]) plate_with_female();
|
||||
|
||||
// ---------------------------------------------------------------- note on the two readings
|
||||
// This file is written for the PHYSICAL reading: a printable alignment key, where `clr` and
|
||||
// `depth` are real millimetres and flat facets genuinely help — they slice without the
|
||||
// stair-stepping a tessellated curve produces, and they print without support on the
|
||||
// shallow front face.
|
||||
//
|
||||
// If the intent is instead the VIEWPORT GLYPH for a CAD mate connector, then:
|
||||
// - `clr` and `depth` are meaningless: a symbol does not mate with anything;
|
||||
// - all dimensions must become SCREEN PIXELS scaled by upp = 1/zoom, because every gizmo
|
||||
// in that viewport is screen-constant and must not shrink with the model;
|
||||
// - "low-poly for rendering performance" is not a real reason at ~2-20 glyphs per frame.
|
||||
// The real reason to keep flat facets there is LEGIBILITY: hard normals give distinct
|
||||
// value steps between facets, and that is what lets a 22-px solid read as an oriented
|
||||
// object instead of a grey blob.
|
||||
// The vertex logic above is identical under both readings. Only the units and the clearance
|
||||
// change.
|
||||
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB |
@@ -1,226 +0,0 @@
|
||||
solid OpenSCAD_Model
|
||||
facet normal 1 -0 0
|
||||
outer loop
|
||||
vertex 15 -9 0
|
||||
vertex 15 9 -8
|
||||
vertex 15 9 0
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 1 0 0
|
||||
outer loop
|
||||
vertex 15 9 -8
|
||||
vertex 15 -9 0
|
||||
vertex 15 -9 -8
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0 0 1
|
||||
outer loop
|
||||
vertex 15 9 0
|
||||
vertex 5.3246 1.63218 0
|
||||
vertex 15 -9 0
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0 0 1
|
||||
outer loop
|
||||
vertex 15 9 0
|
||||
vertex -4.79494 3.42759 0
|
||||
vertex 5.3246 1.63218 0
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0 0 1
|
||||
outer loop
|
||||
vertex 15 9 0
|
||||
vertex -5.97725 3.87059 0
|
||||
vertex -4.79494 3.42759 0
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0 0 1
|
||||
outer loop
|
||||
vertex -5.97725 3.87059 0
|
||||
vertex -15 9 0
|
||||
vertex -5.97725 -3.87059 0
|
||||
endloop
|
||||
endfacet
|
||||
facet normal -0 0 1
|
||||
outer loop
|
||||
vertex -15 9 0
|
||||
vertex -5.97725 3.87059 0
|
||||
vertex 15 9 0
|
||||
endloop
|
||||
endfacet
|
||||
facet normal -0 0 1
|
||||
outer loop
|
||||
vertex 5.3246 -1.63218 0
|
||||
vertex 15 -9 0
|
||||
vertex 5.3246 1.63218 0
|
||||
endloop
|
||||
endfacet
|
||||
facet normal -0 0 1
|
||||
outer loop
|
||||
vertex -4.79494 -3.42759 0
|
||||
vertex 15 -9 0
|
||||
vertex 5.3246 -1.63218 0
|
||||
endloop
|
||||
endfacet
|
||||
facet normal -0 0 1
|
||||
outer loop
|
||||
vertex -5.97725 -3.87059 0
|
||||
vertex 15 -9 0
|
||||
vertex -4.79494 -3.42759 0
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0 0 1
|
||||
outer loop
|
||||
vertex -5.97725 -3.87059 0
|
||||
vertex -15 -9 0
|
||||
vertex 15 -9 0
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0 0 1
|
||||
outer loop
|
||||
vertex -15 -9 0
|
||||
vertex -5.97725 -3.87059 0
|
||||
vertex -15 9 0
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0 0 -1
|
||||
outer loop
|
||||
vertex -15 -9 -8
|
||||
vertex 15 9 -8
|
||||
vertex 15 -9 -8
|
||||
endloop
|
||||
endfacet
|
||||
facet normal -0 0 -1
|
||||
outer loop
|
||||
vertex 15 9 -8
|
||||
vertex -15 -9 -8
|
||||
vertex -15 9 -8
|
||||
endloop
|
||||
endfacet
|
||||
facet normal -1 0 0
|
||||
outer loop
|
||||
vertex -15 -9 -8
|
||||
vertex -15 9 0
|
||||
vertex -15 9 -8
|
||||
endloop
|
||||
endfacet
|
||||
facet normal -1 -0 0
|
||||
outer loop
|
||||
vertex -15 9 0
|
||||
vertex -15 -9 -8
|
||||
vertex -15 -9 0
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0 1 -0
|
||||
outer loop
|
||||
vertex 15 9 -8
|
||||
vertex -15 9 0
|
||||
vertex 15 9 0
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0 1 0
|
||||
outer loop
|
||||
vertex -15 9 0
|
||||
vertex 15 9 -8
|
||||
vertex -15 9 -8
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0 -1 0
|
||||
outer loop
|
||||
vertex -15 -9 -8
|
||||
vertex 15 -9 0
|
||||
vertex -15 -9 0
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0 -1 -0
|
||||
outer loop
|
||||
vertex 15 -9 0
|
||||
vertex -15 -9 -8
|
||||
vertex 15 -9 -8
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0 0 1
|
||||
outer loop
|
||||
vertex -6.2 4.2 -0.4
|
||||
vertex 6.2 -2 -0.4
|
||||
vertex 6.2 2 -0.4
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0 0 1
|
||||
outer loop
|
||||
vertex 6.2 -2 -0.4
|
||||
vertex -6.2 4.2 -0.4
|
||||
vertex -6.2 -4.2 -0.4
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0.873667 0 -0.486524
|
||||
outer loop
|
||||
vertex -5.97725 -3.87059 0
|
||||
vertex -6.2 4.2 -0.4
|
||||
vertex -5.97725 3.87059 0
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0.873667 0 -0.486524
|
||||
outer loop
|
||||
vertex -6.2 4.2 -0.4
|
||||
vertex -5.97725 -3.87059 0
|
||||
vertex -6.2 -4.2 -0.4
|
||||
endloop
|
||||
endfacet
|
||||
facet normal -0.107146 0.603912 -0.789816
|
||||
outer loop
|
||||
vertex 6.2 -2 -0.4
|
||||
vertex -4.79494 -3.42759 0
|
||||
vertex 5.3246 -1.63218 0
|
||||
endloop
|
||||
endfacet
|
||||
facet normal -0.107147 0.603918 -0.789812
|
||||
outer loop
|
||||
vertex -4.79494 -3.42759 0
|
||||
vertex 6.2 -2 -0.4
|
||||
vertex -6.2 -4.2 -0.4
|
||||
endloop
|
||||
endfacet
|
||||
facet normal -0.304068 0.811519 -0.498978
|
||||
outer loop
|
||||
vertex -4.79494 -3.42759 0
|
||||
vertex -6.2 -4.2 -0.4
|
||||
vertex -5.97725 -3.87059 0
|
||||
endloop
|
||||
endfacet
|
||||
facet normal -0.304068 -0.811519 -0.498978
|
||||
outer loop
|
||||
vertex -5.97725 3.87059 0
|
||||
vertex -6.2 4.2 -0.4
|
||||
vertex -4.79494 3.42759 0
|
||||
endloop
|
||||
endfacet
|
||||
facet normal -0.107146 -0.603912 -0.789816
|
||||
outer loop
|
||||
vertex -4.79494 3.42759 0
|
||||
vertex 6.2 2 -0.4
|
||||
vertex 5.3246 1.63218 0
|
||||
endloop
|
||||
endfacet
|
||||
facet normal -0.107147 -0.603918 -0.789812
|
||||
outer loop
|
||||
vertex 6.2 2 -0.4
|
||||
vertex -4.79494 3.42759 0
|
||||
vertex -6.2 4.2 -0.4
|
||||
endloop
|
||||
endfacet
|
||||
facet normal -0.415603 0 -0.909546
|
||||
outer loop
|
||||
vertex 5.3246 -1.63218 0
|
||||
vertex 6.2 2 -0.4
|
||||
vertex 6.2 -2 -0.4
|
||||
endloop
|
||||
endfacet
|
||||
facet normal -0.415603 0 -0.909546
|
||||
outer loop
|
||||
vertex 6.2 2 -0.4
|
||||
vertex 5.3246 -1.63218 0
|
||||
vertex 5.3246 1.63218 0
|
||||
endloop
|
||||
endfacet
|
||||
endsolid OpenSCAD_Model
|
||||