Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
caf74b1e39 | ||
|
|
a4c55c9976 | ||
|
|
ee1b845746 | ||
|
|
61b865706f | ||
|
|
55cc95d122 | ||
|
|
277ff35325 |
@@ -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,188 +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, and moving settings that sibling presets repeat onto shared bases after fix-variant or while drafting. 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
|
||||
|
||||
This skill describes how OrcaSlicer system profiles are drafted and shaped: the rules, equations and
|
||||
patterns a profile follows. Use it to draft new profiles, modify existing ones, fix profile issues and
|
||||
review profile changes.
|
||||
|
||||
A bundle is the index `resources/profiles/<Vendor>.json` plus the folder `<Vendor>/`. The vendor id is
|
||||
the filename stem (`BBL`), not the index's display `name` (`Bambulab`). The index is the loader's only
|
||||
entry point: an unindexed preset never loads. `OrcaFilamentLibrary` is the shared filament bundle,
|
||||
loaded first; `blacklist.json` is data, not a bundle.
|
||||
|
||||
## References
|
||||
|
||||
Read the reference for the task before editing; load others only when the task crosses into them.
|
||||
Paths below are relative to this skill. Commands run from the repository root; on Windows use `py -3`
|
||||
for `python3`.
|
||||
|
||||
| Task | Read |
|
||||
| --- | --- |
|
||||
| Add or tune a filament, brand or material; fix compatibility, alias shadowing or overlapping coverage | [filament-profiles.md](references/filament-profiles.md) |
|
||||
| Add a printer or nozzle; change models, variants, assets or per-extruder vectors | [machine-profiles.md](references/machine-profiles.md) |
|
||||
| Add or tune extruder variants (`extruder_type` Direct Drive / Bowden × nozzle volume type Standard / High Flow / TPU High Flow / E3D High Flow / Extra High Flow variants) on a printer, process or filament | [extruder-variants.md](references/extruder-variants.md) |
|
||||
| Add a quality tier or tune a process | [process-profiles.md](references/process-profiles.md) |
|
||||
| Draft several presets, or clean up after `fix-variant`: which base each shared value belongs on, when a new base pays off, proving nothing loads differently | [shared-bases.md](references/shared-bases.md) |
|
||||
| Name a preset; check what a name must equal; base names, uniqueness, filenames | [naming.md](references/naming.md) |
|
||||
| Create a vendor bundle; index, `version`, `inherits`, `include`; migrate preset names; diagnose why a bundle fails to load | [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 |
|
||||
| Run checks, interpret failures, test another tree or verify in the app | [validation.md](references/validation.md) |
|
||||
| Review a profile diff | [review-checklist.md](references/review-checklist.md) |
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Bump the `version` of every bundle you change**, `OrcaFilamentLibrary.json` included when affected.
|
||||
Increment the last component and carry `.99` into the third (`02.04.00.99` → `02.04.01.00`). The
|
||||
updater installs only a strictly newer version, and CI does not check the bump.
|
||||
2. **Register every preset, bases included, parents before children.** `update-index` writes the four
|
||||
`*_list` arrays from the files on disk; `check` fails unless the index equals its output. Each index
|
||||
entry's `name` must equal the file's `name`.
|
||||
3. **Generate ids; never invent or copy them.** Keep existing ids during ordinary tuning. New presets
|
||||
normally omit them until `generate-id`; bases carry no `setting_id`. BBL's own `setting_id`s and a wrongly
|
||||
inherited `filament_id` need the explicit handling in [ids.md](references/ids.md).
|
||||
4. **A name is an identity; preserve shipped selectable names.** Every reference (`inherits`,
|
||||
`compatible_printers`, `default_*`, `printer_model`) is the exact, case-sensitive `name`. Renaming or
|
||||
deleting a shipped selectable preset, or flipping its `instantiation` from `"true"` to `"false"`,
|
||||
needs `renamed_from` (a `;`-separated string) on a selectable successor
|
||||
([migration rules](references/vendor-bundle.md#renamed_from)); update in-tree references too.
|
||||
5. **Values are strings or arrays of strings.** `"instantiation": "false"`, never `false`. A
|
||||
`machine_model`'s `nozzle_diameter` is a `;`-separated string; a `machine`'s is an array. Custom
|
||||
G-code is one string. Wrong types can abort loading of the bundle or of every vendor
|
||||
([failure scopes](references/vendor-bundle.md#failure-scopes)).
|
||||
6. **Unknown keys are dropped silently.** Confirm every new key exists in
|
||||
`src/libslic3r/PrintConfig.cpp`; a key a neighbouring file writes is no evidence it exists. `check` rejects,
|
||||
and `normalize` removes, known obsolete keys, but neither detects an arbitrary misspelling. A key
|
||||
missing from the definitions may be a legacy name the loader still renames
|
||||
(`tool_change_gcode` → `change_filament_gcode`) or whose value it rewrites (`DirectDrive` →
|
||||
`Direct Drive`); check `PrintConfigDef::handle_legacy` before removing one, and write the current name
|
||||
in new edits.
|
||||
7. **Write overrides only.** Inherit the bundle's bases and restate just what differs; follow the
|
||||
bundle's existing layering and style, except that a new filament prefers the library's bases. A
|
||||
value that every preset of a group shares goes on the group's base
|
||||
([shared bases](references/shared-bases.md)).
|
||||
8. **One load error can discard a whole vendor bundle**: an unresolved `inherits`, a missing indexed
|
||||
file, two selectable presets with one name, an unknown `printer_model` or `printer_variant`, a
|
||||
filament with no resolvable `filament_id`, `nil` in a non-nullable key. `inherits` and `include`
|
||||
resolve only inside the bundle, except that filaments may inherit from OrcaFilamentLibrary.
|
||||
9. **Filament compatibility names exact printer variants.** Every instantiated filament outside the
|
||||
library writes a non-empty `compatible_printers` in its own file. Library fallbacks may omit it;
|
||||
library printer-specific tunes use a non-empty list. One variant may be claimed by only one preset
|
||||
per filament product (`filament_id`); an overlap is resolved by moving the variant to the most
|
||||
specific preset, which is preferred over deleting a preset
|
||||
([one variant, one profile](references/filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product)).
|
||||
10. **One all-printer preset per product; colour is a runtime property, never a preset.** Never ship
|
||||
presets that differ only by colour. CI accepts them, so this is a review call
|
||||
([colour](references/filament-profiles.md#colour-is-a-runtime-property)).
|
||||
11. **Write a variant key at full width or not at all.** A key in the four variant sets holds exactly
|
||||
`N` values in the selectable preset that writes it, `N = S × k` in the
|
||||
[sizing equation](references/extruder-variants.md#sizing-equation): one per variant, a (normal,
|
||||
silent) pair per variant for the `machine_max_*` limits; one value is not "the same for every
|
||||
variant". Profiles must be correct as written: `check` judges each selectable preset by the
|
||||
equation, never by what the loader pads or cuts, and `check --strict` also holds what a preset
|
||||
inherits to its own width, as BBL writes it. A base is never judged on its own: its array widths
|
||||
count, under `--strict`, where they reach a preset, while the id and layout rules judge the
|
||||
composed preset, inherited values included, without it. Declare the variant layout on a
|
||||
multi-extruder printer whose extruders need different values
|
||||
([widths](references/extruder-variants.md#widths)).
|
||||
12. **Run the full checks before reporting completion.** A `--vendor` run is only a development loop.
|
||||
Review also covers version bumps, assets, non-default processes and hardware tuning, which CI cannot
|
||||
establish.
|
||||
|
||||
## Names
|
||||
|
||||
| Type | Shape | What the loader uses |
|
||||
| --- | --- | --- |
|
||||
| `machine_model` | `<Model>` (`Bambu Lab X1 Carbon`) | the exact string, named by each variant's `printer_model`; also the `<Model>_cover.png` stem |
|
||||
| `machine` | `<Model> <nozzle> nozzle` | the exact string, named by `compatible_printers`; `printer_variant` holds the nozzle token (`0.4`, [rules](references/machine-profiles.md#printer_model-and-printer_variant)) |
|
||||
| `process` | `<lh>mm <Quality> @<target>` | the exact string when referenced or selected; `@<target>` is a label, compatibility comes from the preset's list or condition |
|
||||
| `filament` | `<Product> @<target>` | text before the first `@` is the alias (shadowing, `filament_id`); the rest is a label, with reserved targets `@base` and `@System` |
|
||||
|
||||
The shapes are convention; `check` enforces only uniqueness. Per-type conventions, base names and
|
||||
filename rules are in [naming.md](references/naming.md).
|
||||
|
||||
## Creating or modifying a profile
|
||||
|
||||
1. **Inspect the diff and the neighbouring presets.** Read their `name`, parent chain and children:
|
||||
edits to a base, or to a leaf that others inherit, 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](references/naming.md#filenames-and-paths).
|
||||
2. **Author explicit metadata.** Set `type` yourself (`machine` vs `machine_model` especially), and use
|
||||
`"from": "system"` and a 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. **Put shared values on shared bases** when drafting several presets, and after `fix-variant`, which
|
||||
widens an array in every preset that writes it and moves nothing. Each value goes on the base of the
|
||||
level that determines it, a new base only where it pays for itself, and a restructure must leave
|
||||
every selectable preset loading what it loaded: `snapshot` before the edit, `compare` after it
|
||||
([shared-bases.md](references/shared-bases.md)).
|
||||
4. **Bump the version**, then run the authoring commands in order for each affected bundle, reading
|
||||
every diff and resolving every error before moving on:
|
||||
|
||||
```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 accept `--dry-run`. `normalize` changes content and can reformat entire files;
|
||||
rerun `update-index` after any change to `inherits` or `include`, since it orders by them.
|
||||
**Do not use `trim` in this workflow:** it deletes unindexed files, including one you just added. Do
|
||||
not use `normalize --force` for routine edits. An error in a bundle you did not touch predates your
|
||||
change: confirm it on a clean checkout and report it rather than fixing it in the same change.
|
||||
5. **Validate:**
|
||||
|
||||
```bash
|
||||
./scripts/check_profile.sh --vendor "<Vendor>" # development loop
|
||||
./scripts/check_profile.sh # full tree before the PR
|
||||
```
|
||||
|
||||
On Windows use `scripts\check_profile.bat -Vendor "<Vendor>"` / `scripts\check_profile.bat`. Logs
|
||||
land in a per-user cache dir ([validation.md](references/validation.md)). Id checks stay tree-wide
|
||||
under `--vendor`, and filament-only bundles skip the default slice check. Under `--vendor` read only
|
||||
`profile_tool` and `validate_slice`: the other three checks fail on library presets that name other
|
||||
vendors' printers ([why](references/validation.md#the-five-checks)).
|
||||
6. **Verify the changed behaviour.** Slice newly added non-default processes and filaments
|
||||
[explicitly](references/validation.md#checking-a-copy-of-the-tree), and
|
||||
[test in the app](references/validation.md#testing-in-the-app) for selection or UI behaviour. Report
|
||||
the checks actually run, their failures and skips, and any hardware tuning still unverified.
|
||||
|
||||
## Symptom → first look
|
||||
|
||||
| Symptom | Start here |
|
||||
| --- | --- |
|
||||
| A vendor disappears | the app's log or the `validate_system` log; [failure scopes](references/vendor-bundle.md#failure-scopes) |
|
||||
| A setting has no effect | key spelling or a legacy name (`PrintConfigDef::handle_legacy`), value type, or a config key placed on a `machine_model` |
|
||||
| A preset exists but is not selectable | index registration, `instantiation`, whether it is installed (chosen in the setup wizard, or listed in the model's `default_materials`), 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) |
|
||||
| Presets differ only by colour, or an all-printer library preset lacks `@System` | [colour is a runtime property](references/filament-profiles.md#colour-is-a-runtime-property) |
|
||||
| High Flow (or a second extruder) slices with Standard (or extruder 1) values; a variant switch is missing; a variant's tuned values never arrive | [extruder variants](references/extruder-variants.md#variant-strings), [widths](references/extruder-variants.md#widths), [variant names](references/validation.md#variant-names) |
|
||||
| Values land on the wrong extruder or mode after a variant was added | [inserting a variant](references/extruder-variants.md#adding-a-variant-inserts-its-values-at-its-variant-index), [padding and composition](references/extruder-variants.md#padding-truncation-and-composition) |
|
||||
| A resolved value matches neither the file nor its `inherits` parent; an array is not the width the file wrote, or a child got only a base's first value | [composition order and `include`](references/vendor-bundle.md#inherits-and-include) |
|
||||
| A bed temperature is ignored | [the twelve plate 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 this skill and the checkout disagree, the checkout wins: `scripts/orca_profile_tool.py` for the
|
||||
tool and its flags; `src/libslic3r/Preset*.cpp` for loading and compatibility;
|
||||
`src/libslic3r/PrintConfig.cpp` for keys, types, nullable options, the variant key sets and legacy
|
||||
handling; `src/dev-utils/OrcaSlicer_profile_validator.cpp` and `.github/workflows/check_profiles.yml`
|
||||
for validation coverage; `docs/HLSD/filament_id.md` for filament identity. The wiki's
|
||||
[profile 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.
|
||||
|
||||
## Editing this skill
|
||||
|
||||
The skill describes what a profile must be, not what the shipped tree currently is. State rules,
|
||||
equations, patterns and profile shapes; never inventories of shipped defects, counts, dated
|
||||
measurements or lists of which vendors do what. Test each sentence: if editing the profiles alone,
|
||||
with the engine and tool unchanged, could make it false, state the rule behind it or give a generic
|
||||
example instead. Example files named as models to copy, and commit hashes cited as the reason for a
|
||||
rule, are fine.
|
||||
@@ -1,547 +0,0 @@
|
||||
# Extruder variants
|
||||
|
||||
Use this when a printer's hotend or extruder can be in more than one hardware configuration that
|
||||
needs different settings: a Standard and a High Flow nozzle, a TPU High Flow, E3D High Flow or Extra High Flow nozzle, or
|
||||
a Direct Drive and a Bowden extruder on one machine. Variants let one printer, process and filament preset carry a
|
||||
separate value per configuration; the user picks the configuration in the sidebar, and slicing uses
|
||||
the matching values. They are not for nozzle **diameter**: that stays one `machine` preset per
|
||||
`printer_variant`.
|
||||
|
||||
## Variant strings
|
||||
|
||||
- A **variant string** is `"<extruder_type> <nozzle volume type>"`: the extruder's `extruder_type`
|
||||
value, a space, and its nozzle volume type (the project's `nozzle_volume_type`, seeded by the
|
||||
printer preset's `default_nozzle_volume_type`), e.g. `"Direct Drive High Flow"`.
|
||||
- A **variant** is one entry of a preset's variant list, named by its variant string (plus an extruder
|
||||
id for printer and process lists). A variant-aware key holds **one value per variant** (a
|
||||
(normal, silent) pair for the `machine_max_*` limits), and each preset declares its variants in
|
||||
that list. Slicing picks, for each extruder, the variant whose
|
||||
variant string equals the current `extruder_type` + nozzle volume type.
|
||||
- Matching is an **exact string compare** of the whole string (plus the extruder id for printer and
|
||||
process lists) against the preset's own resolved list, in any order. With no match the **first
|
||||
variant** is used, silently: variant index 0 for printer and process keys (extruder 1's first
|
||||
variant, whichever extruder asks), the filament's own first variant for filament keys. Printer and
|
||||
process keys are matched only on a printer with several extruders or whose
|
||||
`extruder_variant_list` offers several variant strings (`support_different_extruders`); on any
|
||||
other printer their variant index 0 is read whatever it names. Nothing rejects a profile for a
|
||||
variant mismatch; mistakes surface only as wrong values in the G-code.
|
||||
- Variant index and array length follow [Widths](#widths).
|
||||
|
||||
The complete enum is `s_keys_map_ExtruderType` and `s_keys_map_NozzleVolumeType` in
|
||||
`src/libslic3r/PrintConfig.cpp` (`grep -A5 s_keys_map_NozzleVolumeType` there to confirm):
|
||||
|
||||
| Part | Values |
|
||||
| --- | --- |
|
||||
| Extruder type | `Direct Drive`, `Bowden` |
|
||||
| Nozzle volume type | `Standard`, `High Flow`, `TPU High Flow`, `E3D High Flow`, `Extra High Flow` (`Hybrid` exists but is runtime-only) |
|
||||
|
||||
So the ten legal variant strings are the two extruder types × the five writable nozzle volume types,
|
||||
and this table is the whole test of legality: a string's presence in a shipped profile is no evidence
|
||||
for it. `Hybrid` (an extruder with several sub-nozzles) is never a variant: a filament on it reads the
|
||||
variant of its own nozzle volume type from the project's `filament_volume_map`, printer and process
|
||||
keys get one variant per nozzle volume type the extruder holds (`extruder_nozzle_stats`), and any
|
||||
other lookup reads `Standard`. Never write it in a variant string. The same per-filament and
|
||||
per-type reading applies on any extruder once `extruder_nozzle_stats` lists more nozzle volume
|
||||
types than there are extruders.
|
||||
|
||||
Every other string (a nozzle volume type name from another slicer, a typo, a variant copied from a
|
||||
shipped profile) is a **dead variant**: nothing selects it, and since lookup is by string it does not
|
||||
shift the variants beside it; it still counts toward the variant length when arrays are sized.
|
||||
`orca_profile_tool.py check` reports it as an error in every bundle, BBL included
|
||||
([variant names](validation.md#variant-names)). Legacy names are errors too: in the four variant
|
||||
lists, `default_nozzle_volume_type` and `nozzle_volume_type`, the loader still rewrites `Normal` →
|
||||
`Standard` and `Big Traffic` → `High Flow`, so a ported `Direct Drive Normal` variant would load, but
|
||||
`check` rejects the spelling and names the enum name to write. `DirectDrive` is rewritten only in
|
||||
`extruder_type`, so `DirectDrive Standard` in a variant list is dead. Write the enum names;
|
||||
`normalize` does not convert them.
|
||||
|
||||
### A nozzle the enum does not name
|
||||
|
||||
The nozzle volume types are fixed by the engine, and a profile cannot add one. A nozzle the table does
|
||||
not name needs the type added in code first, which is outside profile work; until then any string
|
||||
for it is a dead variant. Once the engine has the type, its variant string is the enum name after
|
||||
the extruder type, e.g. `"Direct Drive <name>"`, and `check` accepts it with no tool change, since it
|
||||
reads the enum from `PrintConfig.cpp`. Existing arrays are unaffected
|
||||
([slice time](#slice-time-and-existing-users)).
|
||||
|
||||
## When to use variants
|
||||
|
||||
| Hardware | Do |
|
||||
| --- | --- |
|
||||
| One extruder, one nozzle type | Nothing. Without a variant list the printer has one variant ([variant length](#widths)) and no printer-key lookup takes place; a filament with several variants still gets the one for the printer's variant string. |
|
||||
| Bowden-only printer | Nothing either. A list-less printer matches no string, so every printer-key lookup reads variant index 0 whether that variant is called `"Bowden Standard"` or `"Direct Drive Standard"`; naming it is cosmetic while it is the printer's only variant. A multi-extruder Bowden printer that declares the layout writes `"Bowden Standard"` variants: `"Direct Drive Standard"` entries match none of its extruders, so every extruder reads variant index 0 (and `check` reports them under [Printer rule 3](#printer-machine)). A filament with a `"Bowden Standard"` variant does get that variant there. |
|
||||
| Nozzle types the user swaps (Standard / High Flow / TPU High Flow / E3D High Flow / Extra High Flow) | The printer lists them as variants; tune the keys that really differ per nozzle volume type. |
|
||||
| Extruders of different types on one machine | One `extruder_type` per extruder, each extruder listing its own variants. |
|
||||
| Several extruders that need different values in a variant key (retraction, z-hop, `nozzle_volume`, the `machine_max_*` limits) | Declare the layout even with a single nozzle volume type: `extruder_variant_list` with one `"<type> Standard"` per extruder, and the flattened pair. Without it the arrays still hold one value per extruder ([variant length](#widths)), but the loader keeps only extruder 1's value of a list-less printer's arrays, so every extruder prints with it. |
|
||||
|
||||
A preset without variant keys keeps working on a variant printer: its single variant is applied to
|
||||
every extruder. So adding variants to a printer does not break existing processes or library
|
||||
filaments; it only makes per-variant tuning possible.
|
||||
|
||||
## Printer (`machine`)
|
||||
|
||||
```json
|
||||
"extruder_type": ["Direct Drive", "Direct Drive"],
|
||||
"extruder_variant_list": ["Direct Drive Standard,Direct Drive High Flow",
|
||||
"Direct Drive Standard,Direct Drive High Flow,Direct Drive TPU High Flow"],
|
||||
"printer_extruder_id": ["1", "1", "2", "2", "2"],
|
||||
"printer_extruder_variant": ["Direct Drive Standard", "Direct Drive High Flow",
|
||||
"Direct Drive Standard", "Direct Drive High Flow", "Direct Drive TPU High Flow"],
|
||||
"default_nozzle_volume_type": ["Standard", "Standard"]
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
1. `extruder_variant_list` has **one entry per extruder**; each entry is the `,`-joined variants that
|
||||
extruder supports. It is the per-extruder menu the sidebar offers. It is in no
|
||||
[variant set](#the-four-key-sets), so the variant-length resize leaves it alone. Extruder 1's
|
||||
first variant, variant index 0 of the flattened pair, is the fallback of every extruder whose
|
||||
variant is missing when the arrays are collapsed for slicing
|
||||
([slice time](#slice-time-and-existing-users)); it is not necessarily the configuration the user
|
||||
sees, which is `default_nozzle_volume_type` (rule 4).
|
||||
2. `printer_extruder_variant` is that list **flattened** extruder-major, one entry per variant, and
|
||||
`printer_extruder_id` gives each entry its 1-based extruder. These two size and address every
|
||||
variant. Write all three keys and keep them in agreement. At load with
|
||||
`single_extruder_multi_material` off, and in the app when the printer tab loads a printer with a
|
||||
different number of extruders, the pair is rebuilt from `extruder_variant_list` (one
|
||||
`Direct Drive Standard` per extruder when the list is absent) and the variant arrays are resized to
|
||||
the rebuilt pair, padded with their first value or cut. The resize skips the `machine_max_*` limits
|
||||
and `hotend_heating_rate` / `hotend_cooling_rate`: they keep their width, and an extruder beyond it
|
||||
reads their first value, so extruder 2 and up of a list-less printer take extruder 1's normal limit
|
||||
as their silent one too. With the three in agreement that changes nothing; a pair written without
|
||||
the list is replaced. A listed variant the pair lacks is a menu choice that reads variant index 0.
|
||||
- The pair without `extruder_variant_list` slices, but the sidebar offers no variant switch and
|
||||
the app cannot add variants to a list-less process: nothing is lost while every extruder
|
||||
has exactly one variant, and every further variant is unreachable.
|
||||
- A missing or one-value `printer_extruder_id` is extruder 1 at every index
|
||||
([the id trap](#padding-truncation-and-composition)) unless the load-time rebuild above
|
||||
replaces the pair (`single_extruder_multi_material` off).
|
||||
- `check` reports against this rule ([variant arrays](validation.md#variant-arrays)). Errors: a
|
||||
pair that is not the flattening, an id array that does not give each entry its extruder, a list
|
||||
of more than one variant without the pair, and a pair without the list that puts several variants
|
||||
on one extruder. A pair without the list and one variant per extruder is a warning where the
|
||||
load-time rebuild would replace it (`single_extruder_multi_material` off, and a pair other than
|
||||
one `Direct Drive Standard` per extruder), and passes otherwise.
|
||||
3. Every variant string in an entry must start with that extruder's `extruder_type`.
|
||||
4. `default_nozzle_volume_type` has one value per extruder and must name a nozzle volume type that
|
||||
extruder's variants list; it seeds the sidebar. The live choice is `nozzle_volume_type` in the
|
||||
project config, never a preset key.
|
||||
5. Size every array by the set its key belongs to (the three sets are listed in full in
|
||||
[The four key sets](#the-four-key-sets); the lengths are worked through in the
|
||||
[sizing equation](#sizing-equation)): exactly the length below in each selectable preset that
|
||||
writes it, or leave the key out and the preset takes what reaches it, the default or its base's
|
||||
array. One value is no
|
||||
shorthand for "the same for every variant"; write the value for every variant:
|
||||
|
||||
| Set | Length | Keys |
|
||||
| --- | --- | --- |
|
||||
| `printer_extruder_options` | extruders (`E`) | the 8 per-extruder keys outside the variant scheme (`extruder_type`, `nozzle_diameter`, `default_nozzle_volume_type`, …) |
|
||||
| `printer_options_with_variant_1` | variant length (`S`) | the full list below; not guessable from names |
|
||||
| `printer_options_with_variant_2` | 2 × variant length | the 16 `machine_max_*` limits, at [stride 2](#widths): a (normal, silent) pair per variant |
|
||||
|
||||
Put the variant layout on the shared base of all printers that share the hardware, and let the
|
||||
nozzle-diameter siblings inherit it, restating only the variant arrays whose values change. `check`
|
||||
does not judge a base on its own; its arrays count where they reach a selectable preset, under
|
||||
`check --strict`. The loader stores a base at its **own** `printer_extruder_variant`, one variant
|
||||
when it writes none whatever its extruder count, and cuts a wider array to its first values before
|
||||
any child inherits it; so a multi-extruder base whose extruders need different values declares the
|
||||
layout itself ([composition](#padding-truncation-and-composition)). When a variant array can move from
|
||||
the presets to a base is in [shared-bases.md](shared-bases.md#variant-arrays-on-a-base).
|
||||
|
||||
## Process
|
||||
|
||||
1. `print_extruder_variant` + `print_extruder_id` list the (extruder id, variant string) pairs of the
|
||||
process's variants. A variant is found by that pair, never by position: what is **required** is
|
||||
that every pair a compatible printer (by list or condition) can select is present, in any order. A
|
||||
missing pair reads the process's variant index 0 for that extruder, an extra pair is a variant
|
||||
nothing selects, and neither is reported. The exception is a single-extruder printer whose
|
||||
`extruder_variant_list` offers one variant string: no pair is matched there and variant index 0
|
||||
is read, so a process shared with such a printer lists that printer's pair first. **Mirroring** the printer's `printer_extruder_variant` +
|
||||
`printer_extruder_id` entry for entry is the convention; follow it, so the arrays compare by eye,
|
||||
but a different order with every pair present is a nit, not a defect. A process shared by printers
|
||||
whose pairs differ falls back to variant index 0 on the pairs it lacks; give each layout its own base.
|
||||
2. Every key in `print_options_with_variant` ([the full list](#the-four-key-sets), which is not
|
||||
"every speed") has exactly one value per variant, or is left out. `print_extruder_id` needs its
|
||||
value per variant too: one value pads to extruder 1 everywhere. `check` holds a
|
||||
`print_extruder_id` that reaches the preset, written or inherited, to one entry per variant on any
|
||||
printer, without `--strict`, and warns when it is absent and a variant repeats.
|
||||
3. Only add process variants if speeds or accelerations really differ per nozzle volume type or
|
||||
extruder. Otherwise omit the variant keys: a one-variant process needs no list, because match and
|
||||
no-match both read variant index 0, and that variant is copied to every extruder at slice time.
|
||||
4. Put the lists on the process base for that printer layout, so leaves stay small.
|
||||
|
||||
## Filament
|
||||
|
||||
```json
|
||||
"filament_extruder_variant": ["Direct Drive Standard", "Direct Drive High Flow"],
|
||||
"filament_max_volumetric_speed": ["21", "29"],
|
||||
"filament_flow_ratio": ["0.98", "0.98"],
|
||||
"filament_retraction_length": ["nil", "0.4"]
|
||||
```
|
||||
|
||||
1. `filament_extruder_variant` lists variants **without extruder ids**: a filament's High Flow variant
|
||||
is used on whichever extruder is in High Flow. Its entries must be distinct, since the lookup
|
||||
returns the first equal string and a repeated entry is a variant nothing selects. There is no
|
||||
filament id key: `filament_extruder_id` exists only as a G-code placeholder, not as a filament
|
||||
option (its option and its set entry are commented out in `PrintConfig.cpp`), so a filament file that writes it loses the key as unknown.
|
||||
`filament_extruder_compatibility` is unrelated to variants (it says which extruders the filament
|
||||
may be loaded into).
|
||||
2. Every key in `filament_options_with_variant` ([the full list](#the-four-key-sets)) that reaches the
|
||||
preset, whether written, included or inherited, is resized to its variant count: write it at exactly
|
||||
that width, or leave it out. Keys outside the set
|
||||
(`filament_type`, plate temperatures, `fan_max_speed`, `slow_down_min_speed`, …) are never addressed by variant index; the preset contributes their first value however wide a
|
||||
file writes them.
|
||||
3. Cover every variant the material is meant to print on across its `compatible_printers`. Leave out
|
||||
a variant deliberately when the material should not be tuned for it (e.g. a TPU High Flow variant
|
||||
only on TPU filaments); an extruder reporting that variant string then reads the first variant.
|
||||
4. Order the variants like the printer's, Standard first, so the first-variant fallback is the
|
||||
conservative one.
|
||||
5. Tune what really differs: `filament_max_volumetric_speed` is the usual difference between nozzle
|
||||
volume types, then flow ratio, temperature and retraction. Use measured values; never copy the
|
||||
Standard value into the High Flow variant and call it tuned.
|
||||
6. `nil` is legal per variant in the nullable override keys, occupies one entry like any value, and
|
||||
keeps the printer's value for that variant only.
|
||||
7. Declare a multi-variant list with the arrays it sizes: on the filament, in its own file or in a
|
||||
template it pulls in with [`include`](vendor-bundle.md#inherits-and-include). The filament may be
|
||||
compatible with printers that list fewer variants, or none: each printer takes the filament's variant
|
||||
for its own variant string, else the filament's first variant. A one-variant list on a shared base
|
||||
is harmless, because one variant is the width a list-less preset has anyway; and a list-less base is
|
||||
stored at that width, so a wider array on it reaches its children as its first value only.
|
||||
A key such a base writes reaches a multi-variant child as one value, which the loader spreads over
|
||||
the child's variants; `check --strict` reports it at the child, which restates it at its own width.
|
||||
|
||||
## Widths
|
||||
|
||||
Every variant key is a flat array addressed by **variant index**: index `n` belongs to entry `n` of
|
||||
the preset's own variant list (`printer_extruder_variant`, `print_extruder_variant` or
|
||||
`filament_extruder_variant`). The index is found by exact compare of the string
|
||||
`"<extruder_type> <nozzle volume type>"` (plus the 1-based extruder id for printer and process lists),
|
||||
and the value is read at `index × stride`:
|
||||
|
||||
| Stride | Keys | Layout |
|
||||
| --- | --- | --- |
|
||||
| 1 | `printer_options_with_variant_1`, `print_options_with_variant`, `filament_options_with_variant` | `[variant 0, variant 1, …]` |
|
||||
| 2 | `printer_options_with_variant_2` (the `machine_max_*` limits) | `[variant 0 normal, variant 0 silent, variant 1 normal, variant 1 silent, …]` |
|
||||
|
||||
So a variant array is `variant length × stride` long. The **variant length** is the number of
|
||||
entries in the preset's variant list, counted on the preset's config **after** `include` and
|
||||
`inherits` are applied, dead variants included. An inherited array arrives already resized to the
|
||||
base's own variant length, an included one at the width its file wrote
|
||||
([composition](#padding-truncation-and-composition)):
|
||||
|
||||
| Preset | Variant length |
|
||||
| --- | --- |
|
||||
| `machine` | `len(printer_extruder_variant)`; without it, the variants `extruder_variant_list` offers; without both, **one per extruder** (`len(nozzle_diameter)`), since the list's default is one `Direct Drive Standard` per extruder. `extruders_count` is a printer-tab field, not a preset key. The loader honours that default only halfway for a system preset: it sizes the composed preset by the one-entry default `printer_extruder_variant`, cutting every variant array to its first value, and only then (with `single_extruder_multi_material` off) rebuilds the pair to one variant per extruder ([Printer rule 2](#printer-machine)) and pads the arrays with that value. The rule's width is still one per extruder; to give extruders different values, declare the layout. |
|
||||
| `process` | `len(print_extruder_variant)`; without one, 1 |
|
||||
| `filament` | `len(filament_extruder_variant)`; without one, 1 |
|
||||
|
||||
The per-extruder keys in `printer_extruder_options` ([listed with the sets](#the-four-key-sets)) are
|
||||
outside this scheme and stay one value per extruder. Keys outside [the four sets](#the-four-key-sets)
|
||||
are never variant-resized or addressed by variant index, however wide a shipped file writes them; a
|
||||
per-extruder vector holds `len(nozzle_diameter)` values, and a shorter one acts as padded with its
|
||||
first value ([machine-profiles.md](machine-profiles.md#multi-extruder-idex-and-tool-changers)).
|
||||
|
||||
### Sizing equation
|
||||
|
||||
For a key in one of the four variant sets:
|
||||
|
||||
```
|
||||
E = extruders = len(nozzle_diameter) = len(extruder_type)
|
||||
= len(default_nozzle_volume_type) = len(extruder_variant_list)
|
||||
V_i = variants listed for extruder i = ","-separated entries of extruder_variant_list[i],
|
||||
each "<extruder_type[i]> <nozzle volume type>"
|
||||
S = variant length = V_1 + V_2 + … + V_E
|
||||
= len(printer_extruder_variant) = len(printer_extruder_id)
|
||||
k = stride = 2 for printer_options_with_variant_2, else 1
|
||||
|
||||
N = values the key holds, one per variant:
|
||||
machine (printer_options_with_variant_1, _2) = S × k
|
||||
process (print_options_with_variant) = len(print_extruder_variant) = len(print_extruder_id)
|
||||
filament (filament_options_with_variant) = len(filament_extruder_variant)
|
||||
values[s × k + m] = variant index s, mode m (m = 0 normal, m = 1 silent; only m = 0 at stride 1)
|
||||
variant index s = (printer_extruder_id[s], printer_extruder_variant[s])
|
||||
```
|
||||
|
||||
`printer_extruder_variant` is not sized by the equation; it defines `S`: it is `extruder_variant_list`
|
||||
flattened extruder by extruder, and `printer_extruder_id[s]` is the extruder that index `s` came from.
|
||||
A process that mirrors the printer's pairs, as [Process rule 1](#process) asks, has `N = S`; a
|
||||
filament lists each variant string it is tuned for once, with no extruder id, so its `N` is its own
|
||||
and independent of any one printer: it serves every printer in its `compatible_printers`, and a
|
||||
variant string no extruder of a printer reports is simply never read there (a two-extruder printer
|
||||
with `S = 7` serves a filament whose `N` is 3, and the filament keeps its 3 on a printer that offers
|
||||
two variant strings). The
|
||||
pair is what the loader reads, so the equality with the sum holds when the three keys agree, as
|
||||
[Printer rule 2](#printer-machine) requires.
|
||||
|
||||
A selectable preset that writes a variant key writes it at its own `N`; any other width, one value
|
||||
included, is an error. A preset that leaves a key out takes what reaches it, the default or an array
|
||||
it inherits or includes, which the loader resizes to the preset's `N`; `check --strict` holds that
|
||||
array to the preset's `N` too. A base is not judged on its own: its arrays count only where they reach
|
||||
a preset that does not override them. BBL is the model for strict: every printer-specific machine,
|
||||
process and filament declares its layout and restates every variant key at its own `N`, even where all
|
||||
the values are the same, keys Orca added to the sets included.
|
||||
Without the lists, `extruder_variant_list` defaults to one
|
||||
`Direct Drive Standard` per extruder, so a machine has `V_i = 1` and `S = E`, and a process or filament
|
||||
has `N = 1`; the loader cuts a list-less machine to its first variant and, with
|
||||
`single_extruder_multi_material` off, widens it again with that value ([variant length](#widths)). The per-extruder keys outside the sets
|
||||
(`printer_extruder_options` plus `extruder_offset` and `extruder_colour`) and `extruder_variant_list`
|
||||
itself hold `E` values.
|
||||
|
||||
### Sizing examples
|
||||
|
||||
**One extruder, two nozzle volume types**: `E = 1`, `V_1 = 2`, so `S = 2`; stride-1 keys hold 2
|
||||
values, `machine_max_*` hold 4:
|
||||
|
||||
```json
|
||||
"nozzle_diameter": ["0.4"],
|
||||
"extruder_type": ["Direct Drive"],
|
||||
"extruder_variant_list": ["Direct Drive Standard,Direct Drive High Flow"],
|
||||
"printer_extruder_id": ["1", "1"],
|
||||
"printer_extruder_variant": ["Direct Drive Standard", "Direct Drive High Flow"],
|
||||
"default_nozzle_volume_type": ["Standard"],
|
||||
"retraction_length": ["0.8", "1.0"],
|
||||
"machine_max_speed_x": ["500", "200", "600", "250"]
|
||||
```
|
||||
|
||||
The matching process lists the same two pairs (`print_extruder_id` `["1", "1"]`,
|
||||
`print_extruder_variant` as above) and holds 2 values per key, e.g. `outer_wall_speed`
|
||||
`["200", "260"]`; a filament for it lists `["Direct Drive Standard", "Direct Drive High Flow"]` and
|
||||
holds 2 values per key, e.g. `filament_max_volumetric_speed` `["16", "24"]`.
|
||||
|
||||
**Four extruders, one nozzle volume type each** (a tool changer): `E = 4`, every `V_i = 1`, so
|
||||
`S = 4`; stride-1 keys hold 4 values, `machine_max_*` hold 8:
|
||||
|
||||
```json
|
||||
"nozzle_diameter": ["0.4", "0.4", "0.6", "0.4"],
|
||||
"extruder_type": ["Direct Drive", "Direct Drive", "Direct Drive", "Direct Drive"],
|
||||
"extruder_variant_list": ["Direct Drive Standard", "Direct Drive Standard",
|
||||
"Direct Drive Standard", "Direct Drive Standard"],
|
||||
"printer_extruder_id": ["1", "2", "3", "4"],
|
||||
"printer_extruder_variant": ["Direct Drive Standard", "Direct Drive Standard",
|
||||
"Direct Drive Standard", "Direct Drive Standard"],
|
||||
"default_nozzle_volume_type": ["Standard", "Standard", "Standard", "Standard"],
|
||||
"retraction_length": ["0.8", "0.8", "1.2", "0.8"],
|
||||
"machine_max_speed_x": ["500", "200", "500", "200", "500", "200", "500", "200"]
|
||||
```
|
||||
|
||||
The (normal, silent) pair is repeated per extruder: `["500", "200"]` would be width 2 against
|
||||
`S × k = 8` (the loader pads it to `500, 200, 500, 500, …`) and `["500"]` width 1; both are errors. The
|
||||
process mirrors the four pairs (`print_extruder_id` `["1", "2", "3", "4"]`) with 4 values per key, or
|
||||
omits the variant keys altogether when nothing differs per extruder (then one value per key). A
|
||||
filament for it lists only `["Direct Drive Standard"]`: one entry, so one value per key. Drop the
|
||||
layout from this printer and the widths stay the same, since the default list gives `S = E = 4`; but
|
||||
the loader then keeps only the first variant ([variant length](#widths)): `retraction_length`
|
||||
becomes `0.8` on every extruder and extruder 3 loses its `1.2`.
|
||||
|
||||
### Adding a variant inserts its values at its variant index
|
||||
|
||||
Indexes run extruder-major: extruder 1's variants in `extruder_variant_list` order, then extruder
|
||||
2's. A new variant's values go in at its index, not at the end. Giving extruder 1 of a two-extruder
|
||||
printer a High Flow option, when only extruder 2 had one:
|
||||
|
||||
| Key | Before | After |
|
||||
| --- | --- | --- |
|
||||
| `extruder_variant_list` | `["Direct Drive Standard", "Direct Drive Standard,Direct Drive High Flow"]` | `["Direct Drive Standard,Direct Drive High Flow", "Direct Drive Standard,Direct Drive High Flow"]` |
|
||||
| `printer_extruder_id` | `["1", "2", "2"]` | `["1", "1", "2", "2"]` |
|
||||
| `printer_extruder_variant` | `[Standard, Standard, High Flow]` | `[Standard, High Flow, Standard, High Flow]` (full strings in the file) |
|
||||
| `retraction_length` (stride 1) | `["0.8", "1.0", "1.2"]` | `["0.8", "?", "1.0", "1.2"]`: one value at index 1 |
|
||||
| `machine_max_speed_x` (stride 2) | `["500", "200", "600", "250", "700", "300"]` | `["500", "200", "?", "?", "600", "250", "700", "300"]`: a (normal, silent) pair at position 2 |
|
||||
| `print_extruder_id` / `print_extruder_variant` / `outer_wall_speed` | mirror the printer | the same insertion at index 1 |
|
||||
| `filament_extruder_variant` `[Standard, High Flow]` | — | unchanged: filament variants carry no extruder id, so the existing High Flow variant now serves both extruders |
|
||||
|
||||
Every `?` is a measured value for that nozzle, on the machine limits as much as on retraction.
|
||||
Removing or renaming a variant shifts the later values the same way in reverse; a variant string that
|
||||
no longer matches the enum is simply a variant nothing selects.
|
||||
|
||||
### Padding, truncation and composition
|
||||
|
||||
**Every key in the set widens, whether or not a file restates it.** At load each variant key of the
|
||||
composed config is resized to the length of the preset's own `*_extruder_variant` (the one-entry
|
||||
default when it writes none) × stride: a short array is **padded by repeating its first value**, a
|
||||
long one is truncated to its first values, without a word from the loader or the validator. The rule
|
||||
does not follow from this padding: a file writes a variant key at **exactly `variant length × stride`**
|
||||
or not at all. Any other length, one value included (which the loader spreads over every variant, at
|
||||
stride 2 over normal *and* silent alike), is a mistake the loader hides and
|
||||
`orca_profile_tool.py check` reports as an error in the selectable preset that writes it, even when
|
||||
every value is the same; `check --strict` also reports an array that reaches a selectable preset at
|
||||
another width
|
||||
([variant arrays](validation.md#variant-arrays)).
|
||||
|
||||
The loader sizes a list-less preset of any type to one variant at this step, a list-less machine
|
||||
included: a two-extruder machine without a layout that writes `retraction_length` `["0.8", "0.9"]`, the
|
||||
width the equation asks for, stores `["0.8"]`, which the pair rebuild and the slice-time collapse hand
|
||||
to both extruders. A 3-value array on a preset of variant length 2 keeps its first two; at length 4 it
|
||||
becomes `[a, b, c, a]`.
|
||||
|
||||
Composition hands down widths in two ways:
|
||||
|
||||
- **`inherits` hands down the parent's resized arrays.** A base is stored after its own resize, at
|
||||
the length of its own `*_extruder_variant` (one variant for a base of any type that writes none,
|
||||
whatever its extruder count), so an array wider than the base's list is cut to its first values
|
||||
before any child sees it, and a child that adds variants gets those first values padded. Widen an
|
||||
array only on a preset whose own resolved list already has the entries. `check` judges selectable
|
||||
presets only, at the width each file wrote: an array a base's own resize cuts is not seen (a review
|
||||
item), and an inherited array of another width than a child's list is left to the loader's resize
|
||||
unless `check --strict`, which asks the child to restate it at its own width.
|
||||
- **`include` hands down the template's diff at its pre-resize width.** The template contributes every
|
||||
key where its composed config differs from the built-in defaults, taken before its own resize, so the
|
||||
arrays it writes arrive at the width its file wrote, and the includer's own list sizes them. A key the
|
||||
template sets to the built-in default is not passed on
|
||||
([`include`](vendor-bundle.md#inherits-and-include)).
|
||||
|
||||
The id keys are the trap in this padding: `printer_extruder_id` and `print_extruder_id` are members
|
||||
with default `[1]`, so a missing or one-value id array beside a longer variant list is padded to
|
||||
extruder 1 at every index. That is right on a single-extruder printer and wrong on a multi-extruder
|
||||
one, where every variant is then addressed as extruder 1's. A machine escapes it only where the
|
||||
load-time pair rebuild of [Printer rule 2](#printer-machine) runs (`single_extruder_multi_material`
|
||||
off); a process's `print_extruder_id` is never rebuilt at load. `check`
|
||||
holds an id array that reaches a selectable preset, written or inherited, to one entry per variant on
|
||||
any printer; `fix-variant` never pads an id array or a variant list, since those address the
|
||||
variants rather than fill them.
|
||||
|
||||
Consequences:
|
||||
|
||||
- A base that gains a variant silently pads every descendant that restates a variant array at the old
|
||||
width, and a short `machine_max_*` array copies variant 0's *normal* limit into the silent entries
|
||||
too; a descendant that restates nothing inherits the widened array and needs no edit. Extend, in one
|
||||
change: the printer base and each nozzle-diameter sibling that restates a variant array, the process
|
||||
bases that mirror the printer's variants, and the filaments that should cover the variant.
|
||||
- A one-value override is reported, and the loader spreads it over **all** variants, overwriting the
|
||||
ones that should differ.
|
||||
- A process or filament that omits the new variant is not padded; the variant resolves to its index 0.
|
||||
|
||||
### Slice time and existing users
|
||||
|
||||
**Slice time collapses variants to extruders.** When printer, process and filaments are combined on a
|
||||
printer with several extruders or several variant strings, each printer and process variant key is
|
||||
re-gathered to one entry per extruder (× stride) in extruder order, using each extruder's live nozzle
|
||||
volume type, or to one entry per nozzle volume type for an extruder that holds several (Hybrid, or
|
||||
`extruder_nozzle_stats` listing more types than there are extruders); on any other printer they are
|
||||
not re-gathered and variant index 0 is read. Filament keys are re-gathered to one entry per filament,
|
||||
on a printer with a single variant too once a filament has several variants, and under a dynamic
|
||||
nozzle map to one entry per variant each filament prints through. Custom G-code and the
|
||||
`machine_max_*` limits therefore index by extruder or filament, never by variant index; variant order
|
||||
matters only inside the preset. An extruder with no matching variant reads variant index 0, extruder
|
||||
1's first variant, whichever extruder it is; under layered nozzle grouping, `get_config_index_base`
|
||||
reads the collapsed arrays and falls back to the same extruder's first entry instead.
|
||||
|
||||
**Existing user presets and projects follow the variant string, not the position.** A user preset
|
||||
stores every variant array it changed (nullable keys as per-variant diffs, `nil` where equal to the
|
||||
parent). On load each parent variant takes the child's value for the variant with the same extruder id
|
||||
and variant string; variants the parent gained keep the parent's value, and a child whose arrays the
|
||||
parent cannot map keeps its own. Per-object process overrides are remapped when a printer change
|
||||
alters the extruder count or the length of `printer_extruder_variant`, by variant string alone (no
|
||||
extruder id; of several matching values the smallest wins), and a single-value override applies to
|
||||
every variant. Adding or reordering variants in a shipped preset is therefore safe for existing
|
||||
users; renaming a variant, or moving it to another extruder id, loses their values for it.
|
||||
|
||||
**A new nozzle volume type changes no array.** Adding one to the code widens nothing until a profile
|
||||
lists the new variant string; until then every existing array keeps its length and meaning.
|
||||
|
||||
## The four key sets
|
||||
|
||||
Membership is literal (four `std::set<std::string>` initializers in `src/libslic3r/PrintConfig.cpp`)
|
||||
and **not guessable from names**: `ironing_speed`, `skirt_speed`, `wipe_speed`, `scarf_joint_speed`,
|
||||
`small_support_perimeter_speed` and `wipe_tower_max_purge_speed` are process speeds outside the set,
|
||||
while every process `*_acceleration` and `*_jerk` key is inside, and so are
|
||||
`small_perimeter_threshold`, `top_solid_infill_flow_ratio` and `slowdown_for_curled_perimeters`;
|
||||
`filament_flush_temp` is in and `filament_flush_temp_fast` out; `use_firmware_retraction` is out and
|
||||
`travel_slope` and `retract_lift_enforce` in. The three variant-list keys and the two id
|
||||
keys are members of their own set (the list sizes itself, a no-op; the id keys are padded like any
|
||||
other member, [the id trap](#padding-truncation-and-composition)), while `extruder_variant_list` is in
|
||||
no set: the variant-length resize leaves it alone, and only the pair rebuild of
|
||||
[Printer rule 2](#printer-machine) pads it. The per-extruder `printer_extruder_options` is listed
|
||||
last for contrast; it is not a variant set.
|
||||
|
||||
`check` and `fix-variant` read the four sets from `PrintConfig.cpp` on every run, so they follow the
|
||||
engine. The lists below are from the 2026-09-29 checkout; regenerate them from the repository root
|
||||
before relying on them (the recipe strips comments, since an initializer can carry a commented-out entry):
|
||||
|
||||
```bash
|
||||
python3 - <<'EOF'
|
||||
import re
|
||||
src = open('src/libslic3r/PrintConfig.cpp', encoding='utf-8', errors='replace').read()
|
||||
for name in ['printer_options_with_variant_1', 'printer_options_with_variant_2',
|
||||
'print_options_with_variant', 'filament_options_with_variant',
|
||||
'printer_extruder_options']:
|
||||
body = re.search(r'std::set<std::string>\s+' + name + r'\s*=\s*\{(.*?)\};', src, re.S).group(1)
|
||||
body = re.sub(r'/\*.*?\*/', '', body, flags=re.S)
|
||||
body = re.sub(r'//[^\n]*', '', body)
|
||||
print(name, sorted(set(re.findall(r'"([^"]+)"', body))))
|
||||
EOF
|
||||
```
|
||||
|
||||
**`printer_options_with_variant_1`**, machine, stride 1 (27): `deretraction_speed`, `hotend_cooling_rate`, `hotend_heating_rate`, `long_retractions_when_cut`, `nozzle_flush_dataset`, `nozzle_type`, `nozzle_volume`, `printer_extruder_id`, `printer_extruder_variant`, `retract_after_wipe`, `retract_before_wipe`, `retract_length_toolchange`, `retract_lift_above`, `retract_lift_below`, `retract_lift_enforce`, `retract_restart_extra`, `retract_restart_extra_toolchange`, `retract_when_changing_layer`, `retraction_distances_when_cut`, `retraction_length`, `retraction_minimum_travel`, `retraction_speed`, `travel_slope`, `wipe`, `wipe_distance`, `z_hop`, `z_hop_types`
|
||||
|
||||
**`printer_options_with_variant_2`**, machine, stride 2 (16): `machine_max_acceleration_e`, `machine_max_acceleration_extruding`, `machine_max_acceleration_retracting`, `machine_max_acceleration_travel`, `machine_max_acceleration_x`, `machine_max_acceleration_y`, `machine_max_acceleration_z`, `machine_max_jerk_e`, `machine_max_jerk_x`, `machine_max_jerk_y`, `machine_max_jerk_z`, `machine_max_junction_deviation`, `machine_max_speed_e`, `machine_max_speed_x`, `machine_max_speed_y`, `machine_max_speed_z`
|
||||
|
||||
**`print_options_with_variant`**, process, stride 1 (45): `bridge_acceleration`, `bridge_speed`, `default_acceleration`, `default_jerk`, `default_junction_deviation`, `enable_overhang_speed`, `gap_infill_speed`, `infill_jerk`, `initial_layer_acceleration`, `initial_layer_infill_speed`, `initial_layer_jerk`, `initial_layer_speed`, `initial_layer_travel_acceleration`, `initial_layer_travel_jerk`, `initial_layer_travel_speed`, `inner_wall_acceleration`, `inner_wall_jerk`, `inner_wall_speed`, `internal_bridge_speed`, `internal_solid_infill_acceleration`, `internal_solid_infill_speed`, `outer_wall_acceleration`, `outer_wall_jerk`, `outer_wall_speed`, `overhang_1_4_speed`, `overhang_2_4_speed`, `overhang_3_4_speed`, `overhang_4_4_speed`, `print_extruder_id`, `print_extruder_variant`, `slowdown_for_curled_perimeters`, `small_perimeter_speed`, `small_perimeter_threshold`, `sparse_infill_acceleration`, `sparse_infill_speed`, `support_interface_speed`, `support_speed`, `top_solid_infill_flow_ratio`, `top_surface_acceleration`, `top_surface_jerk`, `top_surface_speed`, `travel_acceleration`, `travel_jerk`, `travel_speed`, `travel_speed_z`
|
||||
|
||||
**`filament_options_with_variant`**, filament, stride 1 (54): `activate_air_filtration`, `activate_air_filtration_during_print`, `activate_air_filtration_on_completion`, `adaptive_pressure_advance`, `adaptive_pressure_advance_bridges`, `adaptive_pressure_advance_model`, `adaptive_pressure_advance_overhangs`, `complete_print_exhaust_fan_speed`, `during_print_exhaust_fan_speed`, `enable_pressure_advance`, `filament_adaptive_volumetric_speed`, `filament_cooling_before_tower`, `filament_deretraction_speed`, `filament_extruder_variant`, `filament_flow_ratio`, `filament_flush_temp`, `filament_flush_volumetric_speed`, `filament_ironing_flow`, `filament_ironing_inset`, `filament_ironing_spacing`, `filament_ironing_speed`, `filament_long_retractions_when_cut`, `filament_max_volumetric_speed`, `filament_pre_cooling_temperature`, `filament_pre_cooling_temperature_nc`, `filament_preheat_temperature_delta`, `filament_ramming_travel_time`, `filament_ramming_travel_time_nc`, `filament_ramming_volumetric_speed`, `filament_ramming_volumetric_speed_nc`, `filament_retract_after_wipe`, `filament_retract_before_wipe`, `filament_retract_length_nc`, `filament_retract_length_toolchange`, `filament_retract_lift_above`, `filament_retract_lift_below`, `filament_retract_lift_enforce`, `filament_retract_restart_extra`, `filament_retract_restart_extra_toolchange`, `filament_retract_when_changing_layer`, `filament_retraction_distances_when_cut`, `filament_retraction_length`, `filament_retraction_minimum_travel`, `filament_retraction_speed`, `filament_wipe`, `filament_wipe_distance`, `filament_z_hop`, `filament_z_hop_types`, `long_retractions_when_ec`, `nozzle_temperature`, `nozzle_temperature_initial_layer`, `pressure_advance`, `retraction_distances_when_ec`, `volumetric_speed_coefficients`
|
||||
|
||||
**`printer_extruder_options`**, machine, one value per extruder, not a variant set (8):
|
||||
`default_nozzle_volume_type`, `extruder_max_nozzle_count`, `extruder_printable_area`,
|
||||
`extruder_printable_height`, `extruder_type`, `max_layer_height`, `min_layer_height`,
|
||||
`nozzle_diameter`. These are never addressed by variant index; `extruder_offset`, `extruder_colour` and
|
||||
`extruder_variant_list` are per extruder too. A shorter array acts as padded with its first value, and
|
||||
entries beyond the extruder count are never read
|
||||
([per-extruder vectors](machine-profiles.md#multi-extruder-idex-and-tool-changers)).
|
||||
|
||||
## Checking and testing
|
||||
|
||||
Checklist for a new variant profile set:
|
||||
|
||||
1. Decide the variants per extruder from the real hardware; pick legal strings only.
|
||||
2. Printer base: `extruder_type`, `extruder_variant_list`, flattened `printer_extruder_variant` +
|
||||
`printer_extruder_id`, `default_nozzle_volume_type`; every variant array at variant length, every
|
||||
`machine_max_*` at 2 × variant length as (normal, silent) pairs, even where the values are the
|
||||
same, values in variant order.
|
||||
3. A process base per variant layout, or no variant keys at all.
|
||||
4. Filaments for the printer: a variant list covering the intended variants, every variant key at that
|
||||
width, measured values per nozzle volume type.
|
||||
5. Run the usual authoring commands and full checks. `check` holds each array of steps 2–4 to the
|
||||
width of the selectable preset that writes it, `check --strict` also to every selectable preset it
|
||||
reaches, and
|
||||
the printer's layout keys through every selectable preset
|
||||
([variant arrays](validation.md#variant-arrays)); `check_variant_names` holds every variant string,
|
||||
`extruder_type`, `nozzle_volume_type` and `default_nozzle_volume_type` to the enums
|
||||
([variant names](validation.md#variant-names)). The choice of variants, the variant order and
|
||||
the measured values are not checked.
|
||||
6. In the app, for each nozzle volume type in the sidebar combo: slice and confirm the G-code uses that
|
||||
variant's values (e.g. volumetric speed limit, retraction). `validate_slice` only slices the default
|
||||
nozzle volume type.
|
||||
|
||||
To review rather than author, run the same list against the diff. `check` catches a wrong array length,
|
||||
a layout key out of step, and a variant string the enums cannot build. The failures no check catches: a
|
||||
new variant appended instead of inserted at its index, an array widened on a base whose list is
|
||||
shorter (cut before any child inherits it), a process lacking a pair its printer can select, and a High
|
||||
Flow variant copied from Standard.
|
||||
|
||||
### UI facts to design around
|
||||
|
||||
- The single-extruder sidebar shows a **nozzle volume type combo** (tooltip `Flow`, in place of the
|
||||
nozzle-diameter selector) only when `extruder_variant_list` offers more than one distinct variant
|
||||
string (`support_different_extruders`); four extruders listing `Direct Drive Standard` each show
|
||||
none.
|
||||
- The two-extruder sidebar's per-extruder nozzle volume type combos are shown for BBL printers only.
|
||||
Another vendor's multi-extruder printer falls back to the single-extruder layout: one combo (for the
|
||||
first extruder) when the variants differ, otherwise the nozzle-diameter selector, so the other
|
||||
extruders' nozzle volume type stays at `default_nozzle_volume_type`.
|
||||
- High Flow is hidden from the combo when `printer_variant` is `0.2` or the printer model is
|
||||
`Bambu Lab X1E`, and E3D High Flow unless `printer_variant` is `0.4` or `0.6`. A variant listed on
|
||||
another nozzle diameter is never selectable there.
|
||||
- The filament tab shows a variant switch built from the filament's own `filament_extruder_variant`;
|
||||
a multi-variant filament whose tab shows none did not resolve its variant list through `include` or
|
||||
`inherits`. The printer and process tabs show one entry per extruder instead, labelled with that
|
||||
extruder's live nozzle volume type (two for Hybrid), and only on two-extruder printers whose
|
||||
`extruder_variant_list` offers more than one variant string; elsewhere they edit the variant of the
|
||||
nozzle volume type selected in the sidebar (an X1C shows no switch).
|
||||
|
||||
### Worked examples in the tree
|
||||
|
||||
`BBL/machine/fdm_bbl_3dp_001_common.json` (one extruder), `fdm_bbl_3dp_002_common.json` (two
|
||||
extruders), `Bambu Lab X1 Carbon 0.4 nozzle.json` (Standard + High Flow), `Bambu Lab H2D 0.4
|
||||
nozzle.json` (extruders with different variant sets), `Bambu Lab X2D 0.4 nozzle.json` (Direct Drive +
|
||||
Bowden), with their `@BBL` processes and filaments. The H2D and X2D examples also carry
|
||||
`E3D High Flow` variants. BBL's multi-variant filament lists come from `fdm_filament_template_*`
|
||||
presets that the printer-specific filaments pull in with `include`, not from their `inherits` chain.
|
||||
@@ -1,286 +0,0 @@
|
||||
# Filament profiles and OrcaFilamentLibrary
|
||||
|
||||
`OrcaFilamentLibrary` is the filament-only bundle the loader reads **first**, so any vendor's filament 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 vendor | `OrcaFilamentLibrary/filament/<Brand>/<PrinterVendor>/` (recommended); `<PrinterVendor>/filament/<Brand>/` also works |
|
||||
| A printer vendor's tune of a generic, or its own product | `<PrinterVendor>/filament/` |
|
||||
|
||||
Both locations in the third 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 brand should contribute to: `OrcaFilamentLibrary/filament/<Brand>/` is the brand's own folder,
|
||||
while a printer vendor's folder belongs to that printer vendor.
|
||||
|
||||
Library layout: `filament/base/fdm_filament_*.json` material 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, and `filament_vendor` inside the JSON is the real vendor string.
|
||||
|
||||
## The three-part shape
|
||||
|
||||
```jsonc
|
||||
// OrcaFilamentLibrary/filament/Polymaker/Fiberon PA6-CF @base.json — the product root: identity + material values
|
||||
{ "type": "filament", "name": "Fiberon PA6-CF @base", "from": "system",
|
||||
"instantiation": "false", "inherits": "fdm_filament_pa",
|
||||
"filament_id": "OFkOviHk", // minted here by generate-id; every child inherits it
|
||||
"filament_vendor": ["Polymaker"], "filament_type": ["PA6-CF"], /* … */ }
|
||||
|
||||
// OrcaFilamentLibrary/filament/Polymaker/Fiberon PA6-CF @System.json — the selectable all-printer shim, 7 keys
|
||||
{ "type": "filament", "name": "Fiberon PA6-CF @System", "from": "system",
|
||||
"instantiation": "true", "inherits": "Fiberon PA6-CF @base",
|
||||
"setting_id": "…", "compatible_printers": [] }
|
||||
|
||||
// BBL/filament/Polymaker/Fiberon PA6-CF @BBL X1C.json — a printer tune (BBL keeps its own copy of the @base)
|
||||
{ …, "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 root is really `instantiation: "false"`. A base carries
|
||||
**no** `setting_id`, no `compatible_printers` and no `filament_settings_id`. Only the `setting_id`
|
||||
half is enforced; the other two are unchecked, so a neighbouring base that carries them is no model.
|
||||
- Every `@System` shim must be `"instantiation": "true"`; one set to `"false"` would ship but could
|
||||
never be selected, and no check catches it. The shim exists only for products in the library.
|
||||
- `filament_cost`, `filament_density`, `filament_type` and `filament_vendor` belong on the root and
|
||||
should not appear in a printer tune.
|
||||
- A brand `@base` duplicated across bundles is legal (a vendor bundle may keep its own copy of a
|
||||
library product root, with the same id): bases never become selectable presets and the
|
||||
duplicate-name error covers only those, so there is none.
|
||||
- You may inherit from an instantiated preset as well as from a base.
|
||||
|
||||
## Colour is a runtime property
|
||||
|
||||
`filament_id` identifies a product, not a colour; filament sync and AMS read the colour from the spool
|
||||
at runtime. A product ships one all-printer preset and the colour is chosen at runtime, never a sibling
|
||||
preset that differs only by colour. A material family (PLA vs PLA Matte vs PLA Silk) is a new product;
|
||||
a colour is not. A printer tune keeps the product alias and does not multiply per colour either.
|
||||
|
||||
CI does not catch this (per-colour presets pass `check`), so it is a review call.
|
||||
|
||||
## The two most common contributions
|
||||
|
||||
**A printer vendor tuning a generic.** Keep the `Generic X` alias so it 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 file:
|
||||
|
||||
```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 on a material base so `generate-id`
|
||||
can mint the id, then one instantiated leaf per printer in the same bundle. Inheriting
|
||||
`Generic X @System` directly gives the product the generic's id, which the tool cannot fix
|
||||
([ids.md](ids.md#what-generate-id-does-and-does-not-fix)). 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.
|
||||
|
||||
To offer either kind by default, add its name to each model's `default_materials`; put it first in the
|
||||
machine's `default_filament_profile` only if it should be the preselected filament
|
||||
([machine keys](machine-profiles.md#other-keys)).
|
||||
|
||||
## `compatible_printers`
|
||||
|
||||
- **Library fallbacks** (`@System`): 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
|
||||
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: `validate_system` reads the resolved config, so an inherited list
|
||||
satisfies it, while `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* collides with the library generic's
|
||||
`filament_id` on every printer.
|
||||
- Copying a base's full printer list onto a nozzle-specific tune produces duplicate combobox entries: a
|
||||
real shipped bug twice over.
|
||||
|
||||
## Overlapping coverage: one variant, one profile per product
|
||||
|
||||
`filament_id` is the **product** key, not the preset key: every preset of one product shares it
|
||||
(`<filament_vendor>/<filament_type>/<alias>`). So if one printer variant appears in the
|
||||
`compatible_printers` of two presets of that product, the slicer cannot tell them apart at AMS match
|
||||
time. `validate_system` reports `Ambiguous AMS filament match: N filament presets share filament_id "X"
|
||||
and are all compatible with printer "Y"`; `orca_profile_tool.py check` does **not** see it and passes.
|
||||
Resolve the overlap by **specificity**: keep the variant on the most specific profile and remove it from
|
||||
every more general one. Deleting a profile is the least preferred fix: moving coverage keeps the tune
|
||||
that users rely on.
|
||||
|
||||
Judge specificity from the profile's `compatible_printers` (how many variants it actually covers) and
|
||||
use the name only as a secondary, often vague hint; decide by the lists, with a judgement call on the
|
||||
name. Naming conventions differ by vendor: BBL's is the reference (`@<Vendor> <Model>` for a whole
|
||||
model, `@<Vendor> <Model> <nozzle> nozzle` for one variant, `@<Vendor>` for a vendor-wide generic), but
|
||||
others vary (`@<printer model>`, a printer serial, or Creality's `@<Model>-all`). A name never overrides
|
||||
the list; see [preset naming](naming.md#filament) for the shapes.
|
||||
|
||||
Specificity, most to least:
|
||||
|
||||
1. **Variant-specialized**: lists a single printer variant (BBL-style
|
||||
`… @<Vendor> <Model> <nozzle> nozzle`).
|
||||
2. **Model-specialized**: lists the variants of one printer model (BBL-style `… @<Vendor> <Model>`). It
|
||||
should cover every variant of its model, not only the nozzle it was authored for.
|
||||
3. **Family / series**: lists variants spanning a printer family or series.
|
||||
4. **Generic / catch-all**: vendor-wide, covering many unrelated models (often the bare
|
||||
`Generic <mat> @<Vendor>`).
|
||||
|
||||
Rules:
|
||||
|
||||
- A model-specialized profile is extended to **all** variants of its model, and each variant it thereby
|
||||
starts covering is removed from the family and generic profiles that also listed it, including
|
||||
variants that had no overlap before. Apply it per nozzle, not just 0.4.
|
||||
- Apply it **per product**: trim only the material that has a specialized profile from the generic; a
|
||||
material whose product has no specialized profile keeps the variant in the generic.
|
||||
- Never strip coverage a variant has nowhere else to get. If a variant has no variant-level specialized
|
||||
profile, the next level down keeps it; when the model has specialized profiles, the model-level one
|
||||
wins over the family and generic ones.
|
||||
- Moving coverage is preferred over deleting. If a profile must be deleted, remove the more general one,
|
||||
not the specialized profile that carries the tune.
|
||||
- After moving coverage, repoint the affected machine's `default_filament_profile` and clean the model's
|
||||
`default_materials`: they should name the most specific profile that covers the variant, and should
|
||||
not keep generic entries that no longer cover the model. This rule applies equally when adding or
|
||||
fixing defaults.
|
||||
|
||||
Several profiles of one product with **disjoint** `compatible_printers` is the intended end state.
|
||||
Adding coverage to the specialized profile and removing it from the generic is the preferred direction.
|
||||
|
||||
**Detection caveat:** `orca_profile_tool.py check` is blind to this; only the validator behind the full
|
||||
`./scripts/check_profile.sh` reports it (`validate_system`). Always confirm with that, not the
|
||||
vendor-scoped loop.
|
||||
|
||||
## 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` is hidden on every printer that a same-alias preset
|
||||
lists in a non-empty `compatible_printers`, whether that preset is in the library or in a vendor bundle
|
||||
(a printer matches by its own name or its parent's).
|
||||
|
||||
Two consequences:
|
||||
|
||||
- **Only an unrestricted library fallback can be shadowed.** Two printer-specific presets sharing an
|
||||
alias do not hide each other; overlapping lists for the same product trip the ambiguous-match error
|
||||
above instead.
|
||||
- This is why adding `Generic PLA @<printer>` to a vendor silently removes the library
|
||||
`Generic PLA @System` from that printer. That is intended, and the reason a vendor tuning a generic
|
||||
must **keep the `Generic X` alias**.
|
||||
|
||||
The literal spelling `Generic <mat> @System` is load-bearing beyond shadowing: when a user preset, an
|
||||
imported preset or a 3MF project names a parent that no longer resolves and contains `Generic`, the
|
||||
loader rewrites the name into `Generic <mat> @System` and retries. Only the library ships those names,
|
||||
so keep them.
|
||||
|
||||
## `filament_id`, `filament_vendor`, `filament_type`
|
||||
|
||||
`filament_id` is minted from the triple `(filament_vendor, filament_type, alias)`. `filament_vendor` and
|
||||
`filament_type` are therefore **identity, not decoration**: editing either, or the alias, 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 `check` rejects as a scalar outright. A
|
||||
scalar `"PP"` once hung the filament and 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. 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"`
|
||||
|
||||
`"nil"` is legal only in an option defined as nullable (`add_nullable`, or `nullable = true`, in
|
||||
`src/libslic3r/PrintConfig.cpp`). Anywhere else it fails the file, and with it the **whole bundle**
|
||||
(`Failed loading configuration file`, after `Deserializing nil into a non-nullable object` or
|
||||
`Invalid value provided for parameter <key>: nil`). To leave a non-nullable key unset, omit it; do not
|
||||
write `nil`.
|
||||
|
||||
In filament presets a minority of the `filament_*` keys are nullable, plus `long_retractions_when_ec`
|
||||
and `retraction_distances_when_ec`. About half of them 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's or extruder's own value*. The rest are ordinary nullable options
|
||||
(`filament_flow_ratio`, `filament_flush_temp`, `filament_adaptive_volumetric_speed`, …), where it means
|
||||
*unset*. Check the option's definition before writing `nil` anywhere else.
|
||||
|
||||
## Tuning per nozzle and per variant
|
||||
|
||||
Between a product's `@X` and `@X 0.N nozzle` tunes the keys that usually 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` (switched on
|
||||
by `enable_pressure_advance`).
|
||||
|
||||
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.
|
||||
|
||||
On a printer with extruder variants, a filament tunes these per variant too:
|
||||
`filament_max_volumetric_speed`, `filament_flow_ratio`, `nozzle_temperature`, pressure advance and the
|
||||
retraction overrides carry one value per variant of `filament_extruder_variant` (Standard, High Flow, …). The
|
||||
exact key set is [`filament_options_with_variant`](extruder-variants.md#the-four-key-sets);
|
||||
`slow_down_min_speed` and `fan_max_speed` are not in it. Keep every such array at exactly that width, even where the
|
||||
setting does not differ per variant, and measure the High Flow variant rather than copying Standard
|
||||
([extruder-variants.md](extruder-variants.md#filament)).
|
||||
|
||||
## Bed temperature is twelve keys, not one
|
||||
|
||||
There is no single "bed temperature". The plate type selected for the printer (`Cool Plate`,
|
||||
`Engineering Plate`, `High Temp Plate`, `Textured PEI Plate`, `Textured Cool Plate`, `Supertack Plate`)
|
||||
picks one of six keys, each with an `_initial_layer` twin: `cool_plate_temp`, `eng_plate_temp`,
|
||||
`hot_plate_temp`, `textured_plate_temp`, `textured_cool_plate_temp` and `supertack_plate_temp`.
|
||||
|
||||
`textured_cool_plate_temp` is the one most often forgotten. A non-BBL printer with
|
||||
`support_multi_bed_types` off hides the plate selector and uses the printer preset's `default_bed_type`
|
||||
(High Temp Plate, `hot_plate_temp`, when unset or invalid), but a loaded project or a CLI config can
|
||||
still carry another plate. So set every plate the printer plausibly has, as the sibling presets in the
|
||||
bundle do.
|
||||
|
||||
## Style
|
||||
|
||||
- Overrides, not full copies: an instantiated filament preset carries around a dozen non-meta keys,
|
||||
and a library `@System` shim two or three. A preset that restates fifty-plus keys from its parent is
|
||||
the pattern to move away from, not to copy. Commit `6943b6ddc3` is the stated model for converting such presets: flip true
|
||||
bases to `instantiation: "false"`, strip their `compatible_printers`, `setting_id` and
|
||||
`filament_settings_id`, and add `renamed_from` on the surviving selectable preset. A value every
|
||||
printer tune of a product shares goes on the product's `@base`
|
||||
([shared bases](shared-bases.md#levels)).
|
||||
- Prefer the library's `fdm_filament_*` bases over a vendor-local copy; for a new preset, even in a
|
||||
bundle whose older presets use one: a local copy drifts 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 on its own: a file that leads with
|
||||
`compatible_printers` passes `check`.
|
||||
- **Every vector-typed (`co…s`) key must be a JSON array.** Only a scalar `filament_type` is an outright
|
||||
error; `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.
|
||||
@@ -1,156 +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>/<alias>`, using the resolved (inherited) first values and the name up to the first `@`, right-trimmed |
|
||||
| Shape | 16 base62 characters | `OF` + 6 base62 characters |
|
||||
| Required on | every `instantiation: "true"` preset | every **instantiated** filament, own or inherited |
|
||||
| Forbidden on | bases (`instantiation` not `"true"`) | — (a product's root base is exactly where it belongs) |
|
||||
| Scope | unique across the whole tree | shared by every preset of the product, in every bundle |
|
||||
|
||||
`<type>` is `machine`, `process` or `filament`, and the vendor is the **folder** name (`BBL`), not the
|
||||
display name (`Bambulab`). Renaming a preset changes its `setting_id`; renaming a filament's alias, or
|
||||
editing its `filament_vendor` or `filament_type`, also changes its `filament_id`, and the old id is not
|
||||
forwarded.
|
||||
|
||||
## The tool
|
||||
|
||||
`scripts/orca_profile_tool.py` takes a subcommand:
|
||||
|
||||
| Command | Does |
|
||||
| --- | --- |
|
||||
| `check` | everything CI's `profile_tool` step runs; see [validation.md](validation.md#orca_profile_toolpy-check) |
|
||||
| `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 is `normalize` → `update-index` → `generate-id` →
|
||||
`check`. Each step feeds the next, so it is not interchangeable. 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` to one pass; they exclude each other, and
|
||||
passing neither writes both.
|
||||
- `--vendor` is repeatable and narrows **only what is written**: an id is a function of its own key
|
||||
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 are kept, and each pass touches only
|
||||
its one key line. 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`. `normalize` is the opposite by design (it
|
||||
rewrites whole files into canonical shape), which is why `check` demands it already be a no-op. A file
|
||||
committed with CRLF line endings changes on every line under `normalize`; read the diff before
|
||||
committing it.
|
||||
|
||||
Exit 1 from `generate-id` does not mean nothing was written: it writes every id it can and reports the
|
||||
rest, so read the diff before rerunning. 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 key `settings_id`, moving its value to `setting_id` only on an instantiated BBL
|
||||
preset that lacks one (everywhere else the old value is discarded and a fresh id minted);
|
||||
- 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, and roots of one filament resolving different
|
||||
`(filament_vendor, filament_type)` pairs.
|
||||
|
||||
**Does not fix: a preset that *inherits* a wrong `filament_id`.** This is check 2b (the label `docs/HLSD/filament_id.md` and the tool use), 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 library generic's id: wrong product
|
||||
```
|
||||
|
||||
The preset resolves *an* id, so `generate-id` neither inserts nor rewrites one, 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`, …) with its own `filament_vendor` and `filament_type`. No `fdm_filament_*` base
|
||||
carries a `filament_id`, so the filament now resolves none and `generate-id` mints it on the root.
|
||||
This is the product-root shape ([the three-part shape](filament-profiles.md#the-three-part-shape)).
|
||||
2. **Declare the tool-computed id on the preset itself.** Use the expected value `check` reports, or
|
||||
compute it with the function below; this is not a manually chosen id. First make sure the preset
|
||||
resolves the right `filament_vendor` and `filament_type`: 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`.
|
||||
`generate_preset_setting_id('<vendor folder>', '<type>', '<name>')` is the `setting_id` equivalent.
|
||||
|
||||
A vendor's tune of a generic that keeps the `Generic X` alias is not this case: inheriting the
|
||||
generic's id is correct there, because the product really is the library generic
|
||||
([filament-profiles.md](filament-profiles.md#the-two-most-common-contributions)).
|
||||
|
||||
## BBL's exception, precisely
|
||||
|
||||
The exception covers **`setting_id` assignment only**, keyed on the *folder* name `BBL`:
|
||||
|
||||
- The tool never mints or replaces a `setting_id` in `BBL/`: those are Bambu's own ids. 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
|
||||
unique across the tree, and BBL `filament_id`s are minted like everyone else's, as `OF…` ids.
|
||||
|
||||
## Ids other systems compose
|
||||
|
||||
No id from another system is the mint of a triple, so `check` rejects one used as a `filament_id` 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`. `blacklist.json` and
|
||||
`BBL/filament/filaments_color_codes.json` reference Bambu catalog ids by design. The rule is about
|
||||
`filament_id` and nothing else: every BBL `setting_id` starts with `G`, and that is Bambu's own
|
||||
preset id, not a leaked catalog id.
|
||||
- **Qidi's `QD_…`**: composed at runtime by the printer's filament box
|
||||
(`QD_<series>_<vendor>_<typeidx>`), not a preset id.
|
||||
- **`P` + 7 hex digits, and `"null"`**: what the app gives a *user*-created filament.
|
||||
|
||||
## Tests
|
||||
|
||||
`python3 -m unittest discover -s scripts/tests -t scripts` (`py -3 -m …` on Windows) runs the tool's
|
||||
unit tests. 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,232 +0,0 @@
|
||||
# Printer models and variants
|
||||
|
||||
Both live in `resources/profiles/<Vendor>/machine/`; models go in `machine_model_list`, variants and
|
||||
shared bases in `machine_list`. Every one of them is registered. A bundle may nest further subfolders
|
||||
under `machine/`, so recurse rather than globbing `machine/*.json`.
|
||||
|
||||
## `machine_model`: a record, not a config preset
|
||||
|
||||
The loader reads a fixed set of keys from a `machine_model` and stores only these (`version` and `url`
|
||||
are recognised 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**, a printer config key such as `default_bed_type` or a
|
||||
misspelling included, so a neighbour carrying a key is no evidence it does anything. Printer config options belong on the `machine` preset, never here. The loader drops a model
|
||||
silently if its index entry has no name or its `nozzle_diameter` yields no sizes; `check` also requires
|
||||
the file's own `name`.
|
||||
|
||||
```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` 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 a value starting with `SL` means SLA; everything else is FFF. Write `FFF`; `FGF` behaves as FFF. |
|
||||
| `nozzle_diameter` | `;`-separated string, one token per available size. Order is free (`0.4;0.2;0.6;0.8` puts the default first). This list is the authoritative set of legal `printer_variant` values. |
|
||||
| `default_materials` | `;`-separated filament **preset names**, not `,`; case-sensitive (`@System`); order is ignored. Used to preselect filaments in the setup wizard *and* to install a printer's filaments on first run, so a dangling entry costs a real user a filament. Every name must exist (`check` fails on a name matching no filament file, here or in `default_filament_profile`), and every variant of the model needs at least one entry compatible with it (`validate_system`). |
|
||||
| `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** (named by the
|
||||
vendor id). 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 files exist.** A missing `hotend_model` falls back to
|
||||
`resources/profiles/hotend.stl`; a missing `bed_model` makes the bed render as a generic custom bed, and a missing
|
||||
`bed_texture` renders no texture.
|
||||
Verify by hand, in exact case.
|
||||
|
||||
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. A missing cover degrades to a placeholder in both the wizard and the sidebar.
|
||||
|
||||
## `machine`: the 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`. `default_filament_profile` is optional; when written it
|
||||
is an array (`["Generic PLA @System"]`), while the model's `default_materials` is a `;`-separated
|
||||
string. Unlike a `machine_model`, a `machine` **is** a config preset, so a key belonging to another
|
||||
preset type is a reported error and is removed; a misspelled key is still dropped silently.
|
||||
|
||||
### `printer_model` and `printer_variant`
|
||||
|
||||
1. `printer_model` is non-empty and names a model of this bundle exactly.
|
||||
2. `printer_variant` is non-empty and an exact token of that model's `;`-separated `nozzle_diameter`
|
||||
list.
|
||||
3. For instantiated presets, when validating: 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 the set of `nozzle_diameter` values.
|
||||
|
||||
Rules 1 and 2 are loader-enforced: failing either discards 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 extruder**; `printer_variant` lists the **distinct** diameters
|
||||
joined with `+`: `["0.4","0.4","0.6","0.6"]` against `"0.4+0.6"` passes because the comparison is on
|
||||
sets.
|
||||
|
||||
Write `printer_variant` as a bare diameter matching the model's list, with no unit. The conventional
|
||||
values are `0.2`, `0.25`, `0.4`, `0.5`, `0.6`, `0.8` and `1.0`; a suffixed form (`0.4HF`, `0.4HS`) or
|
||||
the `+` form is legal under rule 3. A `printer_variant` is **not** required to be unique within a
|
||||
model: an IDEX model's normal, `COPY MODE` and `MIRROR MODE` presets can all be `0.4`.
|
||||
|
||||
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. A variant whose `printer_model` names a sibling model by mistake
|
||||
leaves its own model's size in exactly that state.
|
||||
|
||||
### Other keys
|
||||
|
||||
- `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, and `check` does not resolve the name. Check the exact
|
||||
default reference yourself.
|
||||
- `default_filament_profile` is an **array**, one name per element. Entry 0 is the filament preselected
|
||||
when the printer is chosen; entry *i* is the preferred replacement when filament *i* is incompatible,
|
||||
and any listed name outranks an unlisted one. The validator checks every entry. The list of a
|
||||
printer's filaments is the model's `default_materials`: a new filament goes into `default_materials`;
|
||||
put it first in `default_filament_profile` only if it should become the preselected one.
|
||||
- `printable_area` is an array of `"XxY"` strings: four points for a rectangle; a delta or other circular bed
|
||||
is a polygon with one point per segment.
|
||||
- A non-BBL printer shows the plate selector only with `support_multi_bed_types` `"1"`; otherwise it
|
||||
uses its `default_bed_type` (a plate name such as `"Textured PEI Plate"`; High Temp Plate when unset).
|
||||
Filaments still set every plate
|
||||
([twelve keys](filament-profiles.md#bed-temperature-is-twelve-keys-not-one)).
|
||||
- `gcode_flavor` is usually set once in the base; the common values are `klipper`, `marlin`, `marlin2`
|
||||
and `reprapfirmware`.
|
||||
- `printer_settings_id` does nothing in a preset file: the app replaces it with the selected preset's
|
||||
name before slicing. Omit it, and do not copy it when cloning a bundle.
|
||||
- `min_layer_height` / `max_layer_height` are **machine** keys (one per extruder), never process keys.
|
||||
|
||||
## Bases
|
||||
|
||||
The conventional machine root is a base named `fdm_machine_common`, with `fdm_klipper_common` on top
|
||||
of it for Klipper printers. Which values a hardware
|
||||
family or model base holds, and when adding one pays off, is in [shared-bases.md](shared-bases.md).
|
||||
|
||||
**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
|
||||
([naming.md](naming.md)).
|
||||
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. Add the variants to the filaments they should offer, and set the model's `default_materials` so every
|
||||
variant has a compatible entry.
|
||||
5. Register everything (`update-index`), bump the version, run the id tool, validate: the
|
||||
[authoring workflow](../SKILL.md#creating-or-modifying-a-profile).
|
||||
|
||||
## 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
|
||||
(a smaller diff, but the sibling's edits now reach this file too). Follow the bundle.
|
||||
3. Override what actually changes with the 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)), and
|
||||
extend the filaments' `compatible_printers` so at least one `default_materials` entry covers the new
|
||||
variant.
|
||||
5. Register both, bump the version, run the id tool, validate.
|
||||
|
||||
## Multi-extruder, IDEX and tool changers
|
||||
|
||||
Per-extruder vectors hold one value per extruder (`len(nozzle_diameter)`), and a wrong length raises no
|
||||
error: a short vector acts as padded with its **first** value, not the last (`extruder_offset`
|
||||
`["0x0","50x0"]` on a 4-extruder machine reads as `0x0, 50x0, 0x0, 0x0`), and entries beyond the
|
||||
extruder count are never read.
|
||||
|
||||
Note the two sizing families. The plain per-extruder keys (`printer_extruder_options`: `extruder_type`,
|
||||
`nozzle_diameter`, `default_nozzle_volume_type`, `extruder_printable_height`, `min_layer_height`,
|
||||
`max_layer_height`, …, given in full with the [key sets](extruder-variants.md#the-four-key-sets), plus
|
||||
`extruder_offset` and `extruder_colour`) hold one value per extruder. The variant sets
|
||||
(`retraction_length`, `z_hop`, `wipe`, `nozzle_type`, the `machine_max_*` limits at stride 2;
|
||||
[the full lists](extruder-variants.md#the-four-key-sets)) are sized to the variant length:
|
||||
`len(printer_extruder_variant)`, or one variant per extruder when the resolved preset writes no layout,
|
||||
since `extruder_variant_list` defaults to one `Direct Drive Standard` per extruder
|
||||
([widths](extruder-variants.md#widths)). `extruders_count` is a printer-tab field, not a preset key; the
|
||||
loader drops it.
|
||||
|
||||
- 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 sets to the variant
|
||||
length × stride, one value per variant (per extruder when there is no layout; a (normal, silent)
|
||||
pair for the `machine_max_*` limits), even where the values are the same; the loader keeps only the first value of a list-less printer's variant arrays, so declare
|
||||
the layout when the extruders differ. A single `["0x0"]` `extruder_offset` on a dual or
|
||||
multi-extruder machine pads every extruder to the same offset, so the offset never applies.
|
||||
- Overriding `nozzle_diameter` to a different count without restating every per-extruder vector is the
|
||||
other half of the trap: a 4-extruder preset on a 5-extruder base inherits 5-entry vectors against 4
|
||||
extruders.
|
||||
The reverse is silent too: a base is stored resized to its own `printer_extruder_variant` (one variant
|
||||
when it writes none, whatever its extruder count), so a wider variant array on it reaches the children
|
||||
as its first value padded ([composition](extruder-variants.md#padding-truncation-and-composition)).
|
||||
`check` does not judge the base on its own; where its extruders need different values, declare the
|
||||
layout on the base.
|
||||
|
||||
Structure to copy: `Custom/machine/fdm_toolchanger_common.json` + `Custom/machine/MyToolChanger 0.4
|
||||
nozzle.json` (a minimal variant on a base that gives the per-extruder vectors five entries), and
|
||||
`Ratrig/machine/RatRig V-Core 4 IDEX 300 0.4 nozzle.json` for IDEX. Take the structure from them and
|
||||
the widths from the [sizing equation](extruder-variants.md#sizing-equation). Both are list-less, so
|
||||
every variant array holds one value per extruder and the loader keeps only the first: fine while every
|
||||
extruder shares the same retraction and limits. Add the extruder-variant layout
|
||||
(`extruder_variant_list`, `printer_extruder_variant` / `printer_extruder_id`,
|
||||
`default_nozzle_volume_type`) when the hardware has swappable nozzle volume types or mixed extruder
|
||||
types, or as soon as one extruder needs its own value in a variant key; how to author it, and the
|
||||
matching process and filament variants, is in [extruder-variants.md](extruder-variants.md).
|
||||
(`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`. Each is one string with
|
||||
embedded `\n`. Never split G-code into a JSON array of lines: the loader joins array elements with `,`
|
||||
into a single line (a one-element array is equivalent to the string): a two-element
|
||||
`machine_start_gcode` becomes one line, `PRINT_START …,SET_PRESSURE_ADVANCE ADVANCE=0.046`.
|
||||
Conditionals are `{if …}` / `{elsif …}` / `{else}` / `{endif}`.
|
||||
|
||||
Placeholder errors only surface when the G-code 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); a printer whose output has no
|
||||
`CP TOOLCHANGE START` fails it, because its `change_filament_gcode` never expanded.
|
||||
@@ -1,94 +0,0 @@
|
||||
# Preset naming
|
||||
|
||||
A preset's `name` is its identity, not decoration. The index registers it; `inherits`,
|
||||
`compatible_printers`, `printer_model` and the `default_*` keys reference it by the exact,
|
||||
case-sensitive string; `renamed_from` migrates it; `setting_id` and `filament_id` hash it
|
||||
([ids.md](ids.md)). Treat a name change as an identity change that needs
|
||||
[migration](vendor-bundle.md#renamed_from), not a relabel. Which part of each name the loader acts on
|
||||
is the table in [SKILL.md](../SKILL.md#names); this page holds the conventions.
|
||||
|
||||
## `machine_model`
|
||||
|
||||
`<Model>`, vendor-prefixed: `Bambu Lab X1 Carbon`, `Creality K1`, `Prusa CORE One`. Each variant's
|
||||
`printer_model` names it verbatim (a mismatch discards the bundle), and its index entry equals the
|
||||
file's `name`. It is also the stem of `<Model>_cover.png` and, by convention, of the bed assets
|
||||
(`<Model>_buildplate_model.stl`).
|
||||
|
||||
## `machine` (variant)
|
||||
|
||||
`<Model> <nozzle> nozzle` is near-universal (`Bambu Lab X1 Carbon 0.4 nozzle`). Casing varies
|
||||
(`nozzle` / `Nozzle`): match the bundle, not this page. A variant that is not nozzle-specific (a
|
||||
special toolhead, a multi-material build, IDEX copy and mirror modes such as
|
||||
`<Model> COPY MODE (0.4 nozzle)`) may drop or reshape the suffix; it is still an exact reference. `printer_variant`
|
||||
holds the nozzle token: `0.4`, a suffixed `0.4HF`, or `0.4+0.6` for mixed nozzles
|
||||
([rules](machine-profiles.md#printer_model-and-printer_variant)).
|
||||
|
||||
## `process`
|
||||
|
||||
`<layer height>mm <quality> @<target>`. The quality word stays before `@` and the printer target after
|
||||
it: a printer model in the quality position leaves the tier undescribed. The `@<target>` is a label,
|
||||
and need not equal any variant name; compatibility comes from `compatible_printers` or the
|
||||
condition. The quality ladder and per-nozzle labels are in
|
||||
[process-profiles.md](process-profiles.md#naming).
|
||||
|
||||
## `filament`
|
||||
|
||||
`<Product> @<target>`. The product half, up to the first `@` and right-trimmed, is the **alias**:
|
||||
shadowing matches on it and `filament_id` hashes it. The target half is a label, except for the
|
||||
reserved forms:
|
||||
|
||||
- `@base`: a non-instantiated product root. Convention only; a base is really
|
||||
`instantiation: "false"` without `setting_id`
|
||||
([the three-part shape](filament-profiles.md#the-three-part-shape)).
|
||||
- `@System`: the OrcaFilamentLibrary selectable shim, and the convention for an all-printer product
|
||||
(`<Product> @System`, empty `compatible_printers`). Not enforced, so a deviation is worth a review
|
||||
comment. The literal `Generic <mat> @System` is also load-bearing for project recovery
|
||||
([alias shadowing](filament-profiles.md#alias-shadowing)).
|
||||
- Printer tunes. BBL's shape is the reference: `@<Vendor>` (vendor-wide), `@<Vendor> <Model>` (one
|
||||
model), `@<Vendor> <Model> <nozzle> nozzle` (one variant). Other vendors differ: a bare model
|
||||
(`QIDI ABS-GF@Q2-Series`), a printer serial, Creality's `@<Model>-all`. Judge specificity from
|
||||
`compatible_printers`, never from the name
|
||||
([one variant, one profile](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product)).
|
||||
- No colour in the product name: `<Product> <Colour>` presets are not authored; colour is chosen at
|
||||
runtime ([colour](filament-profiles.md#colour-is-a-runtime-property)).
|
||||
|
||||
## Bases
|
||||
|
||||
| Type | Base names |
|
||||
| --- | --- |
|
||||
| `machine` | `fdm_machine_common`, `fdm_<vendor>_common`, `fdm_klipper_common`, or an established machine-family base |
|
||||
| `process` | `fdm_process_*`: shared roots and per-layer-height or per-nozzle bases such as `fdm_process_single_0.20` or `fdm_process_<vendor>_<lh>_nozzle_<n>` |
|
||||
| `filament` | `fdm_filament_*` material roots, `<Product> @base` product roots |
|
||||
|
||||
There is no leading-underscore convention. Base names repeat across bundles by design:
|
||||
every bundle may have its own `fdm_process_common`, and a product root such as `Fiberon PA6-CF @base`
|
||||
can exist in both the library and a vendor. Investigate a newly authored base that kept an unrelated
|
||||
selectable preset's name from a copy.
|
||||
|
||||
## Uniqueness
|
||||
|
||||
- Type + name is unique within a bundle, indexed or not; `check` enforces it.
|
||||
- `machine_model` names are unique across the whole tree; `check` enforces it even with `--vendor`.
|
||||
Printer-type lookup matches `printer_model` against every vendor's models and takes the first, so a
|
||||
duplicate makes it depend on vendor order.
|
||||
- At load, two selectable presets with one name discard the bundle, and a duplicate across vendors
|
||||
is a validator error. Two bases with one name, or a base and a selectable preset, load silently and
|
||||
the first in the index wins; an unindexed twin is therefore one `sub_path` edit away from becoming
|
||||
the parent every child resolves to.
|
||||
|
||||
## Filenames and paths
|
||||
|
||||
The loader keys off `name`, and a filename that disagrees usually still loads, but keep the filename
|
||||
equal to the `name` and to the index `sub_path`. Match the exact case of every `sub_path` and asset
|
||||
filename: Linux filesystems distinguish case even when a macOS or Windows checkout does not, and
|
||||
preset-name references are case-sensitive on every platform. Avoid Windows-invalid characters
|
||||
(`< > : " | ? *`), reserved device names such as `CON` and `NUL` (with any extension), and trailing
|
||||
spaces or dots in a path component; a space right before `.json` is not a trailing space.
|
||||
|
||||
## Checking names
|
||||
|
||||
Check the `name` of every newly added profile, and every intentional rename, against its type and role
|
||||
(model, selectable preset or base). `check` does not enforce the shapes on this page: inspect the
|
||||
added or renamed presets in the diff, use neighbouring names as context, and follow the bundle's
|
||||
established style where the conventions allow variation. Preserve shipped names during ordinary
|
||||
tuning; renaming a shipped selectable preset needs `renamed_from`.
|
||||
@@ -1,155 +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>"` is near-universal, so match it: the quality word before `@`,
|
||||
the printer label after it ([naming.md](naming.md#process)).
|
||||
|
||||
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.
|
||||
Newer BBL printers add High Quality, Balanced Quality and Strength tiers. Match neighbouring names rather
|
||||
than renaming shipped tiers to fit the table. On a model with several nozzles, processes for the other
|
||||
nozzles usually carry the nozzle in the label (`0.30mm Standard @BBL X1C 0.6 nozzle`); follow the bundle.
|
||||
|
||||
The `@target` is a human label, not a reference: it need not equal any printer variant name.
|
||||
Compatibility comes from the resolved list or condition, not this label.
|
||||
|
||||
## Shape
|
||||
|
||||
A selectable leaf has `type`, `setting_id`, `name` and `instantiation`, normally `inherits` and
|
||||
`from`, plus compatibility; its slicing keys, `layer_height` included, normally come from its bases. A
|
||||
base has `type`, `name`, `instantiation`, `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` and
|
||||
`compatible_printers`, and the per-nozzle base holds the layer height and all eight line widths.
|
||||
[shared-bases.md](shared-bases.md#levels) says which level each process setting belongs to.
|
||||
|
||||
BBL's *layering* is a model too (every leaf inherits a base, names its printers directly and holds no
|
||||
layer height of its own), but not its content: its leaves carry multi-variant `print_extruder_variant`
|
||||
arrays that no single-variant vendor needs ([extruder-variants.md](extruder-variants.md#process)).
|
||||
|
||||
A bundle has its own `fdm_process_common` as the inherits-less root, since a process inherits only
|
||||
inside its bundle; starting a new bundle's from another vendor's copy is fine.
|
||||
|
||||
Beware leaf-inherits-leaf: a bundle may chain selectable processes several levels deep, so editing one
|
||||
silently changes others. Check a leaf's children before editing it.
|
||||
|
||||
## Compatibility
|
||||
|
||||
A leaf sets `compatible_printers` directly, inherits it from a base, or falls 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**. 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.
|
||||
- A regex in a condition must match the **whole** string, so wrap the keyword in `.*`; `.` also spans
|
||||
the newlines inside `printer_notes`.
|
||||
- A `printer_notes` keyword that prefixes another model's keyword matches both. Guard it with a
|
||||
character class after the keyword, and combine terms with `and`:
|
||||
|
||||
```
|
||||
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`.
|
||||
|
||||
A leaf listing a whole model family is where a newly added printer is usually forgotten.
|
||||
|
||||
## Values 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 `min_layer_height` / `max_layer_height` |
|
||||
| print speeds | consistent with flow limits and hardware tuning |
|
||||
| shell layers, wall loops, accelerations, support Z distances | preserve the intended thickness, motion and support behaviour |
|
||||
|
||||
**A common starting pattern is line width = 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 or automatic widths, and validate their resolved
|
||||
values.
|
||||
|
||||
`min_layer_height` and `max_layer_height` are machine keys; no process file sets them.
|
||||
|
||||
### Slicing limits
|
||||
|
||||
Slicing rejects a process that breaks one of these (the message in italics):
|
||||
|
||||
1. `initial_layer_print_height` ≤ the smallest `nozzle_diameter` (with a raft, the nozzle of the raft's
|
||||
first-layer extruder).
|
||||
2. `layer_height` ≤ the smallest `nozzle_diameter`: *"Layer height cannot exceed nozzle diameter."*
|
||||
3. `line_width` and the seven per-region widths (inner and outer wall, sparse infill, internal solid
|
||||
infill, top surface, skin, skeleton) > `layer_height`: *"Line width too small"*.
|
||||
`support_line_width` is checked only when the object has support or a raft;
|
||||
`initial_layer_line_width` is never checked. A width that resolves to 0 (automatic) is skipped.
|
||||
4. Every width ≤ 5 × the largest `nozzle_diameter`: *"Line width too large"*.
|
||||
|
||||
Two further rules cover `bridge_line_width`: it must not exceed the nozzle diameter, and must exceed
|
||||
`layer_height` unless `thick_bridges` and `thick_internal_bridges` are both on. The slice 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, the five `setting_id` rules (present on
|
||||
selectable presets, absent from bases, equal to the formula outside `BBL/`, unique across the tree, and
|
||||
no misspelled key `settings_id`), and the variant arrays: every array of `print_options_with_variant`
|
||||
exactly `variant length × stride` wide in each selectable process that writes it, and a written
|
||||
`print_extruder_id` one entry per variant
|
||||
([variant arrays](validation.md#variant-arrays)). `compatible_printers` presence is checked for
|
||||
**filaments only**.
|
||||
|
||||
The loader derives a missing `setting_id` on the fly, so the validator accepts 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**, 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 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 reported only through a
|
||||
selectable child that inherits it unchanged; it goes unreported when every child overrides the list,
|
||||
or when the base has no instantiated children.
|
||||
- Nothing flags an orphan base that nothing inherits, 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 bundle's existing ladder.
|
||||
2. If the bundle 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 and validate: the
|
||||
[authoring workflow](../SKILL.md#creating-or-modifying-a-profile).
|
||||
5. Slice this process explicitly with its intended printer
|
||||
([on a copy of the tree](validation.md#checking-a-copy-of-the-tree)); 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,277 +0,0 @@
|
||||
# Reviewing a profile change
|
||||
|
||||
Run `./scripts/check_profile.sh` on the applied diff first ([validation.md](validation.md) says what CI
|
||||
runs), then work through the items below: delivery, identity and backward compatibility first, then the
|
||||
affected preset types. The table lists the gaps CI cannot see, so only a reviewer catches them.
|
||||
|
||||
| Not checked by CI | Consequence |
|
||||
| --- | --- |
|
||||
| The `version` bump | The change never reaches an upgrading user; an absent `version` hides the vendor from the setup wizard |
|
||||
| A misspelled setting key | The 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` / cover pointing at a missing asset | A missing bed model renders a generic custom bed and a missing texture renders none, the hotend falls back to the generic model, the cover shows a placeholder |
|
||||
| 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 a base whose children all override it (or that has no instantiated children) | The reference check walks resolved selectable presets, so it reports a base's list only through a child that inherits it unchanged (a bad `inherits` in a base *is* caught) |
|
||||
| A base nothing inherits | Dead weight, usually the leftover of an unfinished nozzle addition |
|
||||
| A `renamed_from` whose old name is still a live preset | The redirect is inert while a live preset carries that name |
|
||||
| A preset differentiated only by colour, or an all-printer library preset without `@System` | Per-colour presets split one product across several ids and the selector fills with near-duplicates; CI stays green |
|
||||
| Plate temperatures for plates the printer has | The user's plate reads an unset or inherited temperature |
|
||||
| Per-extruder vector length on a multi-extruder printer | Silently padded (with the **first** value) or truncated |
|
||||
| A new variant appended instead of inserted at its variant index, a variant array widened on a base with a shorter list, or a variant a filament/process lacks | Values shift onto the wrong extruder, are cut before any child inherits them, or resolve to the first variant: High Flow silently gets Standard values |
|
||||
| A name that ignores its type's convention: a printer model in a `process` quality position, or an unrelated target label or base name left in a copied preset | The selector misrepresents the preset's quality or intended printer |
|
||||
| Values: temperatures, speeds, widths, pressure advance | A wrong value prints wrong while CI stays green |
|
||||
|
||||
## 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 the app reinstalls a bundled profile set only when its version is
|
||||
newer than the installed one: without a bump the change reaches neither an upgrading user nor the
|
||||
author's own running app. Without any `version` the vendor vanishes from the setup wizard, and neither
|
||||
`check` nor the validator reports it.
|
||||
|
||||
## 2. Was the index rebuilt, and does the diff contain only this change?
|
||||
|
||||
`check` 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
|
||||
([what normalize changes](validation.md#normalize-and-update-index-are-part-of-the-check)). Check that
|
||||
the keys it removed were meant to go.
|
||||
|
||||
Index order is dependency order, not alphabetical: parents and include templates before the presets
|
||||
that use them, then, in name order, the entries that neither depend on nor are depended on by another
|
||||
entry of their own list (such as a leaf filament whose only parent is in the library), and any entry on
|
||||
a dependency cycle. Judge a hand-placed entry only by
|
||||
`update-index --dry-run`: if it reports nothing to rebuild, the position is not a finding.
|
||||
|
||||
*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 renamed alias, 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 selectable preset
|
||||
removes the name from the preset collection. It needs `renamed_from` on a selectable successor
|
||||
([renamed_from](vendor-bundle.md#renamed_from)), 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 with `renamed_from`. 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 ([SKILL.md rule 9](../SKILL.md#rules); the resolved-vs-own-key trap
|
||||
is in [filament-profiles.md](filament-profiles.md#compatible_printers)). A `machine_model` name instead
|
||||
of a variant name is the usual mistake: `check` passes it, the validator reports
|
||||
`references unknown compatible_printers`. Watch for a nozzle-specific tune 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.
|
||||
|
||||
Two presets of one product (`filament_id`) must not share a variant. Resolve it by specificity: move the
|
||||
variant to the most specific preset and remove it from the more general ones, which is preferred over
|
||||
deleting a profile. Then repoint the machine's `default_filament_profile` and the model's
|
||||
`default_materials` at the profile that now covers it. See
|
||||
[one variant, one profile](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product).
|
||||
|
||||
*Why:* real shipped bugs twice (`b7b3418baf` filaments "showing up everywhere", `ff83aa41ef` duplicate
|
||||
Flashforge entries). The Python `check` passes on an overlap; only the full `check_profile.sh`
|
||||
(`validate_system`) reports `Ambiguous AMS filament match`.
|
||||
|
||||
## 6. One product, one all-printer preset; colour is not a preset
|
||||
|
||||
No presets that differ only by colour: `filament_id` identifies a product, and the colour comes from the
|
||||
spool at runtime. An all-printer library product is a `<Product> @System` shim with an empty
|
||||
`compatible_printers` ([colour](filament-profiles.md#colour-is-a-runtime-property)).
|
||||
|
||||
*Why:* per-colour presets pass every check, so this is a review call.
|
||||
|
||||
## 7. Model ↔ variant ↔ process consistency
|
||||
|
||||
- A 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. Every size in the model's list has a
|
||||
variant (unchecked).
|
||||
- `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.
|
||||
- Each variant of the model has at least one compatible entry in the model's `default_materials`.
|
||||
|
||||
*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,
|
||||
so another compatible process can conceal a bad reference. Inspect it even after a pass.
|
||||
|
||||
## 8. Types and spellings
|
||||
|
||||
Every value a string or an array of strings; `filament_type` an array; `instantiation` the string
|
||||
`"true"` / `"false"`; custom G-code one string, never an array of lines ([SKILL.md rule 5](../SKILL.md#rules)).
|
||||
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 6](../SKILL.md#rules)), the single most common way a profile
|
||||
edit does nothing while CI stays green.
|
||||
|
||||
## 9. Blast radius of a base edit
|
||||
|
||||
A change to `fdm_*_common.json` or any other base 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: a bundle may chain leaf-inherits-leaf several levels deep. A
|
||||
newly added base that nothing inherits is dead weight, and a dangling
|
||||
`compatible_printers` inside a base is reported only through a child that inherits it unchanged. A diff
|
||||
that only moves values between presets and bases must leave every selectable preset loading what it
|
||||
loaded: ask for the `compare` result ([shared bases](shared-bases.md#nothing-loads-differently)), and
|
||||
check each new base against the [balance rules](shared-bases.md#balance).
|
||||
|
||||
## 10. Do the numbers make sense for the nozzle and material?
|
||||
|
||||
Check resolved widths and layer heights against the nozzle, temperatures against the material (PLA
|
||||
values under an ASA name print wrong), and flow limits / pressure advance against the actual hardware
|
||||
and material. The patterns in [process-profiles.md](process-profiles.md#values-to-review-per-nozzle)
|
||||
are examples, not mandatory values; [filament-profiles.md](filament-profiles.md#tuning-per-nozzle-and-per-variant)
|
||||
explains what to revisit for a nozzle change. A cloned preset's unchanged volumetric speed needs
|
||||
particular scrutiny.
|
||||
|
||||
Settings tuned for real hardware cannot be verified by reading the diff. Say so rather than approving
|
||||
numbers nobody measured.
|
||||
|
||||
## 11. Plate temperatures
|
||||
|
||||
A filament sets the plate temperature for every plate the printer plausibly has, as its siblings do;
|
||||
`textured_cool_plate_temp` is the one most often forgotten
|
||||
([twelve keys](filament-profiles.md#bed-temperature-is-twelve-keys-not-one)).
|
||||
|
||||
## 12. Asset references (not checked anywhere)
|
||||
|
||||
`bed_model`, `bed_texture`, `hotend_model` and `<Model>_cover.png` exist under
|
||||
`resources/profiles/<vendor folder>/`, in exact case. Nothing checks them.
|
||||
|
||||
## 13. `default_materials` and `default_filament_profile` (checked by CI)
|
||||
|
||||
`check` fails on a `default_materials` / `default_filament_profile` name that matches no filament file,
|
||||
and `validate_system` on a variant with no compatible system filament in `default_materials`, so a
|
||||
dangling entry no longer reaches review. When compatibility moves between profiles of a product, the
|
||||
machine's `default_filament_profile` and the model's `default_materials` must be repointed at the most
|
||||
specific profile that still covers the variant, dropping generic entries that no longer apply; the same
|
||||
[specificity rule](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product) applies
|
||||
when adding or fixing defaults. Scope the run while working on one vendor:
|
||||
|
||||
```bash
|
||||
python3 scripts/orca_profile_tool.py check --vendor "<Vendor>" # py -3 on Windows
|
||||
```
|
||||
|
||||
## 14. Per-extruder vectors (not checked) and variant arrays (widths and layout checked)
|
||||
|
||||
One entry per extruder for the plain per-extruder vectors; the variant sets are sized to the variant
|
||||
length, `len(printer_extruder_variant)` or one per extruder when the resolved preset has no layout (a
|
||||
per-extruder difference in those keys still needs the layout, since the loader keeps only the
|
||||
first value of a list-less printer's arrays). 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).
|
||||
|
||||
`check` reports a variant array of any type that is not exactly the `variant length × stride` width
|
||||
of the selectable preset that writes it, one value included, as an error whatever the values;
|
||||
`check --strict` also reports one that reaches a selectable preset at another width. A base is not
|
||||
judged on its own. On a machine it
|
||||
also reports layout keys that disagree: `printer_extruder_variant` / `printer_extruder_id` not the
|
||||
flattening of `extruder_variant_list`, a variant without its extruder's `extruder_type` prefix, a
|
||||
`default_nozzle_volume_type` the extruder does not list, a list of several variants without the pair, a
|
||||
pair without the list that puts several variants on one extruder. On a machine or a process, an id
|
||||
array, written or inherited, whose length differs from its variant list is an error; the id and
|
||||
layout rules judge the composed preset without `--strict`. Two layout findings are warnings: a
|
||||
pair without the list that `single_extruder_multi_material` off would replace at load, and a process
|
||||
without `print_extruder_id` whose variants repeat. The full rules are in
|
||||
[variant arrays](validation.md#variant-arrays). `check_variant_names` separately holds every variant
|
||||
string, `extruder_type`, `nozzle_volume_type` and `default_nozzle_volume_type` to the engine's enums
|
||||
in every bundle, BBL included, failing on a dead variant, a legacy spelling and a variant list that
|
||||
names one variant twice ([variant names](validation.md#variant-names)).
|
||||
|
||||
On a multi-variant printer, still check by hand:
|
||||
|
||||
- that a new variant's values sit at its variant index (machine limits as a (normal, silent) pair at
|
||||
`2 × index`) in every file of the chain that restates the key, `include` templates included;
|
||||
- that each process lists every (extruder id, variant) pair its printers can select;
|
||||
- that no array was widened on a base with a shorter variant list: the loader cuts it before the
|
||||
children inherit it ([composition](extruder-variants.md#padding-truncation-and-composition)), and
|
||||
`check`, even `--strict`, composes without that cut, so it passes;
|
||||
- that only keys in [the four sets](extruder-variants.md#the-four-key-sets) carry per-variant values;
|
||||
- that the High Flow variants carry measured values rather than copies
|
||||
([checking and testing](extruder-variants.md#checking-and-testing)).
|
||||
|
||||
## 15. 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
|
||||
([on a copy of the tree](validation.md#checking-a-copy-of-the-tree)).
|
||||
|
||||
## 16. Do the preset names follow the conventions?
|
||||
|
||||
Check **every newly added profile and intentional name change**, including models and bases, against
|
||||
[the naming conventions](naming.md#checking-names): no printer model in a process quality position, no
|
||||
target label or base name left over from a copied preset, the bundle's established style. Preserve
|
||||
shipped names during ordinary tuning; renaming a shipped selectable preset requires the migration in
|
||||
item 4.
|
||||
|
||||
*Why:* CI checks name uniqueness, but does not enforce the naming conventions. Catch naming mistakes
|
||||
before the names ship and existing projects depend on them.
|
||||
|
||||
## 17. Cross-platform filenames and paths (not checked)
|
||||
|
||||
Check for Windows-invalid characters, reserved device names, trailing path-component spaces or dots,
|
||||
and case mismatches between `sub_path` or asset references and the files on disk. See
|
||||
[filenames and paths](naming.md#filenames-and-paths).
|
||||
|
||||
## 18. Housekeeping worth a nit, not a block
|
||||
|
||||
`"from"` other than `"system"` (the bundle loader ignores it, though loading the file as a CLI config
|
||||
rejects anything but `system` / `user` / `User`), `printer_settings_id` copied from another vendor,
|
||||
redundant overrides that restate the parent's value, siblings that each repeat a value their base could
|
||||
hold ([shared bases](shared-bases.md)), and a filename that disagrees with the preset's
|
||||
`name` (the loader keys off `name`).
|
||||
|
||||
---
|
||||
|
||||
## Reporting the review
|
||||
|
||||
A finding is **one defect**: its file (or quoted lines), 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. Say which findings `check` or the validator reports and which
|
||||
only a reader catches: a missing version bump, a misspelled key and wrong temperatures pass CI, so a
|
||||
contributor who only reruns the tools fixes what CI flags and resubmits the rest.
|
||||
|
||||
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, existing users lose a preset, or a value prints wrong while CI stays green (PLA temperatures under an ASA name; a misspelled key whose intended value differs from the inherited one) |
|
||||
| minor | wrong but working: redundant or dead keys that change nothing, `from`, naming |
|
||||
|
||||
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. End with a
|
||||
verdict: can it merge as it stands?
|
||||
@@ -1,254 +0,0 @@
|
||||
# Shared bases
|
||||
|
||||
Use this when drafting several presets at once, after `fix-variant`, or when sibling presets repeat the
|
||||
same values. Each value is written once, on the base of the group it is true for, and a selectable
|
||||
preset holds its identity and what makes it different. The work has two halves: choosing the groups,
|
||||
which is judgment, and moving the values, which must leave every preset loading exactly what it loaded
|
||||
before.
|
||||
|
||||
## When
|
||||
|
||||
- **Drafting** a printer family, a quality ladder or a product line: place each setting at its
|
||||
[level](#levels) before writing any preset, then write the presets as overrides
|
||||
([Rule 7](../SKILL.md#rules)).
|
||||
- **After `fix-variant`.** It resizes each array in the selectable preset that writes it and never
|
||||
moves or deletes a value ([variant arrays](validation.md#variant-arrays)). A family whose presets all
|
||||
wrote one value now repeats the widened array in every preset, and a preset that restated what it
|
||||
inherits now restates it wider.
|
||||
- **Converting full copies**: a machine that inherits nothing, or a filament that restates fifty-plus
|
||||
keys, is the style to move away from ([filament style](filament-profiles.md#style)).
|
||||
|
||||
Out of scope:
|
||||
|
||||
- **BBL.** Its profiles are synced from BambuStudio: a restructure is overwritten by the next sync and
|
||||
makes every later sync diff unreadable. Fix BBL values in the file that holds them.
|
||||
- **OrcaFilamentLibrary bases for one vendor's values.** A library base reaches every bundle's
|
||||
filaments. Put a vendor's shared values on a base in its own bundle (a product `@base`, or a vendor
|
||||
base that inherits the library's), and change a library base only for a value true of every filament
|
||||
below it in every bundle.
|
||||
- **Families the change does not touch.** Restructure the presets you are already changing; a
|
||||
bundle-wide pass is a change of its own. Commit a restructure without any value change, so `compare`
|
||||
alone verifies it.
|
||||
|
||||
## Nothing loads differently
|
||||
|
||||
A restructure changes where values are written, never what a selectable preset loads. Presets keep
|
||||
their `name` and `setting_id`, and user presets and projects refer to a system preset by name and store
|
||||
their own changes against what it loads, so an unchanged load changes nothing for users. Prove it with
|
||||
the bundled helper, from the repository root (every subcommand takes `--profiles <dir>` to work on a
|
||||
copy of the tree):
|
||||
|
||||
```bash
|
||||
python3 .claude/skills/orca-profiles/scripts/shared_settings.py snapshot <before.json> # before any edit
|
||||
python3 .claude/skills/orca-profiles/scripts/shared_settings.py compare <before.json> # after: "0 difference(s)", exit 0
|
||||
```
|
||||
|
||||
`snapshot` records every selectable preset of every bundle as the loader stores it: the parent's stored
|
||||
config, each `include` at the width its file wrote, the preset's own keys, then every variant array
|
||||
resized to the preset's own variant list ([composition](vendor-bundle.md#inherits-and-include)). It
|
||||
models the step `check` does not see: a base is stored after its own resize, so an array wider than a
|
||||
base's list reaches its children cut. It also leaves out what the loader reads from each file and never
|
||||
hands down (`name`, `type`, `from`, `instantiation`, `inherits`, `include`, `setting_id`,
|
||||
`renamed_from`, `description`, `version`, `url`, `is_custom_defined`), so those keys never move to a
|
||||
base. Nor does it record `print_settings_id`, `printer_settings_id` or `filament_settings_id`: the app
|
||||
replaces them with the selected presets' names before slicing, so a file's value never counts. Delete
|
||||
them from the presets you restructure rather than moving them to a base.
|
||||
|
||||
It does not know the built-in defaults, so `compare` lists a key written on one side only (no file of
|
||||
the preset's chain writes it on the other) apart from the differences, and exits 1 for either. A
|
||||
difference is a value the edit changed: undo it, or make it a separate, deliberate change. A one-sided
|
||||
key is no change only when its written value is the option's default in `PrintConfig.cpp`: confirm
|
||||
each, as when a preset that loaded the default by leaving a key out must now write it
|
||||
([Balance 5](#balance)). A value equal to the default needs writing nowhere when no base above writes
|
||||
another: delete it from the presets rather than moving it, and confirm the one-sided keys.
|
||||
|
||||
## Levels
|
||||
|
||||
A base stands for a level of the vendor's catalogue, and a setting lives at the level that determines
|
||||
it. The test for a shared value: if it had to change for one preset of the group, should it change for
|
||||
all of them? If yes, it is the group's and goes on the group's base. A value that is only equal today
|
||||
(two unrelated printers with the same acceleration) stays in each preset: a base built on coincidence
|
||||
is later edited for one preset and silently changes the others. For a default with exceptions
|
||||
([Balance 5](#balance)), ask the question of the presets that inherit the default.
|
||||
|
||||
The tables give each setting's usual level; the test decides for a given bundle: where each toolhead
|
||||
(Bowden or Direct Drive) has its own default filament, `default_filament_profile` follows the
|
||||
toolhead, not the nozzle. Levels run
|
||||
coarse to fine, and a chain need not visit every level: each preset or base inherits the next coarser
|
||||
level that has a base.
|
||||
|
||||
| Machine level | Base | Settings it determines |
|
||||
| --- | --- | --- |
|
||||
| Vendor | `fdm_machine_common`, `fdm_<vendor>_common` | the vendor's defaults for every printer |
|
||||
| Firmware | `fdm_klipper_common`, `fdm_marlin_common` | `gcode_flavor`, G-code that calls the firmware's macros (layer change, pause, filament change), `host_type`, `print_host`, the thumbnail format |
|
||||
| Hardware family: models that share a frame, motion system, toolhead or extruder layout | `fdm_<vendor>_<family>_common` (`fdm_qidi_x3_common`, `fdm_machine_eryone_ER20_common`) | the `machine_max_*` limits, `extruder_clearance_*`, the toolhead's retraction where every nozzle shares it, `z_hop` and wipe, fitted hardware such as `auxiliary_fan`, the extruder count and per-extruder vectors, the variant layout ([Printer rule 2](extruder-variants.md#printer-machine)) |
|
||||
| Model: one `machine_model`, which for an IDEX printer includes its mode | the default-nozzle preset where the bundle hangs its other nozzle presets off it; otherwise `fdm_<vendor>_<model>_common` | `printable_area`, `printable_height`, `bed_exclude_area`, the model's start G-code, a COPY or MIRROR mode's settings |
|
||||
| Nozzle: the selectable preset | none | `printer_model` and `printer_variant`, which name the preset's model and nozzle and stay in every preset as the tree writes them; `nozzle_diameter`, `min_layer_height`, `max_layer_height`, `default_print_profile`, `default_filament_profile`, retraction the vendor tunes per nozzle |
|
||||
|
||||
A family may split once more, into toolhead or revision groups that exist only within it:
|
||||
`fdm_<vendor>_<family>_common` → `fdm_<vendor>_<family>_mk1_common`. A split that crosses another axis is not a
|
||||
level: when every controller comes with every toolhead, a toolhead base under each controller base
|
||||
repeats the same values in each ([Balance 6](#balance)).
|
||||
|
||||
| Process level | Base | Settings it determines |
|
||||
| --- | --- | --- |
|
||||
| Vendor | `fdm_process_common`, `fdm_process_<vendor>_common` | strategy: seam, wall order, infill and support patterns |
|
||||
| Printer family or variant layout | `fdm_process_<vendor>_<family>_common` (`fdm_process_arena_common`, BBL's `fdm_process_dual_common`) | speeds, accelerations and jerk of that motion system; the variant layout ([Process rule 4](extruder-variants.md#process)) |
|
||||
| Layer height × nozzle | `fdm_process_<vendor>_<lh>_nozzle_<n>` (BBL's `fdm_process_single_0.20`) | `layer_height`, line widths, shell layers, speeds scaled to the layer |
|
||||
| Quality × printer: the selectable preset | none | `compatible_printers`, and what is unique to that combination |
|
||||
|
||||
| Filament level | Base | Settings it determines |
|
||||
| --- | --- | --- |
|
||||
| Material | `fdm_filament_<material>` in OrcaFilamentLibrary, shared by every bundle | material defaults |
|
||||
| Product | `<Product> @base` | `filament_id` (minted here, [ids](ids.md)), `filament_vendor`, `filament_type`, density, cost, the product's temperatures and cooling |
|
||||
| Printer or nozzle tune: the selectable preset | none | `compatible_printers` (always in its own file, [Rule 9](../SKILL.md#rules)), volumetric speed, flow ratio, pressure advance and retraction measured on that printer |
|
||||
|
||||
**Equal where they must differ is a copy.** A key that follows a finer level, such as the layer-height
|
||||
limits and line widths that follow the nozzle diameter or `printable_area` that follows the bed, never
|
||||
moves above that level. When presets that differ in it carry the same value, the value was copied: leave
|
||||
it in the presets and report it (the [worked example](#worked-example-an-idex-family) has one).
|
||||
|
||||
## Balance
|
||||
|
||||
1. **One group, one base; use the existing one first.** A base is the home of the presets below it,
|
||||
whatever its name. In a bundle whose presets are all one family, the vendor base is the family base,
|
||||
so the family's values go there. In a bundle that hangs the other nozzles off the default-nozzle preset, that preset is the model's home.
|
||||
Never create a base whose presets are exactly its parent's. Where two existing bases already serve
|
||||
the same presets (a copied `fdm_machine_common` above the vendor's own base), the finer one is the
|
||||
home, and merging the pair is a change of its own. Moving a key into an existing home adds no file.
|
||||
A base value that no preset below it loads is dead: replace it with the group's value when there is
|
||||
one; otherwise leave it and report it, since a future preset would inherit it.
|
||||
2. **A new base must stand for a level and pay for itself.** Its file costs five metadata keys and an
|
||||
index entry, so create it only when it takes `k` keys off `n` selectable presets with
|
||||
`(n − 1) × k > 6`, where a custom G-code value counts as one key per G-code line: what it removes
|
||||
must outnumber what it adds. A model with two nozzles that share three short keys keeps them in both.
|
||||
3. **No ad-hoc bases:** never a base for presets that merely agree (the ones with 0.8 mm retraction),
|
||||
and never a base with one preset below it.
|
||||
4. **Keep every selectable preset within four ancestors**, selectable parents included. When a level would push a preset past that, fold it into the level above or leave
|
||||
its keys in the presets.
|
||||
5. **A default with exceptions.** A value that only some presets below a shared base load moves to that
|
||||
base, whichever axis it follows, when two conditions hold. More presets load it than any other value
|
||||
(on a tie the key stays in the presets), and `w − a > 1`, where `w` presets drop their copy and `a`
|
||||
presets that take the key from the base and load another value, the built-in default included, must
|
||||
now write theirs. Presets that write another value keep it and are unaffected. The base then holds
|
||||
the group's default, and the exceptions stay visible in their own files.
|
||||
6. **One chain; the other axes stay in the presets.** `inherits` follows one axis. When presets vary
|
||||
along several (bed size × controller × toolhead), first fill the existing homes by
|
||||
rules 1 and 5. Then give new bases to the axis whose bases pay most: sum rule 2's count over its
|
||||
bases, less the keys a re-parented preset must now write because it no longer inherits them from its
|
||||
old parent; on a tie, follow the layering the bundle already has. Leave the other axes' keys in the
|
||||
presets, and never repeat one axis's bases under each group of another. Outside BBL, whose synced
|
||||
presets need theirs, add no `include` template for a second axis: the loader reads `include` only
|
||||
since #15869 (2026-09-25), and an app that predates it ignores the key, so the template's settings
|
||||
never reach the preset.
|
||||
|
||||
Name a new base after its level ([base names](naming.md#bases)). The name must be unique in its bundle
|
||||
and must not equal a selectable preset's: two such presets load silently and the first in the index wins
|
||||
([uniqueness](naming.md#uniqueness)).
|
||||
|
||||
## Restated values
|
||||
|
||||
`candidates` lists every key a file writes that it would load unchanged without writing it. Delete it
|
||||
when the value is what the presets below the base that supplies it share: more of them load it,
|
||||
written or inherited, than any other value. A family that restates machine limits, clearances and
|
||||
G-code every other printer of the bundle loads from `fdm_klipper_common` drops its copies. When most
|
||||
presets below that base load another value, the match is a coincidence: keep the key, and move it to
|
||||
the level of the presets that share it. Keys of the nozzle level stay in the preset in
|
||||
either case.
|
||||
|
||||
After a restructure the report still lists keys that are right where they are: the nozzle-level keys
|
||||
each preset keeps, and arrays a list-less multi-extruder base writes at its presets' width for
|
||||
`check --strict`.
|
||||
|
||||
## Variant arrays on a base
|
||||
|
||||
- The loader stores a base with its variant arrays resized to the base's own variant list, one variant
|
||||
when it writes none, so an array on a narrower base reaches its presets as its first value, padded.
|
||||
Move a variant array to a base only when the base declares the presets' variant list (and, for a
|
||||
machine or process, their ids), moving the layout keys with it as
|
||||
[Printer rule 2](extruder-variants.md#printer-machine) and [Process rule 4](extruder-variants.md#process)
|
||||
ask; or when the presets load its first value anyway: every value is equal, or the presets are
|
||||
list-less machines, which the loader cuts to one variant. `compare` catches a cut.
|
||||
- Write the array on the base at the width of the presets it serves, their `N`
|
||||
([sizing equation](extruder-variants.md#sizing-equation)), so `check --strict` judges the right width
|
||||
where it reaches them. When the presets below a base need different widths (single- and
|
||||
dual-extruder models on one base), leave the array in the presets, or on bases that each serve one
|
||||
width.
|
||||
- On a list-less multi-extruder family base, declare the extruder count: `nozzle_diameter` and the other
|
||||
per-extruder vectors at one entry per extruder. The base then has its presets' width, and
|
||||
`fix-variant --strict` writes an array that reaches the presets at another width into the base once,
|
||||
instead of into every preset.
|
||||
- Deleting a restated variant array leaves the preset on the inherited array. Plain `check` accepts
|
||||
that; `check --strict` reports it when the inherited width differs, which is why a bundle held to
|
||||
`--strict` restates the array at each preset's width.
|
||||
|
||||
## Procedure
|
||||
|
||||
1. **Snapshot** the tree before any edit, `fix-variant` included. After `fix-variant`, run `compare`: a
|
||||
difference is a value its padding changed (it repeats the last value, the loader the first). Set that
|
||||
value deliberately, then snapshot again as the baseline for the restructure.
|
||||
2. **List the candidates:**
|
||||
|
||||
```bash
|
||||
python3 .claude/skills/orca-profiles/scripts/shared_settings.py candidates --vendor "<Vendor>" --type machine
|
||||
python3 .claude/skills/orca-profiles/scripts/shared_settings.py candidates --vendor "<Vendor>" --type machine --group-by printer_model
|
||||
```
|
||||
|
||||
It prints the [restated values](#restated-values), then, per base, the keys every selectable preset
|
||||
below it loads with one value and how many of those presets write it themselves, and under
|
||||
`default with exceptions` the values that pass [Balance 5](#balance), with `w` and `a`. With
|
||||
`--group-by <key>` it groups the selectable presets by that key's value instead and names each
|
||||
group's nearest common base, where a new base would go: `printer_model` for models, `gcode_flavor`
|
||||
for firmware, `extruder_type` or `default_filament_profile` for toolheads, `filament_id` for
|
||||
filament products, `layer_height` for processes. The report is evidence, not a plan: it cannot tell
|
||||
a shared value from a coincidence or a copy.
|
||||
3. **Decide each key** by [Levels](#levels), [Balance](#balance) and
|
||||
[Restated values](#restated-values): delete the restatements of shared values, move group values up
|
||||
to the group's home, and create only the bases that pay.
|
||||
4. **Edit.** Each new base gets `"type"`, `"name"`, `"from": "system"`, `"instantiation": "false"`, no
|
||||
`setting_id`, and `inherits` set to the presets' old parent. Point the presets' `inherits` at it and
|
||||
delete the moved keys from them. Bump the version, then run `normalize`, `update-index` (it orders
|
||||
parents first) and `generate-id --dry-run`, which must write nothing: bases take no id and presets
|
||||
keep theirs.
|
||||
5. **In a bundle held to `--strict`, run `fix-variant --strict` now**, so it writes into the new bases.
|
||||
6. **Verify.** `compare` prints `0 difference(s)`, and every one-sided key it lists is a default.
|
||||
`check` reports no error it did not report before, and neither does `check --strict` where the bundle
|
||||
passes it. Then run the [authoring checks](../SKILL.md#creating-or-modifying-a-profile).
|
||||
7. **Report** each base added (name, level, presets below it, keys it holds), the keys left in presets
|
||||
and why, the copies found, and the `compare` result.
|
||||
|
||||
## Worked example: an IDEX family
|
||||
|
||||
A Klipper bundle's IDEX family has 36 selectable machines (bed size 300, 400 or 500 × normal, COPY or
|
||||
MIRROR mode × 0.4, 0.5, 0.6 or 0.8 nozzle) that all inherit `fdm_klipper_common` directly and write 49
|
||||
keys each. `fix-variant` has widened their retraction arrays to two values and their machine limits
|
||||
to four.
|
||||
|
||||
- **Restated.** All 36 restate 10 values that every other printer of the bundle loads from
|
||||
`fdm_klipper_common`: three machine limits, the three clearances, wipe, `retract_before_wipe`, and the
|
||||
layer-change and pause G-code. Delete them.
|
||||
- **Family.** All 36 load one value for 24 more keys: the other machine limits, `extruder_offset`,
|
||||
retraction, `z_hop`, `single_extruder_multi_material`, `manual_filament_change`, the remaining G-code
|
||||
except the start G-code, and thumbnails. A new family base, `fdm_<vendor>_<family>_idex_common`,
|
||||
holds them, plus `nozzle_diameter` `["0.4", "0.4"]` for the extruder count: at least
|
||||
`(36 − 1) × 24 = 840`.
|
||||
- **Model.** Each `printer_model` (a bed size in one mode) has four nozzle presets that share
|
||||
`printable_area`, `printable_height` and a three-line `machine_start_gcode`: `(4 − 1) × 5 = 15`, so
|
||||
one base per model, nine in all. The family does not hang its other nozzles off a default-nozzle
|
||||
preset, so the model level here is a base.
|
||||
- **A coincidence.** The twelve 500 presets' `printable_height` 500 equals `fdm_klipper_common`'s, but
|
||||
every preset of the bundle writes its own height and 300 is the most common. The 500 is the base's
|
||||
leftover, not a shared value, so it stays on the model bases.
|
||||
- **A copy.** In COPY and MIRROR mode, every nozzle of a model carries the 0.4 nozzle's
|
||||
`min_layer_height`, `max_layer_height` and `retract_lift_below`: 0.06, 0.3 and 0.2 on the 0.8 nozzle,
|
||||
where normal mode has 0.12, 0.5 and 0.3. `candidates --group-by printer_model` lists the first and
|
||||
last as shared by the model, and `max_layer_height` as restated from `fdm_klipper_common`, whose value
|
||||
is also 0.3. They follow the nozzle, so they stay in the presets and are reported for tuning.
|
||||
- **Nozzle.** Each preset keeps `printer_model`, `printer_variant`, `nozzle_diameter`,
|
||||
`min_layer_height`, `max_layer_height` and `retract_lift_below` beside its metadata.
|
||||
- **Result.** 36 full presets become 36 short ones on 10 new bases. `compare` reports 0 differences,
|
||||
`check --vendor "<Vendor>"` passes as before, and `generate-id --dry-run` writes nothing.
|
||||
`check --strict` reports more errors than before, because the deleted variant arrays now reach the
|
||||
presets at `fdm_klipper_common`'s one value. `fix-variant --strict` writes those arrays into the
|
||||
family base alone, after which `check --strict` passes for the family and `compare` still reports 0
|
||||
differences.
|
||||
@@ -1,447 +0,0 @@
|
||||
# Validating profiles
|
||||
|
||||
```bash
|
||||
./scripts/check_profile.sh # everything CI runs
|
||||
./scripts/check_profile.sh --vendor "<Vendor>" # development 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. Its 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 runs even after an earlier one fails; the script exits non-zero if any failed, and writes
|
||||
`logs/<check>.log` plus, on failure, `pr_comment.md` (the same report CI posts on the PR) under a
|
||||
per-user cache dir:
|
||||
|
||||
| Platform | Cache dir |
|
||||
| --- | --- |
|
||||
| macOS | `~/Library/Caches/orca-profile-check` |
|
||||
| Linux | `${XDG_CACHE_HOME:-~/.cache}/orca-profile-check` |
|
||||
| Windows | `%LOCALAPPDATA%\orca-profile-check` |
|
||||
|
||||
It is named apart from OrcaSlicer's own per-user dirs and sits outside the checkout, so every worktree
|
||||
shares one copy and each run overwrites its `logs/`. `--work-dir` / `-WorkDir` overrides it.
|
||||
|
||||
When other worktrees or agents may run checks too, pass `--work-dir <a dir of your own>` from the start
|
||||
and capture the console output yourself: the shared `logs/` can belong to another run by the time you
|
||||
read them. With `--work-dir`, also pass `--validator` pointing at the cached nightly, so the new dir
|
||||
does not download it again:
|
||||
|
||||
| Platform | Cached validator |
|
||||
| --- | --- |
|
||||
| Linux | `<cache dir>/validator/OrcaSlicer_profile_validator` |
|
||||
| macOS | `<cache dir>/validator/OrcaSlicer_profile_validator.app/Contents/MacOS/OrcaSlicer_profile_validator` |
|
||||
| Windows | `<cache dir>\validator\OrcaSlicer_profile_validator.exe` |
|
||||
|
||||
Copying the cached `profile-fixtures/` into the new dir reuses the fixture archives; the fixture
|
||||
`manifest.json` is still downloaded on every run, so `validate_custom` needs the network either way.
|
||||
`another run is using <dir>` means a live run holds `<dir>/.lock`: leave it and use your own
|
||||
`--work-dir`. Only a `.lock` with no `check_profile` process alive is a crash leftover; delete it 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, variant strings and array widths and layout keys, dangling `default_materials`, id length, **all `setting_id` and `filament_id` rules** ([below](#orca_profile_toolpy-check)) |
|
||||
| `validate_system` | `validator -p resources/profiles -l 2` | load errors, missing filament `compatible_printers`, dangling `inherits` / `compatible_*`, duplicate `filament_id` per printer (`Ambiguous AMS filament match`), printer defaults that name no compatible system filament |
|
||||
| `validate_slice` | `validator -p … -s -l 2` | custom G-code expansion and unresolvable printer defaults, by slicing |
|
||||
| `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 defaults to on, so the duplicate-`filament_id` check runs whether or not you
|
||||
pass it, and `validate_system` already fails on duplicates. The binary's own `--help` ("Off unless this
|
||||
flag is present") does not reflect that default.
|
||||
|
||||
**A `--vendor` run reads differently from CI.** The validator's `-v` loads that vendor plus
|
||||
OrcaFilamentLibrary and nothing else, so every library tune whose `compatible_printers` names another
|
||||
vendor's printers fails `validate_system`, `validate_filament_subtypes` and each `validate_custom`
|
||||
fixture with thousands of `references unknown compatible_printers "Bambu Lab …"` lines. Under
|
||||
`--vendor`, `profile_tool` and `validate_slice` are the meaningful results; for the other three, filter
|
||||
the log for your vendor's files and treat only those lines as findings. The unscoped run is the CI
|
||||
result; run it before the PR.
|
||||
|
||||
### `validate_custom`: the backward-compatibility gate
|
||||
|
||||
It 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. It 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. This is what makes a rename, a deletion or an `instantiation` flip a CI
|
||||
failure rather than just a user complaint, and the reason `renamed_from` is mandatory.
|
||||
|
||||
The whole current tree sits under each fixture, so every `validate_system` error fails
|
||||
`validate_custom` too: fix `validate_system` first. Under `--vendor` it copies only the top-level index
|
||||
files, `<Vendor>/` and `OrcaFilamentLibrary/`, and picks fixtures by the index's display `name`
|
||||
(`Bambulab` for `BBL`), not the file stem; fixture presets without that prefix are covered only by an
|
||||
unscoped run, and it warns `validate_custom checked nothing` when none match.
|
||||
|
||||
### `validate_slice`
|
||||
|
||||
It 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` (`change_filament_gcode` never expanded). Non-default processes and
|
||||
filaments get no dedicated coverage; [slice them on a copy](#checking-a-copy-of-the-tree). In the
|
||||
default set, a bundle without a `machine/` folder is recorded as SKIP; naming `validate_slice`
|
||||
explicitly for it fails (`No instantiable printer presets found for vendor OrcaFilamentLibrary`). The
|
||||
validator logs `[error]` lines that do not fail a check (such as `could not found extruder_type`); only
|
||||
each check's PASS or FAIL counts.
|
||||
|
||||
## `orca_profile_tool.py check`
|
||||
|
||||
`check` is one subcommand of the tool that also owns `fix-variant`, `generate-id`, `normalize`,
|
||||
`trim` and `update-index`; [ids.md](ids.md#the-tool) has the writing half.
|
||||
|
||||
| Catches | Scope | Function in the tool |
|
||||
| --- | --- | --- |
|
||||
| two files in one bundle claiming one type + name, indexed or not | per vendor | `check_preset_name_uniqueness` |
|
||||
| a file on disk that no `*_list` references (**an error, not a warning**) | per vendor | `check_index_coverage` |
|
||||
| an index entry whose `name` disagrees with the file, or whose `sub_path` is missing | per vendor | `check_name_consistency` |
|
||||
| a file `normalize` would rewrite, an index `update-index` would rebuild | per vendor | `check_normalized` |
|
||||
| duplicate JSON keys in a file | every file read | the JSON loader |
|
||||
| an instantiated non-library filament with no non-empty `compatible_printers` of its own | per vendor | `check_filament_compatible_printers` |
|
||||
| `extruder_clearance_radius` alongside `extruder_clearance_max_radius` | per vendor | `check_conflict_keys` |
|
||||
| a scalar `filament_type` (`"filament_type": "PLA"`); the five other filament vectors `normalize` arrayifies surface as `normalize would convert <field> to an array` | per vendor | `check_vector_type_keys`, `check_normalized` |
|
||||
| a variant string the two enums cannot build (a dead variant, a legacy spelling included), a variant list naming one variant twice, an `extruder_type`, `nozzle_volume_type` or `default_nozzle_volume_type` that is not an enum name ([variant names](#variant-names)) | per vendor | `check_variant_names` |
|
||||
| a variant array not exactly `variant length × stride` wide in a selectable preset that writes it (with `--strict`, also in one it reaches); machine variant layout keys that disagree; a process id array that does not pair each variant ([variant arrays](#variant-arrays)) | per vendor | `check_variant_arrays` |
|
||||
| a declared `filament_id` longer than 8 characters | per vendor | `check_filament_id_length` |
|
||||
| a `default_materials` name, or a `default_filament_profile` name, matching no filament file ([below](#default-material-references)) | per vendor | `check_machine_default_materials` |
|
||||
| per-key warnings for ignored options, **filament files only** | per vendor | `check_obsolete_keys` |
|
||||
| `setting_id` uniqueness, every `filament_id` rule, and `machine_model` names duplicated across bundles | **tree-wide, ignoring `--vendor` entirely** | `check_setting_id_uniqueness`, `check_filament_ids`, `check_machine_model_name_uniqueness` |
|
||||
|
||||
Because the id and model-name checks stay tree-wide, 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
|
||||
(below); OrcaFilamentLibrary is held to the same rules as any vendor, its sole exemption being that a
|
||||
library filament may leave `compatible_printers` empty. `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
|
||||
(`check --vendor A --vendor OrcaFilamentLibrary`); `check_profile.sh` takes one.
|
||||
- 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): usually an emptied folder, or a `user/` left by a direct validator
|
||||
run. An unscoped `check` skips `user/` by name; `--vendor user` still checks and warns about it.
|
||||
`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.
|
||||
- When there are errors or warnings, the trailing summary 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.
|
||||
- Other options: `--dry-run` on every writing command, `--profiles DIR` to point any command at another
|
||||
tree, `--profile-type` to narrow `normalize`, `trim` and `update-index` to one type
|
||||
([ids.md](ids.md#the-tool)).
|
||||
- `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 keys
|
||||
|
||||
`check` always reports per-key warnings for obsolete options in filament profiles. Its normalization
|
||||
check also rejects obsolete keys across all preset types (`normalize would remove <key>`); `normalize`
|
||||
removes them.
|
||||
|
||||
### Default-material references
|
||||
|
||||
The materials check finds `default_materials` / `default_filament_profile` entries naming a preset that
|
||||
does not exist. It reads each `machine/` file's own key (a model's `default_materials`, a variant's
|
||||
`default_filament_profile`; a file that writes both is checked on `default_materials` only) and accepts
|
||||
any `name` found in the vendor's or OrcaFilamentLibrary's `filament/` files, bases and unindexed files
|
||||
included. The three authoring errors it surfaces are `,` instead of `;`, wrong case (`@system`), and a
|
||||
whole `;`-joined string stuffed into one array element. Only the validator (`validate_system`) requires
|
||||
an instantiated system filament that is compatible with each variant.
|
||||
|
||||
### Variant arrays
|
||||
|
||||
`check_variant_arrays` composes every selectable preset the loader's way (the parent, then each
|
||||
`include` in order, then the file's own keys; a filament's `inherits` may fall through to
|
||||
OrcaFilamentLibrary) and holds each key of [the four variant sets](extruder-variants.md#the-four-key-sets)
|
||||
that the preset writes itself to exactly its `variant length × stride`
|
||||
([widths](extruder-variants.md#widths)). The variant length is the length of the composed preset's
|
||||
own `*_extruder_variant` list; without one, a machine's is the number of variants its
|
||||
`extruder_variant_list` offers, else its extruder count (the list's default is one
|
||||
`Direct Drive Standard` per extruder), and a process's or filament's is 1. Any other width is an
|
||||
error, one value included and even when every value is the same. A key the preset does not write
|
||||
takes what reaches it, the default or an array it inherits or includes, which the loader resizes; it
|
||||
is not checked. A base is not judged on its own: what it writes counts only where it reaches a preset
|
||||
that does not override it.
|
||||
|
||||
`check --strict` also holds every selectable preset to its own width for each key that reaches it,
|
||||
so a preset whose variants differ from those of the file its array comes from restates the array
|
||||
(the error names that file). That is BBL's practice and the target for new printer-specific presets;
|
||||
CI runs `check` without `--strict`.
|
||||
|
||||
An id array that reaches a selectable preset, `printer_extruder_id` or `print_extruder_id`, written or
|
||||
inherited, must have one entry per entry of its variant list on any printer. This rule and the
|
||||
machine layout rules below judge the composed preset without `--strict`, whichever file writes the
|
||||
keys. Beside a written variant list this rule reports it instead of the width
|
||||
rule, so it is reported once. A process that
|
||||
lists variants without `print_extruder_id` gets a **warning** when a variant repeats (every entry then
|
||||
reads as extruder 1, so the repeated variant is unreachable), nothing otherwise.
|
||||
|
||||
On a machine the layout keys are held to [Printer rules 1–4](extruder-variants.md#printer-machine),
|
||||
every failure an error unless marked:
|
||||
|
||||
- With `extruder_variant_list` written: the list has one entry per extruder, as many as
|
||||
`nozzle_diameter`; every variant starts with its extruder's `extruder_type`;
|
||||
`default_nozzle_volume_type` names a nozzle volume type that extruder lists;
|
||||
`printer_extruder_variant` is the list flattened extruder-major and `printer_extruder_id` gives each
|
||||
entry its 1-based extruder (an id array left out reads as extruder 1 everywhere, which passes when
|
||||
those are the flattening's ids). Without the pair, the list may offer one variant in total.
|
||||
- With the pair written and no `extruder_variant_list`: one variant per extruder at most. A pair that
|
||||
`single_extruder_multi_material` off would replace with the default at load is a **warning**; a pair
|
||||
the rebuild would leave as it is passes.
|
||||
|
||||
It does **not** see a base's own resize: it composes at the width each file wrote, so an array wider
|
||||
than a base's list, which the loader cuts before any child inherits it, passes even with `--strict`
|
||||
([composition](extruder-variants.md#padding-truncation-and-composition)). Nor does it see per-extruder
|
||||
vectors outside the sets (`extruder_offset`, `printer_extruder_options`, …), which no variant list
|
||||
sizes. What a variant holds (a High Flow variant copied from Standard, a variant inserted at the wrong
|
||||
index) is review work
|
||||
([item 14](review-checklist.md#14-per-extruder-vectors-not-checked-and-variant-arrays-widths-and-layout-checked));
|
||||
whether it is a name the engine can select at all is `check_variant_names`'.
|
||||
|
||||
`python3 scripts/orca_profile_tool.py fix-variant` resizes every array the width rule rejects in the
|
||||
selectable preset that writes it, leaves bases alone and adds no key: extra values are dropped, missing ones
|
||||
repeat the last value (the last normal/silent pair at stride 2; a lone value fills normal and silent
|
||||
alike). It leaves the variant lists and id arrays to you, since they address the variants rather than
|
||||
fill them. `fix-variant --strict` then also writes each key that reaches a selectable preset at
|
||||
another width: into the most general file on the way down to the preset whose own width is the
|
||||
preset's and whose selectable presets taking it all need that width, else into the preset itself.
|
||||
Presets of every bundle count towards that agreement; `--vendor` limits the files written and
|
||||
`--dry-run` previews. The loader pads with the first value where `fix-variant` repeats the last, so a
|
||||
padded array need not load as before, and
|
||||
trimming deletes values: when the extra values were meant as per-extruder or per-variant values,
|
||||
declare the variant layout instead ([Printer rule 2](extruder-variants.md#printer-machine)) and keep
|
||||
them. `fix-variant` moves no value, so a family whose presets all wrote one value repeats the widened
|
||||
array in each; put it on the family's base afterwards ([shared bases](shared-bases.md)).
|
||||
|
||||
### Variant names
|
||||
|
||||
`check_variant_names` reads the four list keys plus `extruder_type`, `nozzle_volume_type` and
|
||||
`default_nozzle_volume_type` of every preset the bundle's index references, bases included, and holds
|
||||
each entry to the names the engine's two enum maps define (`s_keys_map_ExtruderType`,
|
||||
`s_keys_map_NozzleVolumeType`, read from `PrintConfig.cpp` on every run). The bundle's own files are
|
||||
judged, not the composed config: a bad string is the writing file's error, once. Every finding is an
|
||||
error, and no bundle is exempt: BBL, whose bundle is imported from BambuStudio, is held to OrcaSlicer's
|
||||
enums like any other.
|
||||
|
||||
- A variant string outside `<extruder type> <nozzle volume type>` is a **dead variant**: it still
|
||||
counts toward the variant length the arrays are sized by, so the values written for it silently never
|
||||
reach the G-code. `Hybrid` too, which is runtime-only, and an empty entry. A name BambuStudio's enum
|
||||
has and OrcaSlicer's lacks is dead here as well, and passes with no tool change once the engine gains
|
||||
that nozzle volume type.
|
||||
- A legacy name the loader still rewrites in these keys (`Normal` → `Standard`, `Big Traffic` →
|
||||
`High Flow`) is an error that names the enum name to write; the profile has to spell the enum
|
||||
name. `DirectDrive` is only rewritten in `extruder_type`, so a variant string carrying it is dead.
|
||||
- A variant list naming one variant twice is an error: the lookup returns the first equal string, so
|
||||
the repeat is unreachable and its value sits at an index no extruder reads. A filament list takes
|
||||
strings, `extruder_variant_list` takes them per extruder, and a process takes `(extruder id, variant)`
|
||||
pairs — one string on two extruders is two pairs, not a repeat.
|
||||
- An `extruder_type`, `nozzle_volume_type` or `default_nozzle_volume_type` value that is not an enum
|
||||
name is an error: they are enum options, so an unknown value fails the validator's load of the
|
||||
whole bundle, while the app silently loads the option's default instead. A legacy spelling
|
||||
(`DirectDrive`, `Normal`, `Big Traffic`) and `Hybrid`, an enum value no profile writes, are errors
|
||||
too.
|
||||
|
||||
The variant *order*, the choice of variants, and the values themselves are not checked.
|
||||
|
||||
### `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 the loader ignores (the `ignore` set in `PrintConfigDef::handle_legacy`),
|
||||
across preset types;
|
||||
- resolves the `extruder_clearance_*` conflict pair by keeping the larger;
|
||||
- arrayifies six filament options (`filament_type`, `filament_cost`, `filament_density`,
|
||||
`temperature_vitrification`, `filament_max_volumetric_speed`, `filament_vendor`);
|
||||
- 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. A
|
||||
file committed with CRLF line endings therefore changes on every line; read the diff before committing
|
||||
it.
|
||||
|
||||
**Set `type` explicitly when authoring.** For a file in `machine/` without it, normalization guesses
|
||||
`machine` only if its name contains `nozzle` (case-insensitive), otherwise `machine_model`. That
|
||||
heuristic cannot reliably classify shared machine bases or unusually named variants.
|
||||
|
||||
The tool's obsolete-key set is checked against the loader's ignore list by a unit test. Active options
|
||||
are preserved, including live keys whose *values* the loader rewrites (`extruder_type`: `DirectDrive` →
|
||||
`Direct Drive`; the variant-string keys: `Normal` / `Big Traffic` → `Standard` / `High Flow`), and so are
|
||||
legacy key names the loader migrates (such as `extruder_clearance_max_radius`).
|
||||
|
||||
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; do not run it on a shipped bundle.)
|
||||
- **A misspelled setting key.** `inital_layer_height` and `sparse_infill_densiti` pass `check` cleanly.
|
||||
Verify new keys against `src/libslic3r/PrintConfig.cpp` and the loader's legacy handling
|
||||
(`PrintConfigDef::handle_legacy`: renamed keys, rewritten values and ignored keys).
|
||||
|
||||
`check` does not catch a dangling `default_print_profile` either; check that name by hand.
|
||||
|
||||
## The validator binary
|
||||
|
||||
Built from `src/dev-utils/OrcaSlicer_profile_validator.cpp` with `-DORCA_TOOLS=ON`. Both scripts use a
|
||||
local build under `build*/` when one exists, else they download the nightly into the `validator`
|
||||
subdirectory of the cache dir. `check_profile.sh` searches Release, then RelWithDebInfo, then Debug
|
||||
(each under `build*/src/<config>` and `build*/*/src/<config>`), then single-config `build*/src`, and
|
||||
prefers a host-architecture build tree; `check_profile.ps1` tries Release, RelWithDebInfo, MinSizeRel,
|
||||
then Debug. A stale local build is used silently; `--download` / `-Download` skips local builds and uses the
|
||||
nightly. The download and the fixtures stay cached until `--refresh` / `-Refresh`, so
|
||||
`--download --refresh` (`-Download -Refresh`) matches CI exactly. Windows looks for
|
||||
`OrcaSlicer_profile_validator.exe`.
|
||||
|
||||
If your build lives somewhere else, 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 |
|
||||
| `-o <dir>` | with `-s`, save each printer's G-code there |
|
||||
| `-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 with `-DORCA_TOOLS=ON` and pass `--validator`.
|
||||
|
||||
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>"
|
||||
# Windows: scripts\check_profile.bat -ProfilesDir "<tree>"
|
||||
```
|
||||
|
||||
To slice a process or filament that is not a printer's default, or to read the G-code, work on a copy:
|
||||
copy `resources/profiles` and `resources/info` into one scratch dir (the validator reads `info/` next to
|
||||
the tree), point the printer's `default_print_profile` or first `default_filament_profile` at the preset
|
||||
in the copy, and run the validator with `-o`:
|
||||
|
||||
```bash
|
||||
<validator> -p <scratch>/profiles -v "<Vendor>" -s -o <gcode dir>
|
||||
```
|
||||
|
||||
Each printer's G-code is saved as `<vendor name>__<printer>.gcode`; its embedded config
|
||||
(`; print_settings_id = …`, `; filament_settings_id = …`) shows what was actually sliced.
|
||||
|
||||
## 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.
|
||||
|
||||
## Error → remedy
|
||||
|
||||
| Message | Fix |
|
||||
| --- | --- |
|
||||
| `can not find inherits <parent> for <preset>` | parent missing, unregistered, misspelled, or listed **after** the child |
|
||||
| `can not find include` | the template is misspelled, registered after the includer, or selectable |
|
||||
| `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` |
|
||||
| `Failed loading configuration file <file>` | that file could not be loaded and the whole bundle was discarded: a JSON error, or a value its option cannot take, such as `nil` in a non-nullable key (`Invalid value provided for parameter <key>: nil`, `Deserializing nil into a non-nullable object`); the lines above it name the cause |
|
||||
| `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 a token of the model's `nozzle_diameter` list |
|
||||
| `has printer_variant "<v>" that does not match its nozzle_diameter` | [the set comparison](machine-profiles.md#printer_model-and-printer_variant) |
|
||||
| `references unknown compatible_printers "<p>"` | the printer was renamed or deleted, or a `machine_model` name was used instead of a variant name: fix the reference. Under `--vendor`, usually another vendor's printer ([why](#the-five-checks)) |
|
||||
| `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 resolved-vs-own-key trap is in [filament-profiles.md](filament-profiles.md#compatible_printers) |
|
||||
| `Ambiguous AMS filament match: N filament presets share filament_id "X" and are all compatible with printer "Y"` | make the lists disjoint by [specificity](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product): the specialized profile keeps the variant, the general ones drop it; prefer this over deleting a profile. Or fix an `inherits` pointing at another product's `@base`. `orca_profile_tool.py check` does not catch this; only `validate_system` does |
|
||||
| `Layer height cannot exceed nozzle diameter.` / `Line width too small` / `Line width too large` | the [slicing limits](process-profiles.md#values-to-review-per-nozzle) |
|
||||
| `[ERROR] … no <V>.json list references it, so it never loads` | `update-index`, or delete the file |
|
||||
| `[ERROR] … no <V>.json list 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 |
|
||||
| `Duplicate key error in <file>: Duplicate key detected: <key>` | a key written twice in one file; keep the intended one |
|
||||
| `… must not have a setting_id` / `… is missing a setting_id` / `setting_id "X" in <file> does not match the expected "Y" …` | `generate-id --setting-id` (by hand in `BBL/`, see [ids.md](ids.md#bbls-exception-precisely)) |
|
||||
| `filament_id "X" declared by … does not match the mint of its triple …` | `generate-id --filament-id`; if the id should not be declared here at all, remove it so the preset inherits its root's |
|
||||
| `inherits filament_id "X" but its own triple "V/T/N" mints "Y"` | `generate-id` will **not** fix this; see [ids.md](ids.md#what-generate-id-does-and-does-not-fix) |
|
||||
| `"<key>" has N values for variant length S at stride k, which takes M` (with ` (no <list key>, so …)` after `S`, naming where `S` came from, when the preset writes no list, and ` (it comes from <file>)` at the end under `--strict` for an array the preset does not write) | exactly `S × k` values in variant order, or leave the key out ([widths](extruder-variants.md#widths)); on a list-less multi-extruder printer whose extruders differ, declare the layout; `fix-variant` cuts or pads to that width ([variant arrays](#variant-arrays)); an `S` you did not expect means the variant list did not resolve through `include` or `inherits` |
|
||||
| `printer_extruder_variant […] is not extruder_variant_list flattened extruder-major […]` / `printer_extruder_id […] does not give each entry of printer_extruder_variant its 1-based extruder […]` / `printer_extruder_id has N entries for the M entries of printer_extruder_variant` | write the pair as the flattening of the list, ids in step ([Printer rule 2](extruder-variants.md#printer-machine)) |
|
||||
| `extruder_variant_list has N entries for M extruder(s)` / `extruder_variant_list entry i "…" holds a variant that does not start with extruder i's extruder_type` / `default_nozzle_volume_type "…" is not a nozzle volume type extruder i lists` | [Printer rules 1, 3 and 4](extruder-variants.md#printer-machine) |
|
||||
| `extruder_variant_list offers N variants but the preset writes no printer_extruder_variant/printer_extruder_id` / `printer_extruder_variant lists several variants for extruder N but the preset writes no extruder_variant_list` / `[WARNING] … with single_extruder_multi_material off the loader replaces printer_extruder_variant …` | write all three layout keys ([Printer rule 2](extruder-variants.md#printer-machine)) |
|
||||
| `print_extruder_id has N entries for the M entries of print_extruder_variant` / `[WARNING] … print_extruder_variant repeats a variant but print_extruder_id is absent` | one id per variant entry, mirroring the printer's pairs ([Process rule 1](extruder-variants.md#process)) |
|
||||
| `<key> <where> holds "…", which no extruder can select: "…" is not a nozzle volume type the enum has (…)` / `… it does not start with an extruder type the enum has (…)` / `… is empty` / `… Hybrid names the sub-nozzles of one hybrid extruder at runtime` | write a legal variant string, `<extruder type> <nozzle volume type>` from the two enums ([variant names](#variant-names), [variant strings](extruder-variants.md#variant-strings)); in every bundle, BBL included |
|
||||
| `… holds the legacy variant "…"` / `extruder_type spells the legacy name "…"` / `nozzle_volume_type spells the legacy name "…"` / `default_nozzle_volume_type spells the legacy name "…"` | write the enum name the message gives: the loader still rewrites the legacy one, but `check` rejects it |
|
||||
| `<key> lists "…" N times` / `print_extruder_variant lists the pair (extruder N, "…") N times` | drop the repeat and its value from every variant array: the lookup returns the first equal string |
|
||||
| `extruder_type "…" is not one of (…)` / `nozzle_volume_type "…" is not one of (…)` / `default_nozzle_volume_type "…" is not one of (…)` / `… names "Hybrid", which the engine computes for a hybrid extruder at runtime` | they are enum options, so an unknown value fails the validator's load of the whole bundle and silently becomes the default in the app; use `s_keys_map_ExtruderType` / `s_keys_map_NozzleVolumeType`, `Hybrid` excepted ([variant strings](extruder-variants.md#variant-strings)) |
|
||||
| `[WARNING] No profiles found for vendor: <dir>` | a directory with no matching index (an emptied folder, or a `user/` left by a direct validator run); remove it |
|
||||
| `… has no compatible system filament in its model's "default_materials"` | add a system filament preset compatible with that variant to the model's `default_materials` |
|
||||
| `… names the unknown system filament "<n>" in its "default_materials"` / `… "default_filament_profile"` | name an existing system (not user, not base) filament exactly; `;` separators, exact case |
|
||||
| `Missing filament profile: '<n>' referenced in <file>` | the same, caught by `check`; usual causes are `,` instead of `;`, wrong case (`@system`), or a `;`-joined list packed into one array element |
|
||||
| `machine_model name "<n>" is declared by N bundles` | model names are unique across the tree; rename the new model |
|
||||
| `vendor <V>'s config version: <s> invalid` | the `version` string does not parse; write `MM.mm.pp.bb` |
|
||||
| `[json.exception.type_error.302] type must be string` | locate the non-string value in the index or a model; see [failure scopes](vendor-bundle.md#failure-scopes) |
|
||||
| `Printer "<p>" fell back to a default preset` | the final process or filament selection is a generic Default preset: check the named defaults, their visibility and that compatible presets exist. An incompatible default may instead be replaced without this error |
|
||||
| `Printer "<p>" sliced but the filament change never fired (no CP TOOLCHANGE START)` | `change_filament_gcode` never expanded |
|
||||
|
||||
## CI
|
||||
|
||||
`.github/workflows/check_profiles.yml`, job **"Check profiles"**, runs on `pull_request` into `main` or
|
||||
`release/*` touching `resources/profiles/**`, `resources/printers/**`, `scripts/**`,
|
||||
`src/libslic3r/PrintConfig.cpp` (where the tool reads the variant key sets) or 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
|
||||
(run them locally after changing `scripts/`, with `py -3` on Windows, and keep `-t scripts` or the
|
||||
imports fail). 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 -->`, holding the start of each failing log (30 KB per check,
|
||||
12 KB per `validate_custom` fixture; reproduce locally for the full list); 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 PR limited to their own `resources/profiles/<Vendor>/` folder 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,214 +0,0 @@
|
||||
# Vendor bundles
|
||||
|
||||
A bundle is `resources/profiles/<Vendor>.json` (the index) plus `resources/profiles/<Vendor>/`. The
|
||||
**vendor id is the filename stem**, not the `name` inside; the two may 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": "Phrozen Arco", "sub_path": "machine/Phrozen Arco.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 the profile updater, 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, includes before includers.** The lists load processes first, then
|
||||
filaments, then printers, each in index order, and `inherits` and `include` resolve only against
|
||||
presets of that type already loaded from it. A parent
|
||||
listed after its child produces `can not find inherits <parent> for <child>` and the bundle is
|
||||
discarded; an include listed after its includer is `can not find include`, a counted error that
|
||||
leaves the includer without those keys.
|
||||
3. **The index entry's `name` equals the `name` inside the `sub_path` file.** `renamed_from` does not
|
||||
excuse a mismatch.
|
||||
|
||||
All three are `check` errors, and `update-index` writes an index that satisfies all three from the
|
||||
files on disk, including the dependency ordering (parents and templates before the presets that use
|
||||
them, then, in name order, entries that neither depend on nor are depended on by another entry of their own
|
||||
list, and any entry on a dependency cycle). 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 misspelled key
|
||||
(`"subpath"`), is silently dropped. (A misspelled `sub_path` value is a `check` error naming the entry.)
|
||||
|
||||
`BBL/cli_config.json` and, in `BBL/filament/`, `filaments_color_codes.json`, `filament_id_map.json`,
|
||||
`filament_name_map.json` and `support_recommended_params.json` are auxiliary data files read by path,
|
||||
not presets. The last three carry a `type` key and look like presets; the tool excludes all five from
|
||||
preset maintenance.
|
||||
|
||||
## `version`
|
||||
|
||||
Four components, `MM.mm.pp.bb`, compared as a version number in which the fourth is folded into the
|
||||
third (`patch × 100 + build`). Write all four components, zero-padded.
|
||||
|
||||
- **Bump the version for every bundle the change touches.** The app installs bundled profiles only
|
||||
when their version is newer than the installed one, and the `.opc` preset cache is also keyed on
|
||||
the version. Nothing in profile CI checks the bump.
|
||||
- **Keep the last component ≤ 99.** `02.04.00.100` and `02.04.01.00` both read as
|
||||
`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: it reads as `0.0.0`, which is not a valid version.
|
||||
`check` and the validator still pass, but the vendor is dropped from the setup wizard entirely and gets no preset cache. Confirm the key exists. An
|
||||
*unparseable* version is not silent: it discards the whole bundle (`vendor <V>'s config version: <s>
|
||||
invalid`).
|
||||
|
||||
## Common preset keys
|
||||
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| `type` | `machine_model`, `machine`, `process` or `filament` |
|
||||
| `name` | the preset name, the identity every reference uses; the filename is *not* authoritative |
|
||||
| `inherits` | the parent's exact `name`: no path, no `.json` |
|
||||
| `include` | a template's exact `name`, or an array of them, layered under this preset's own keys ([below](#inherits-and-include)) |
|
||||
| `instantiation` | the **string** `"true"` (selectable) or `"false"` (base) |
|
||||
| `from` | `"system"` for shipped presets |
|
||||
| `setting_id` | generated; required on instantiated presets, forbidden on bases |
|
||||
| `renamed_from` | `;`-separated old names this preset supersedes ([below](#renamed_from)) |
|
||||
|
||||
These are config-preset keys; `machine_model` records have their own
|
||||
[key set](machine-profiles.md#machine_model-a-record-not-a-config-preset). Keep `from` as `"system"`:
|
||||
the bundle loader ignores it, but loading the file as a CLI config accepts only `system`, `user` or
|
||||
`User` and handles their inheritance differently.
|
||||
|
||||
`instantiation` is the one metadata key the validator gates: a missing key or any value other than the
|
||||
strings `"true"` / `"false"` is a counted error (`Missing instantiation attribute for <name>`) that fails
|
||||
the validator, though the preset still loads and is treated as selectable. A file with no
|
||||
`instantiation` whose name contains `gcode`, or that has no `name`, silently becomes an include-only
|
||||
template. A JSON boolean `true` fails harder: it takes the **whole vendor bundle** down.
|
||||
|
||||
## `inherits` and `include`
|
||||
|
||||
`inherits` resolves by exact name **within the same bundle**, plus one exception: filaments may inherit
|
||||
from OrcaFilamentLibrary, which is loaded first. Vendor-to-vendor inheritance always fails, and an
|
||||
unresolved `inherits` discards the bundle. You can inherit from an instantiated preset as well as from a
|
||||
base.
|
||||
|
||||
`"include": ["<name>", …]` (or one bare name) pulls in `instantiation: "false"` presets of the same type
|
||||
from the same bundle (never the library), registered before the includer. It shares a block of keys
|
||||
between presets that do not share a parent: a variant layout, a G-code template. Only `"false"` presets
|
||||
can be included, so a name that resolves to nothing (misspelled, registered after the includer, or a
|
||||
selectable preset) is a counted error (`can not find include`) and the preset loads without it.
|
||||
|
||||
**How a preset's config is composed:** start from the parent's stored config (a root starts from the
|
||||
built-in defaults), apply each preset named in `include` in the order listed, then the preset's own
|
||||
keys. Later layers win, so precedence is own keys > later includes > earlier includes > the `inherits`
|
||||
chain. Only then is every variant key of the composed config resized to its variant length
|
||||
([widths](extruder-variants.md#widths)), and keys of another preset type removed. The two routes hand
|
||||
down different widths:
|
||||
|
||||
- **`inherits` hands down the resized config.** A base is stored *after* its own resize, at the length
|
||||
of its own `*_extruder_variant` (one variant for a base of any type that writes none, whatever its
|
||||
extruder count). A child therefore inherits the base's arrays at the base's width: an array wider than
|
||||
that is cut to its first values before any child sees it, and a child that adds variants gets those
|
||||
first values padded. So widen an array only on a preset whose own variant list already has the
|
||||
entries; a wide array on a narrow base is silently lost at load, and `check`, which composes at the
|
||||
width each file wrote and judges selectable presets only, misses that cut.
|
||||
- **`include` hands down the template's diff, at its pre-resize width.** An included preset contributes
|
||||
every key where its own composed config (its parent, its own includes and its own keys) differs from
|
||||
the built-in defaults, taken *before* its resize. So keys the template inherits are passed on too,
|
||||
arrays it writes arrive at the width its file wrote, and a key it sets to the built-in default value
|
||||
is not passed on at all, so it cannot override what the includer inherited. Resizing happens on the
|
||||
includer, not on the template.
|
||||
|
||||
## `renamed_from`
|
||||
|
||||
One JSON string, `;`-separated for several old names.
|
||||
|
||||
- Write `"A;B"`, never `"A ; B"`: a space after a `;` is skipped, but a space before it stays part of
|
||||
the name (`"A "`), which 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; a preset that gains
|
||||
a `renamed_from` without it quietly loses 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 the index-name rule, 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*, so a neighbour's `renamed_from` is no model.
|
||||
|
||||
## Failure scopes
|
||||
|
||||
| Scope | Cause |
|
||||
| --- | --- |
|
||||
| **Every vendor except OrcaFilamentLibrary, and all user presets** | a non-string where the index or a `machine_model` expects a string (`"version": 2` at the top level of an index, a numeric `name` or `url`, a non-string `nozzle_diameter` or other model key): `[json.exception.type_error.302] type must be string`, and the validator reports `Validation failed` |
|
||||
| **The whole vendor bundle** | index JSON parse error; unparseable `version`; a listed file missing or unparseable; a value its option cannot take, such as `nil` in a non-nullable key (`Failed loading configuration file`); unresolved `inherits`; two selectable presets with one name; empty or unknown `printer_model` / `printer_variant`; a filament resolving no `filament_id`; a JSON boolean `instantiation` |
|
||||
| **A counted error; the preset still loads** | `instantiation` missing or not `"true"` / `"false"`; keys belonging to another preset type (`contains incorrect keys: …, which were removed`); a non-string inside a `*_list` entry (`invalid value type for <key>`); an `include` naming nothing usable (`can not find include`, loads without it) |
|
||||
| **The rest of the file, logged only** | an array with a non-string element (`[0.4]`, `invalid json array`): that key and every key after it in the file are dropped, and no error is counted |
|
||||
| **One value, logged only** | 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; two bases with one name, or a base and a selectable preset with one name (the first in the index wins); misspelled setting key; missing bed, hotend or cover asset. `check` catches the first two; the others reach users |
|
||||
|
||||
Deleting a file the index still lists surfaces as a *parse error* on line 1 (`unexpected end of input`), not "file
|
||||
not found".
|
||||
|
||||
Selectable preset names are a **single namespace across every vendor**: a duplicate within one vendor
|
||||
is a hard bundle failure, and a duplicate across vendors is reported as `Found duplicated preset: <name>
|
||||
in vendor: <vendor>` and still counts as an error. `check` catches the within-bundle case earlier and
|
||||
more precisely, bases included, and including an *unindexed* twin, which is one `sub_path` edit away
|
||||
from silently becoming the parent every child resolves to (the first registered preset of a name wins,
|
||||
so index order decides). Base names, by contrast, repeat across bundles by design:
|
||||
every bundle may have its own `fdm_process_common` ([uniqueness](naming.md#uniqueness)).
|
||||
|
||||
## Starting a whole new vendor bundle
|
||||
|
||||
Nothing generates one; copy the smallest bundle that resembles the hardware. **`Voxelab`** is the
|
||||
minimal shape: a shared machine base, the model, one variant, a shared process base, two processes, and
|
||||
an empty `filament_list`, so the printer takes the library generics. Do *not* start from a bundle that
|
||||
carries local `fdm_filament_*` copies, which drift from the library, or filament presets that restate
|
||||
most of their 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
|
||||
([naming.md](naming.md)).
|
||||
2. `resources/profiles/<Vendor>.json`: `name`, `version` (`01.00.00.00`), `force_update: "0"`,
|
||||
`description`, and all four `*_list` arrays (empty is fine: `update-index` fills them once the files
|
||||
exist, so this step only needs the bundle metadata to be right).
|
||||
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#machine-the-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.
|
||||
|
||||
## `resources/profiles_template/`
|
||||
|
||||
A separate tree (`Template.json` + `Template/`) holding filament and process templates. It is **not** a
|
||||
scaffold for shipped profiles: the app's "create a custom printer / filament" dialog reads it, 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,241 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Find settings to move onto shared bases, and prove a move changed nothing.
|
||||
|
||||
snapshot OUT.json write every selectable preset's config as the loader stores it
|
||||
compare BEFORE.json report every value that differs from the snapshot; exit 1 if any
|
||||
candidates --vendor V restated values; per base, the settings its presets all share and the
|
||||
defaults with exceptions that would pay
|
||||
|
||||
Every subcommand takes --profiles DIR (default resources/profiles).
|
||||
|
||||
Configs are composed the loader's way: the parent's stored config, then each include at
|
||||
the width its file wrote, then the preset's own keys, and every variant key resized to the
|
||||
preset's own variant list (one variant without one), padded with its first value or cut.
|
||||
A base is stored after that resize, so a variant array wider than a base's list reaches its
|
||||
children cut. Not modelled: the built-in defaults. A key no file in a preset's chain writes
|
||||
loads its default, so compare reports a key written on one side only separately: it is no
|
||||
change when the written value is the option's default in PrintConfig.cpp. Nor is it modelled
|
||||
that an include template does not pass on a key equal to the default. Reads
|
||||
scripts/orca_profile_tool.py.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
# The profile tool lives in <repo>/scripts; this file in <repo>/.claude/skills/orca-profiles/scripts.
|
||||
sys.path[:0] = [os.path.join(os.getcwd(), "scripts"),
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), *[os.pardir] * 4, "scripts")]
|
||||
import orca_profile_tool as tool # noqa: E402
|
||||
|
||||
TYPES = ("machine", "process", "filament")
|
||||
# Keys the loader reads from each file as metadata; neither inherits nor include passes them on.
|
||||
PER_FILE = {"type", "name", "from", "instantiation", "setting_id", "renamed_from", "description",
|
||||
"inherits", "include", "version", "url", "is_custom_defined"}
|
||||
# Keys the app replaces with the selected presets' names before slicing: a file's value never counts.
|
||||
REPLACED = {"print_settings_id", "printer_settings_id", "filament_settings_id"}
|
||||
# What a restructure changes by design, or what never reaches a slice.
|
||||
MOVED_BY_DESIGN = {"inherits", "include"} | REPLACED
|
||||
# Keys that stay in their own file: metadata, identity, and each preset's compatibility.
|
||||
NEVER_SHARED = PER_FILE | REPLACED | {"filament_id", "compatible_printers", "compatible_prints",
|
||||
"printer_variant", "printer_model"}
|
||||
|
||||
|
||||
class Tree:
|
||||
def __init__(self, profiles_dir):
|
||||
self.dir = profiles_dir
|
||||
self.scheme = tool._variant_scheme()
|
||||
self.vendors = tool.list_vendor_names(profiles_dir)
|
||||
self.bundles = {v: tool.load_vendor_configs(profiles_dir, v) for v in self.vendors}
|
||||
self.cache = {}
|
||||
|
||||
def lookup(self, vendor, ptype, name, in_ofl=False):
|
||||
"""(vendor the name resolves in, (rel, data)); filaments fall back to the library."""
|
||||
if not in_ofl and name in self.bundles[vendor][ptype]:
|
||||
return vendor, self.bundles[vendor][ptype][name]
|
||||
if ptype == "filament" and tool.OFL in self.bundles and name in self.bundles[tool.OFL][ptype]:
|
||||
return tool.OFL, self.bundles[tool.OFL][ptype][name]
|
||||
return None, None
|
||||
|
||||
def composed(self, vendor, ptype, name, drop=None, seen=frozenset()):
|
||||
"""Config before the preset's own resize: parent stored, includes, own keys."""
|
||||
owner, found = self.lookup(vendor, ptype, name, vendor == tool.OFL)
|
||||
if found is None or (owner, name) in seen:
|
||||
return {}
|
||||
seen = seen | {(owner, name)}
|
||||
data = found[1]
|
||||
config = {}
|
||||
if data.get("inherits"):
|
||||
config.update(self.stored(owner, ptype, data["inherits"], seen))
|
||||
include = data.get("include") or []
|
||||
for included in [include] if isinstance(include, str) else include:
|
||||
if included in self.bundles[owner][ptype]:
|
||||
config.update(self.composed(owner, ptype, included, seen=seen))
|
||||
config = {k: v for k, v in config.items() if k not in PER_FILE}
|
||||
config.update((k, v) for k, v in data.items() if k != drop)
|
||||
return config
|
||||
|
||||
def stored(self, vendor, ptype, name, seen=frozenset()):
|
||||
key = (vendor, ptype, name)
|
||||
if key not in self.cache:
|
||||
self.cache[key] = self.resize(ptype, self.composed(vendor, ptype, name, seen=seen))
|
||||
return self.cache[key]
|
||||
|
||||
def resize(self, ptype, config):
|
||||
list_key, strides = self.scheme[ptype]
|
||||
length = len(tool._as_list(config[list_key])) if list_key in config else 1
|
||||
out = dict(config)
|
||||
for key, stride in strides.items():
|
||||
if key in out:
|
||||
values = tool._as_list(out[key])
|
||||
need = length * stride
|
||||
out[key] = values[:need] + values[:1] * (need - len(values))
|
||||
return out
|
||||
|
||||
def presets(self, vendors=None, ptypes=TYPES):
|
||||
for vendor in vendors or self.vendors:
|
||||
for ptype in ptypes:
|
||||
for name, (rel, data) in sorted(self.bundles[vendor][ptype].items()):
|
||||
yield vendor, ptype, name, rel, data
|
||||
|
||||
|
||||
def snapshot(tree):
|
||||
return {f"{vendor}/{ptype}/{name}": {k: v for k, v in tree.stored(vendor, ptype, name).items()
|
||||
if k not in MOVED_BY_DESIGN}
|
||||
for vendor, ptype, name, _rel, data in tree.presets()
|
||||
if data.get("instantiation") == "true"}
|
||||
|
||||
|
||||
def compare(before, after):
|
||||
changed, one_sided = 0, []
|
||||
for preset in sorted(before.keys() | after.keys()):
|
||||
old, new = before.get(preset), after.get(preset)
|
||||
if old is None or new is None:
|
||||
print(f"{preset}: {'added' if old is None else 'removed'}")
|
||||
changed += 1
|
||||
continue
|
||||
for key in sorted(old.keys() | new.keys()):
|
||||
if key not in old or key not in new:
|
||||
one_sided.append(f"{preset}: {key} "
|
||||
f"{json.dumps(old[key]) if key in old else '(built-in default)'} -> "
|
||||
f"{json.dumps(new[key]) if key in new else '(built-in default)'}")
|
||||
elif old[key] != new[key]:
|
||||
print(f"{preset}: {key} {json.dumps(old[key])} -> {json.dumps(new[key])}")
|
||||
changed += 1
|
||||
for line in one_sided:
|
||||
print(line)
|
||||
print(f"{changed} difference(s)")
|
||||
if one_sided:
|
||||
print(f"{len(one_sided)} key(s) written on one side only: each is a difference unless the "
|
||||
f"written value is the option's default in src/libslic3r/PrintConfig.cpp")
|
||||
return changed + len(one_sided)
|
||||
|
||||
|
||||
def candidates(tree, vendor, ptypes, group_by):
|
||||
for ptype in ptypes:
|
||||
entries = {name: data for _v, _t, name, _rel, data in tree.presets([vendor], (ptype,))}
|
||||
selectable = [n for n, d in entries.items() if d.get("instantiation") == "true"]
|
||||
stored = {n: tree.stored(vendor, ptype, n) for n in entries}
|
||||
list_key = tree.scheme[ptype][0]
|
||||
|
||||
# Restated: a key a file writes that it would inherit unchanged without writing it.
|
||||
restated = {}
|
||||
for name, data in entries.items():
|
||||
restated[name] = sorted(
|
||||
k for k in data if k not in NEVER_SHARED and k != list_key
|
||||
and (data.get("inherits") or data.get("include"))
|
||||
and tree.resize(ptype, tree.composed(vendor, ptype, name, drop=k)).get(k) == stored[name][k])
|
||||
if restated[name]:
|
||||
print(f"{vendor}/{ptype} {name}: restates what it inherits: {', '.join(restated[name])}")
|
||||
|
||||
# Groups: every preset with selectable presets below it, or the --group-by values.
|
||||
chain = {n: [] for n in selectable}
|
||||
for name in selectable:
|
||||
node = entries[name].get("inherits")
|
||||
while node in entries and node not in chain[name]:
|
||||
chain[name].append(node)
|
||||
node = entries[node].get("inherits")
|
||||
groups = defaultdict(list)
|
||||
for name in selectable:
|
||||
if group_by:
|
||||
groups[json.dumps(stored[name].get(group_by))].append(name)
|
||||
else:
|
||||
for base in chain[name]:
|
||||
groups[base].append(name)
|
||||
printed = {}
|
||||
for label, members in sorted(groups.items(), key=lambda g: -len(g[1])):
|
||||
if len(members) < 2 or label == "null":
|
||||
continue
|
||||
if frozenset(members) in printed:
|
||||
print(f"\n{vendor}/{ptype} {label}: the same presets as {printed[frozenset(members)]}")
|
||||
continue
|
||||
common = [b for b in chain[members[0]] if all(b in chain[m] for m in members[1:])]
|
||||
home = common[0] if group_by and common else None if group_by else label
|
||||
|
||||
def below(m):
|
||||
"""m and the files between it and the group's home."""
|
||||
return [m] + chain[m][:chain[m].index(home)] if home in chain[m] else [m]
|
||||
shared, defaults = [], []
|
||||
for key in sorted(set().union(*(entries[m].keys() for m in members)) - NEVER_SHARED):
|
||||
loaded = [json.dumps(stored[m].get(key)) for m in members]
|
||||
counts = Counter(loaded).most_common(2)
|
||||
if len(counts) == 1:
|
||||
writers = sum(key in entries[m] and key not in restated[m] for m in members)
|
||||
if writers >= 2:
|
||||
shared.append(f"{key} ({writers} write it)")
|
||||
continue
|
||||
(top, held), (_, runner_up) = counts
|
||||
if held == runner_up or top == "null" or home is None:
|
||||
continue
|
||||
# Balance 5: w presets drop their copy; a presets that take the key from the home
|
||||
# (no file on their way to it writes it) and load another value must write theirs.
|
||||
w = sum(key in entries[m] and v == top for m, v in zip(members, loaded))
|
||||
a = sum(v != top and not any(key in entries[f] for f in below(m))
|
||||
for m, v in zip(members, loaded))
|
||||
if w - a > 1:
|
||||
shown = top if len(top) <= 40 else top[:37] + "..."
|
||||
defaults.append(f"{key} = {shown}: {held} load it, {w} write it, "
|
||||
f"{a} would have to write their own")
|
||||
if shared or defaults:
|
||||
where = f"; nearest common base {common[0]}" if group_by and common else ""
|
||||
title = f"{group_by} = {label}" if group_by else label
|
||||
print(f"\n{vendor}/{ptype} {title}: {len(members)} presets{where}")
|
||||
printed[frozenset(members)] = title
|
||||
for line in shared:
|
||||
print(f" {line}")
|
||||
if defaults:
|
||||
print(" default with exceptions:")
|
||||
for line in defaults:
|
||||
print(f" {line}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
profiles = argparse.ArgumentParser(add_help=False)
|
||||
profiles.add_argument("--profiles", default=os.path.join("resources", "profiles"),
|
||||
help="profiles directory (default: resources/profiles)")
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
sub.add_parser("snapshot", parents=[profiles]).add_argument("out")
|
||||
sub.add_parser("compare", parents=[profiles]).add_argument("before")
|
||||
cand = sub.add_parser("candidates", parents=[profiles])
|
||||
cand.add_argument("--vendor", required=True)
|
||||
cand.add_argument("--type", choices=TYPES, action="append")
|
||||
cand.add_argument("--group-by", help="group selectable presets by this key's value instead of by "
|
||||
"base: printer_model, gcode_flavor, extruder_type, filament_id, layer_height, ...")
|
||||
args = parser.parse_args()
|
||||
tree = Tree(args.profiles)
|
||||
if args.command == "snapshot":
|
||||
presets = snapshot(tree)
|
||||
with open(args.out, "w", encoding="utf-8") as f:
|
||||
json.dump(presets, f, sort_keys=True)
|
||||
print(f"{len(presets)} selectable presets written to {args.out}")
|
||||
elif args.command == "compare":
|
||||
with open(args.before, encoding="utf-8") as f:
|
||||
sys.exit(1 if compare(json.load(f), snapshot(tree)) else 0)
|
||||
else:
|
||||
candidates(tree, args.vendor, args.type or TYPES, args.group_by)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -183,11 +183,9 @@ jobs:
|
||||
os: ${{ vars.SELF_HOSTED && 'orca-macos-arm64' || 'macos-14' }}
|
||||
artifact: ${{ github.sha }}-tests-macos-arm64
|
||||
test-dir: build/arm64/tests
|
||||
# Slice a two-colour cube through every shipped printer, and through every
|
||||
# system process/filament whose templates no printer's own slice reaches, so
|
||||
# every custom g-code and filename_format shipped is expanded (names in {if}
|
||||
# branches not taken included) - catches slicing regressions the static
|
||||
# profile checks and unit tests can't see.
|
||||
# Slice a two-colour cube through every shipped printer so all custom g-code
|
||||
# (change_filament_gcode, machine start/end, etc.) is expanded - catches
|
||||
# slicing regressions the static profile checks and unit tests can't see.
|
||||
# Profile-only PRs are covered by check_profiles.yml's nightly binary; this
|
||||
# covers src/engine PRs with the PR-built binary.
|
||||
slice_check_linux:
|
||||
@@ -332,13 +330,12 @@ jobs:
|
||||
# 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. run-tests on the same module builds the test
|
||||
# binaries.
|
||||
- name: Inject commit hash, run-tests and cache buster into Flatpak manifest
|
||||
# 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 }}
|
||||
run: |
|
||||
sed -i "/name: OrcaSlicer/{n;s|buildsystem: simple|buildsystem: simple\n run-tests: true\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 flatpak_builder_cache_buster: \"$flatpak_builder_cache_buster\"\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
|
||||
@@ -384,6 +381,9 @@ jobs:
|
||||
save-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.
|
||||
@@ -415,14 +415,6 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
api="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/caches"
|
||||
# The save step reports success even when its tar failed, so keep
|
||||
# the older entries unless the new one is listed.
|
||||
if ! curl -sSf -H "Authorization: Bearer $GH_TOKEN" \
|
||||
"$api?ref=$GITHUB_REF&key=$CCACHE_ENTRY" \
|
||||
| jq -e --arg entry "$CCACHE_ENTRY" 'any(.actions_caches[]; .key == $entry)' > /dev/null; then
|
||||
echo "$CCACHE_ENTRY was not saved; keeping the older entries."
|
||||
exit 0
|
||||
fi
|
||||
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" \
|
||||
@@ -457,13 +449,10 @@ jobs:
|
||||
# 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), the printers/ maps, and
|
||||
# the icon SVGs (NativeCommands icon names are checked against them).
|
||||
# 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 ! -name images -exec rm -rf {} +
|
||||
# Only the SVGs are read; the png/ico/icns/gif assets are ~35MB of dead weight.
|
||||
find "$d/resources/images" -mindepth 1 -maxdepth 1 ! -name '*.svg' -exec rm -rf {} + 2>/dev/null || true
|
||||
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
|
||||
|
||||
@@ -448,26 +448,9 @@ jobs:
|
||||
|
||||
- name: Install nsis
|
||||
if: runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
shell: pwsh
|
||||
# The Chocolatey community feed intermittently 504s, and `choco install`
|
||||
# exits 0 when package resolution fails that way. Unchecked, the job then
|
||||
# builds for ~25 minutes before `cpack -G NSIS` reports a missing makensis.
|
||||
# Retry the install and verify makensis itself, so a real failure stops here.
|
||||
run: |
|
||||
dir "C:/Program Files (x86)/Windows Kits/10/Include"
|
||||
$nsisDir = Join-Path ${env:ProgramFiles(x86)} 'NSIS'
|
||||
$makensis = Join-Path $nsisDir 'makensis.exe'
|
||||
for ($attempt = 1; $attempt -le 3 -and -not (Test-Path $makensis); $attempt++) {
|
||||
if ($attempt -gt 1) { Start-Sleep -Seconds (15 * $attempt) }
|
||||
Write-Host "::group::choco install nsis (attempt $attempt)"
|
||||
choco install nsis --yes --no-progress
|
||||
Write-Host "::endgroup::"
|
||||
}
|
||||
if (-not (Test-Path $makensis)) {
|
||||
throw "NSIS install failed: $makensis not found after 3 attempts."
|
||||
}
|
||||
& $makensis /VERSION
|
||||
$nsisDir | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8
|
||||
choco install nsis
|
||||
|
||||
- name: Build slicer Win
|
||||
if: runner.os == 'Windows'
|
||||
@@ -702,18 +685,6 @@ jobs:
|
||||
name: OrcaSlicer_profile_validator_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }}
|
||||
path: './build/src/Release/OrcaSlicer_profile_validator'
|
||||
|
||||
# generate_system_cache is what scripts/build_preset_cache.sh bakes the
|
||||
# <vendor>.opc caches with; it was already built by the "Build system
|
||||
# preset cache (Linux)" step above. The .opc format is 64-bit
|
||||
# little-endian native, i.e. identical across every platform Orca ships,
|
||||
# so only the Linux binary is published.
|
||||
- name: Upload generate_system_cache Ubuntu
|
||||
if: ${{ ! env.ACT && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: generate_system_cache_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }}
|
||||
path: './build/src/dev-utils/Release/generate_system_cache'
|
||||
|
||||
- name: Deploy Ubuntu release
|
||||
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
@@ -744,17 +715,6 @@ jobs:
|
||||
asset_content_type: application/octet-stream
|
||||
max_releases: 1
|
||||
|
||||
- name: Deploy Ubuntu generate_system_cache release
|
||||
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
with:
|
||||
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
|
||||
release_id: 137995723
|
||||
asset_path: ./build/src/dev-utils/Release/generate_system_cache
|
||||
asset_name: generate_system_cache_Linux${{ env.ubuntu-ver-str }}_nightly
|
||||
asset_content_type: application/octet-stream
|
||||
max_releases: 1
|
||||
|
||||
- name: Deploy orca_custom_preset_tests
|
||||
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
@@ -798,13 +758,6 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# The save step reports success even when its tar failed, so keep
|
||||
# the older entries unless the new one is listed.
|
||||
if ! gh cache list --ref "$GITHUB_REF" --key "$CCACHE_ENTRY" --json key \
|
||||
| jq -e --arg entry "$CCACHE_ENTRY" 'any(.[]; .key == $entry)' > /dev/null; then
|
||||
echo "$CCACHE_ENTRY was not saved; keeping the older entries."
|
||||
exit 0
|
||||
fi
|
||||
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' \
|
||||
|
||||
@@ -14,9 +14,6 @@ on:
|
||||
# this workflow.
|
||||
- 'resources/printers/**'
|
||||
- 'scripts/**'
|
||||
# orca_profile_tool.py reads the variant key sets from PrintConfig.cpp, and its
|
||||
# tests the obsolete keys, so a PR changing either must be checked against the profiles.
|
||||
- 'src/libslic3r/PrintConfig.cpp'
|
||||
- ".github/workflows/check_profiles.yml"
|
||||
|
||||
workflow_dispatch:
|
||||
@@ -74,10 +71,8 @@ jobs:
|
||||
set +e
|
||||
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 2>&1 | tee ${{ runner.temp }}/validate_system.log
|
||||
exit ${PIPESTATUS[0]}
|
||||
# Slice a two-colour cube through every printer, and through every system process/filament whose
|
||||
# templates no printer's own slice reaches, so every custom g-code and filename_format shipped is
|
||||
# expanded (names in {if} branches not taken included) - catches undefined-placeholder /
|
||||
# invalid-flow bugs the static checks above cannot see.
|
||||
# Slice a two-colour cube through every printer so all custom g-code (incl. change_filament_gcode)
|
||||
# is expanded - catches undefined-placeholder / invalid-flow bugs the static checks above cannot see.
|
||||
- name: validate slice (expand custom g-code)
|
||||
id: validate_slice
|
||||
continue-on-error: true
|
||||
@@ -85,8 +80,8 @@ 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.
|
||||
# All vendors' filament_id collisions were fixed (see scripts/filament_id_snapshot.json),
|
||||
# so the duplicate-filament-subtype check runs tree-wide.
|
||||
- name: validate filament subtype check
|
||||
id: validate_filament_subtypes
|
||||
continue-on-error: true
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
name: Daily OFL OTA Update
|
||||
|
||||
run-name: Daily OFL OTA Update [OFL barrier]
|
||||
|
||||
# This workflow is intended for creating and publishing the OrcaFilamentLibrary (OFL) OPC package to
|
||||
# https://github.com/OrcaSlicer/orcaslicer-profiles, which generates an OTA update.
|
||||
# This cronjob runs daily at 00:00 UTC every day and scans main plus every release/vX.Y.Z branch for
|
||||
# changes to resources/profiles/OrcaFilamentLibrary since that branch's own last successful run. Any
|
||||
# branch with no changes is skipped; each changed branch gets its own post_merge_profiles.yml dispatch.
|
||||
#
|
||||
# OFL has no dedicated FOLDER_MERGERS grant (it isn't merged through the PR merge-bot delegation
|
||||
# scheme), so post_merge_profiles.yml is dispatched with an explicit `vendor` input, which that
|
||||
# workflow trusts and uses to bypass the FOLDER_MERGERS check for this trigger. That same explicit-
|
||||
# vendor-dispatch path is also what makes post_merge_profiles.yml call the OTA auto-publish API after
|
||||
# uploading - see post_merge_profiles.yml for both sides of that contract.
|
||||
#
|
||||
# Each run captures a timestamp, dispatches the needed OFL publishers, waits for
|
||||
# all of them to finish, then clears the pending-publish table once. Changes merged
|
||||
# after that timestamp remain pending for the next run.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: write # list this workflow's past runs and dispatch post_merge_profiles.yml
|
||||
contents: read
|
||||
|
||||
env:
|
||||
VENDOR: OrcaFilamentLibrary
|
||||
|
||||
jobs:
|
||||
daily-job:
|
||||
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Capture start timestamp
|
||||
id: start
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
timestamp="$(date -u +%s)"
|
||||
echo "timestamp=$timestamp" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
# Full history: the per-branch "since last successful run" check below
|
||||
# needs to look arbitrarily far back if a prior run failed or was skipped.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fetch all branches
|
||||
shell: bash
|
||||
run: git fetch origin '+refs/heads/*:refs/remotes/origin/*'
|
||||
|
||||
- name: Scan branches and publish changed OFL profiles
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SCAN_UNTIL: ${{ steps.start.outputs.timestamp }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
mapfile -t branches < <(
|
||||
gh api "repos/${{ github.repository }}/branches" --paginate --jq '.[].name' \
|
||||
| grep -E '^(main|release/v[0-9]+\.[0-9]+\.[0-9]+)$' | sort -u
|
||||
)
|
||||
|
||||
# The cron run is the checkpoint: a successful run means every
|
||||
# dispatched branch publisher completed and the pending queue was
|
||||
# cleared. Manual or push-triggered post_merge_profiles runs are not
|
||||
# checkpoints for this scan.
|
||||
successful_cron_runs="$(gh api --method GET \
|
||||
"repos/${{ github.repository }}/actions/workflows/ofl-ota-cronjob.yml/runs" \
|
||||
-f status=success -f branch=main -f per_page=100 --paginate \
|
||||
--jq '.workflow_runs[] | select((.display_title // "") | contains("[OFL barrier]"))')"
|
||||
since="$(jq -rs 'sort_by(.run_started_at) | last.run_started_at // empty' <<< "$successful_cron_runs")"
|
||||
|
||||
for branch in "${branches[@]}"; do
|
||||
echo "::group::$branch"
|
||||
|
||||
if [ -z "$since" ]; then
|
||||
echo "No prior successful OFL cron run; checking $branch through $SCAN_UNTIL."
|
||||
changed_files="$(git log --until="$SCAN_UNTIL" --name-only --pretty=format: "origin/$branch" -- \
|
||||
resources/profiles/OrcaFilamentLibrary resources/profiles/OrcaFilamentLibrary.json \
|
||||
| sed '/^$/d')"
|
||||
else
|
||||
changed_files="$(git log --since="$since" --until="$SCAN_UNTIL" --name-only --pretty=format: "origin/$branch" -- \
|
||||
resources/profiles/OrcaFilamentLibrary resources/profiles/OrcaFilamentLibrary.json \
|
||||
| sed '/^$/d')"
|
||||
fi
|
||||
|
||||
if [ -n "$changed_files" ]; then
|
||||
echo "OFL changed on $branch from ${since:-the beginning} through $SCAN_UNTIL:"
|
||||
echo "$changed_files"
|
||||
changed=true
|
||||
else
|
||||
echo "No OFL changes on $branch through $SCAN_UNTIL."
|
||||
changed=false
|
||||
fi
|
||||
|
||||
if [ "$changed" = true ]; then
|
||||
dispatch_id="${GITHUB_RUN_ID}-${branch//\//-}"
|
||||
# Record successful dispatches for the barrier step below.
|
||||
# Branches whose workflow predates workflow_dispatch are skipped
|
||||
# with a warning, as they were before the barrier was added.
|
||||
if gh workflow run post_merge_profiles.yml \
|
||||
--repo "${{ github.repository }}" \
|
||||
--ref "$branch" \
|
||||
-f vendor="$VENDOR" -f auto_publish=true \
|
||||
-f ofl_cron_dispatch_id="$dispatch_id"; then
|
||||
printf '%s\t%s\n' "$branch" "$dispatch_id" >> "$RUNNER_TEMP/ofl-dispatches.tsv"
|
||||
else
|
||||
echo "::warning::skipping $branch because post_merge_profiles.yml could not be dispatched at that ref"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "::endgroup::"
|
||||
done
|
||||
|
||||
- name: Wait for OFL publishers
|
||||
id: wait
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
DISPATCHES_FILE: ${{ runner.temp }}/ofl-dispatches.tsv
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ ! -s "$DISPATCHES_FILE" ]; then
|
||||
echo "No OFL publisher workflows were dispatched; pending queue will not be cleared."
|
||||
echo "publishers_dispatched=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
: > "$RUNNER_TEMP/ofl-run-ids.tsv"
|
||||
while IFS=$'\t' read -r branch dispatch_id; do
|
||||
[ -n "$branch" ] || continue
|
||||
echo "Waiting for OFL publisher on $branch ($dispatch_id)"
|
||||
|
||||
run_id=""
|
||||
for _ in {1..120}; do
|
||||
runs_json="$(gh api --method GET \
|
||||
"repos/${{ github.repository }}/actions/workflows/post_merge_profiles.yml/runs" \
|
||||
-f branch="$branch" -f event=workflow_dispatch -f per_page=100)"
|
||||
run_id="$(jq -r --arg marker "[OFL cron $dispatch_id]" \
|
||||
'[.workflow_runs[] | select((.display_title // "") | contains($marker))] \
|
||||
| sort_by(.created_at) | last | .id // empty' <<< "$runs_json")"
|
||||
[ -n "$run_id" ] && break
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [ -z "$run_id" ]; then
|
||||
echo "::error::could not find dispatched post_merge_profiles run for $branch ($dispatch_id)"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\t%s\n' "$branch" "$run_id" >> "$RUNNER_TEMP/ofl-run-ids.tsv"
|
||||
done < "$DISPATCHES_FILE"
|
||||
|
||||
all_success=true
|
||||
while IFS=$'\t' read -r branch run_id; do
|
||||
[ -n "$run_id" ] || continue
|
||||
echo "Watching OFL publisher run $run_id for $branch"
|
||||
if ! gh run watch "$run_id" --repo "${{ github.repository }}" --exit-status; then
|
||||
all_success=false
|
||||
fi
|
||||
done < "$RUNNER_TEMP/ofl-run-ids.tsv"
|
||||
|
||||
if [ "$all_success" != true ]; then
|
||||
echo "::error::one or more OFL publisher workflows failed; pending queue will not be cleared"
|
||||
exit 1
|
||||
fi
|
||||
echo "publishers_dispatched=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Clear OFL pending queue
|
||||
if: steps.wait.outputs.publishers_dispatched == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
OTA_API_BASE_URL: ${{ vars.OTA_API_BASE_URL }}
|
||||
OTA_API_KEY: ${{ secrets.OFL_OTA_PUBLISH_KEY }}
|
||||
TIMESTAMP: ${{ steps.start.outputs.timestamp }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[ -n "$OTA_API_BASE_URL" ] || { echo "::error::vars.OTA_API_BASE_URL is not set"; exit 1; }
|
||||
[ -n "$OTA_API_KEY" ] || { echo "::error::secrets.OFL_OTA_PUBLISH_KEY is not set"; exit 1; }
|
||||
|
||||
resp_file="$RUNNER_TEMP/ota-pending-clear-response.json"
|
||||
status="$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST \
|
||||
"${OTA_API_BASE_URL%/}/api/v1/ota/ofl/pending/clear?timestamp=$TIMESTAMP" \
|
||||
-H "Authorization: Bearer $OTA_API_KEY")"
|
||||
body="$(cat "$resp_file")"
|
||||
echo "$body"
|
||||
|
||||
if [ "$status" != "200" ]; then
|
||||
echo "::error::OTA pending-clear call failed with HTTP $status"
|
||||
exit 1
|
||||
fi
|
||||
@@ -5,9 +5,8 @@
|
||||
# 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; a manual run can
|
||||
# name another branch, or pin one build by its run id. Nothing here gates a
|
||||
# build or a PR.
|
||||
# sources checked out at the commit that build was made from. Nothing here
|
||||
# gates a build or a PR.
|
||||
name: Parity Nightly
|
||||
|
||||
on:
|
||||
@@ -21,13 +20,9 @@ on:
|
||||
required: false
|
||||
default: "main"
|
||||
build_branch:
|
||||
description: "branch whose newest successful build_all artifact to test (a PR build is the PR merged into its base; sources are checked out at the PR head)"
|
||||
description: "branch whose latest successful build_all artifact to test"
|
||||
required: false
|
||||
default: "main"
|
||||
build_run_id:
|
||||
description: "build_all run id to test instead of build_branch's newest (same PR caveat)"
|
||||
required: false
|
||||
default: ""
|
||||
fixtures:
|
||||
description: "harness fixture ids, space-separated (empty = all)"
|
||||
required: false
|
||||
@@ -55,53 +50,14 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
BRANCH: ${{ inputs.build_branch || 'main' }}
|
||||
RUN_ID: ${{ inputs.build_run_id }}
|
||||
SCHEDULED: ${{ github.event_name == 'schedule' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -n "$RUN_ID" ]; then
|
||||
[[ $RUN_ID =~ ^[0-9]+$ ]] || { echo "build_run_id must be a numeric run id, got '$RUN_ID'" >&2; exit 1; }
|
||||
# a pinned build is read directly, not through a search; it must come
|
||||
# from this repository, because the later jobs check out its commit here
|
||||
found=$(gh api "repos/$GH_REPO/actions/runs/$RUN_ID" --jq \
|
||||
'select(.path == ".github/workflows/build_all.yml" and .conclusion == "success"
|
||||
and .head_repository.full_name == env.GH_REPO)
|
||||
| "\(.id) \(.head_sha) \(.created_at)"')
|
||||
[ -n "$found" ] || { echo "run $RUN_ID is not a successful build_all run of $GH_REPO" >&2; exit 1; }
|
||||
else
|
||||
# GitHub serves filtered run listings (branch=, status=, head_sha=, ...)
|
||||
# from a search index that has returned weeks-old results, while the
|
||||
# unfiltered listing stays current, so list unfiltered and filter here.
|
||||
# The repository check keeps out fork PRs whose branch has the same
|
||||
# name. A feature branch is normally built only for its PR, and a PR
|
||||
# build compiles the PR merged into its base rather than head_sha, so
|
||||
# a build of the branch itself (push or dispatch) is preferred when
|
||||
# the same page has one.
|
||||
pick='([.workflow_runs[] | select(.head_branch == env.BRANCH and .conclusion == "success"
|
||||
and .head_repository.full_name == env.GH_REPO)]
|
||||
| map(select(.event != "pull_request"))[0] // .[0])
|
||||
| select(.) | "\(.id) \(.head_sha) \(.created_at)"'
|
||||
# a page of 100 runs spans about a day and a half; a manual run may
|
||||
# target a branch that last built weeks ago
|
||||
pages=3
|
||||
if [ "$SCHEDULED" != true ]; then pages=20; fi
|
||||
found=""
|
||||
for page in $(seq "$pages"); do
|
||||
found=$(gh api "repos/$GH_REPO/actions/workflows/build_all.yml/runs?per_page=100&page=$page" --jq "$pick")
|
||||
if [ -n "$found" ]; then break; fi
|
||||
done
|
||||
[ -n "$found" ] || { echo "no successful $BRANCH build among the last $((pages * 100)) build_all runs; pass build_run_id to test an older one" >&2; exit 1; }
|
||||
fi
|
||||
read -r run_id head_sha created <<< "$found"
|
||||
# the nightly fails rather than report on a stale build
|
||||
if [ "$SCHEDULED" = true ] && [ $(( $(date +%s) - $(date -d "$created" +%s) )) -gt 172800 ]; then
|
||||
echo "newest $BRANCH build $run_id is from $created, over 48 hours old" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'run_id=%s\nhead_sha=%s\n' "$run_id" "$head_sha" >> "$GITHUB_OUTPUT"
|
||||
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"
|
||||
echo "Testing build [$run_id](https://github.com/$GH_REPO/actions/runs/$run_id) of \`$head_sha\`, built $created" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
effect:
|
||||
name: Override sweep effect stage (shard ${{ matrix.shard }})
|
||||
|
||||
@@ -1,464 +0,0 @@
|
||||
name: Post-merge profiles
|
||||
|
||||
run-name: >-
|
||||
Post-merge profiles${{
|
||||
inputs.ofl_cron_dispatch_id != '' &&
|
||||
inputs.vendor == 'OrcaFilamentLibrary' &&
|
||||
(inputs.auto_publish == true || inputs.auto_publish == 'true') &&
|
||||
format(' [OFL cron {0}]', inputs.ofl_cron_dispatch_id) ||
|
||||
''
|
||||
}}
|
||||
|
||||
# Push-triggered counterpart to check_profiles.yml (which only gates PRs). When a
|
||||
# profile change lands on main or a release branch, rebuild the affected vendors'
|
||||
# binary preset caches (<vendor>.opc) and publish each as a versioned ZIP asset on
|
||||
# a per-Orca-version release of the profiles repo. From there OrcaCloud's OTA
|
||||
# Manager lists the asset, a maintainer attaches a changelog and hits Publish, and
|
||||
# only then does it become a live OTA update - this workflow does none of that
|
||||
# last part (no changelog, no R2, no webhook).
|
||||
#
|
||||
# A workflow_dispatch carrying a `vendor` input (e.g. the daily OFL cron - OFL has
|
||||
# no FOLDER_MERGERS grant, since it isn't merged through the PR merge-bot delegation
|
||||
# scheme) publishes that vendor directly and skips the FOLDER_MERGERS check below.
|
||||
# workflow_dispatch is already a trusted, explicit trigger, unlike the automatic
|
||||
# push-diff path the FOLDER_MERGERS check exists to gate.
|
||||
#
|
||||
# Separately, an ordinary push whose diff touches an OrcaFilamentLibrary company
|
||||
# folder (resources/profiles/OrcaFilamentLibrary/filament/<Company>/**) records
|
||||
# that PR as pending via POST /api/v1/ota/ofl/pending, regardless of whether
|
||||
# OrcaFilamentLibrary as a whole is authorized to publish in this same run - a
|
||||
# partner's OTA Manager dashboard should see a merged PR immediately, well
|
||||
# before the daily cron actually builds and publishes it.
|
||||
#
|
||||
# Asset contract expected by OrcaCloud's release scanner:
|
||||
# ^(\d+\.\d+\.\d+)_([^_]+)_(\d+(?:\.\d+){3})_(\d{12})\.zip$
|
||||
# <orca_ver>_<vendor>_<profile_version>_<UTC yyyymmddHHMM>.zip (zip root: <vendor>.opc)
|
||||
#
|
||||
# Setup (App + secrets): docs/ota/post-merge-profiles-setup.md
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
# once v2.5.0 stable is released, this will be removed, so nightly won't receive OTA updates.
|
||||
- main
|
||||
# release/vX.Y.Z point-release branches only, not the release/vX.Y working
|
||||
# branch profile PRs land on first - "v*.*.*" requires two literal dots,
|
||||
# which release/vX.Y (one dot) doesn't have.
|
||||
- release/v*.*.*
|
||||
paths:
|
||||
- 'resources/profiles/**'
|
||||
- '.github/workflows/post_merge_profiles.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
vendor:
|
||||
description: >-
|
||||
Publish only this vendor, bypassing the FOLDER_MERGERS grant check.
|
||||
For trusted explicit dispatches only (e.g. the OFL nightly cron).
|
||||
Leave empty to fall back to diffing the triggering commit.
|
||||
required: false
|
||||
type: string
|
||||
auto_publish:
|
||||
description: >-
|
||||
After publishing, also call the OTA auto-publish API to go live
|
||||
immediately, skipping the human changelog/Publish step. Separate
|
||||
from `vendor` on purpose: a maintainer can dispatch with just
|
||||
`vendor` set to rebuild/republish an asset without it going live.
|
||||
Only the OFL nightly cron should set this to true.
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
ofl_cron_dispatch_id:
|
||||
description: >-
|
||||
Unique marker supplied by the trusted OFL daily cron so it can find
|
||||
and wait for this dispatched workflow run.
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read # commits/{sha}/pulls lookup in the OFL-pending step
|
||||
|
||||
# One run per branch; let a run finish rather than cancel it, since it publishes.
|
||||
concurrency:
|
||||
group: post-merge-profiles-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
# generate_system_cache is published to this repo's own nightly-builds release
|
||||
# by build_orca.yml's Linux leg. The job guard pins github.repository to
|
||||
# OrcaSlicer/OrcaSlicer, so this resolves there.
|
||||
TOOL_REPO: ${{ github.repository }}
|
||||
TOOL_ASSET: generate_system_cache_Linux_Ubuntu2404_nightly
|
||||
# Where per-vendor ZIP assets are published; OrcaCloud's OTA reads this repo.
|
||||
PROFILES_OWNER: OrcaSlicer
|
||||
PROFILES_REPO: orcaslicer-profiles
|
||||
|
||||
jobs:
|
||||
publish_profile_caches:
|
||||
name: Publish profile caches
|
||||
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' }}
|
||||
# FOLDER_MERGERS is an environment-scoped variable, shared with the PR
|
||||
# merge bot. Keep this environment free of protection rules so this
|
||||
# push-triggered job does not wait for a reviewer.
|
||||
environment: merge-delegation
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
# Enough history to reach github.event.before for the changed-vendor
|
||||
# diff on a normal push; deeper pushes fall back to HEAD^..HEAD in the
|
||||
# step below. fetch-depth: 0 would clone all of OrcaSlicer's history.
|
||||
fetch-depth: 50
|
||||
|
||||
- name: Resolve changed vendors
|
||||
id: vendors
|
||||
shell: bash
|
||||
env:
|
||||
FOLDER_MERGERS: ${{ vars.FOLDER_MERGERS }}
|
||||
DISPATCH_VENDOR: ${{ github.event_name == 'workflow_dispatch' && inputs.vendor || '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# A vendor has a manifest plus either a preset directory or a version
|
||||
# field; this drops non-vendor files such as blacklist.json. Shared by
|
||||
# both the explicit-dispatch path below and the push-diff path further
|
||||
# down, so the definition of "valid vendor" can't drift between them.
|
||||
is_valid_vendor() {
|
||||
local v="$1"
|
||||
local json="resources/profiles/$v.json"
|
||||
[ -f "$json" ] && { [ -d "resources/profiles/$v" ] || jq -e '.version' "$json" >/dev/null 2>&1; }
|
||||
}
|
||||
|
||||
# Explicit vendor dispatch (e.g. the OFL cron): trust the caller and
|
||||
# skip both the git-diff detection and the FOLDER_MERGERS check below.
|
||||
if [ -n "$DISPATCH_VENDOR" ]; then
|
||||
v="$DISPATCH_VENDOR"
|
||||
# Becomes part of the release asset filename and the OTA API's
|
||||
# payload; keep it to the same charset every real vendor name uses.
|
||||
if ! [[ "$v" =~ ^[A-Za-z0-9]+$ ]]; then
|
||||
echo "::error::vendor '$v' must be alphanumeric"
|
||||
exit 1
|
||||
fi
|
||||
if ! is_valid_vendor "$v"; then
|
||||
echo "::error::vendor '$v' has no resources/profiles/$v.json with a profile directory or version field"
|
||||
exit 1
|
||||
fi
|
||||
echo "vendors=$v" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
base='${{ github.event.before }}'
|
||||
head='${{ github.sha }}'
|
||||
# Zero SHA (branch created / force push) or manual dispatch: fall back
|
||||
# to this commit's own diff.
|
||||
if [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ] || ! git cat-file -e "$base^{commit}" 2>/dev/null; then
|
||||
base="$head^"
|
||||
fi
|
||||
# Exposed so the OFL-pending step below can reuse this exact diff
|
||||
# range instead of re-deriving it (and drifting from this logic).
|
||||
echo "base=$base" >> "$GITHUB_OUTPUT"
|
||||
echo "head=$head" >> "$GITHUB_OUTPUT"
|
||||
mapfile -t candidates < <(
|
||||
git diff --name-only "$base" "$head" -- resources/profiles \
|
||||
| sed -nE 's#^resources/profiles/([^/]+)/.*#\1#p; s#^resources/profiles/([^/]+)\.json$#\1#p' \
|
||||
| sort -u
|
||||
)
|
||||
|
||||
vendors=()
|
||||
for v in "${candidates[@]:-}"; do
|
||||
[ -n "$v" ] || continue
|
||||
if is_valid_vendor "$v"; then
|
||||
vendors+=("$v")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${#vendors[@]}" -eq 0 ]; then
|
||||
echo "vendors=" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# A vendor is eligible only when both the profile directory and its
|
||||
# sibling bundle JSON are covered by at least one FOLDER_MERGERS
|
||||
# grant. The account part is intentionally ignored here: this is a
|
||||
# post-merge safety check, not an authorization check for a command.
|
||||
# An ineligible vendor (e.g. OrcaFilamentLibrary, which has no grant)
|
||||
# is dropped on its own - it never blocks other vendors in the same
|
||||
# push from publishing.
|
||||
grants=()
|
||||
while IFS= read -r raw_line; do
|
||||
line="${raw_line#"${raw_line%%[![:space:]]*}"}"
|
||||
line="${line%"${line##*[![:space:]]}"}"
|
||||
[ -n "$line" ] || continue
|
||||
[[ "$line" == \#* ]] && continue
|
||||
[[ "$line" == *:* ]] || continue
|
||||
|
||||
grant="${line#*:}"
|
||||
grant="${grant#"${grant%%[![:space:]]*}"}"
|
||||
grant="${grant%"${grant##*[![:space:]]}"}"
|
||||
while [[ "$grant" == */ ]]; do grant="${grant%/}"; done
|
||||
grants+=("$grant")
|
||||
done <<< "${FOLDER_MERGERS:-}"
|
||||
|
||||
is_granted() {
|
||||
local path="$1"
|
||||
local grant
|
||||
for grant in "${grants[@]:-}"; do
|
||||
if [[ "$path" == "$grant" || "$path" == "$grant/"* ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
authorized=()
|
||||
unauthorized=()
|
||||
for v in "${vendors[@]}"; do
|
||||
if is_granted "resources/profiles/$v" && is_granted "resources/profiles/$v.json"; then
|
||||
authorized+=("$v")
|
||||
else
|
||||
unauthorized+=("$v")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${#unauthorized[@]}" -ne 0 ]; then
|
||||
echo "::warning::skipping vendor(s) with no FOLDER_MERGERS grant (no asset built or published for them this run): ${unauthorized[*]}"
|
||||
fi
|
||||
|
||||
echo "vendors=${authorized[*]}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Resolve Orca version
|
||||
id: orca
|
||||
# Unconditional: needed both by the vendor-publish pipeline below (only
|
||||
# when vendors is non-empty) and by the OFL-pending step at the end
|
||||
# (which runs whenever OFL itself changed, even if vendors ends up
|
||||
# empty because OFL has no FOLDER_MERGERS grant). Cheap and harmless
|
||||
# to always resolve - version.inc is present on every commit.
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
raw="$(sed -nE 's/^set\(SoftFever_VERSION "([^"]+)".*/\1/p' version.inc | head -1)"
|
||||
[ -n "$raw" ] || { echo "::error::could not read SoftFever_VERSION from version.inc"; exit 1; }
|
||||
orca_ver="${raw%%-*}"
|
||||
if ! [[ "$orca_ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "::error::Orca version '$orca_ver' (from '$raw') is not X.Y.Z"; exit 1
|
||||
fi
|
||||
# release_tag is what the desktop client sends as orca_version and what
|
||||
# OrcaCloud keys R2 on; orca_ver (X.Y.Z) is the asset-name prefix.
|
||||
echo "release_tag=$raw" >> "$GITHUB_OUTPUT"
|
||||
echo "orca_ver=$orca_ver" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate profile versions
|
||||
id: pver
|
||||
if: steps.vendors.outputs.vendors != ''
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
: > "$RUNNER_TEMP/pver.tsv"
|
||||
for v in ${{ steps.vendors.outputs.vendors }}; do
|
||||
pv="$(jq -r '.version // empty' "resources/profiles/$v.json")"
|
||||
if ! [[ "$pv" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "::error::vendor $v version '${pv:-<none>}' must be 4 numeric parts (A.B.C.D) for the OTA asset name; fix resources/profiles/$v.json"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\t%s\n' "$v" "$pv" >> "$RUNNER_TEMP/pver.tsv"
|
||||
done
|
||||
|
||||
- name: Download generate_system_cache
|
||||
if: steps.vendors.outputs.vendors != ''
|
||||
shell: bash
|
||||
env:
|
||||
# gh (with the default token) rather than an unauthenticated curl: keeps
|
||||
# working if TOOL_REPO is ever private and avoids anonymous rate limits.
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release download nightly-builds --repo "$TOOL_REPO" \
|
||||
--pattern "$TOOL_ASSET" --output generate_system_cache --clobber
|
||||
chmod +x generate_system_cache
|
||||
|
||||
- name: Build caches and package assets
|
||||
id: pkg
|
||||
if: steps.vendors.outputs.vendors != ''
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# One timestamp for the whole run so a multi-vendor merge groups together.
|
||||
ts="$(date -u +%Y%m%d%H%M)"
|
||||
orca_ver='${{ steps.orca.outputs.orca_ver }}'
|
||||
out="$RUNNER_TEMP/assets"
|
||||
mkdir -p "$out"
|
||||
|
||||
for v in ${{ steps.vendors.outputs.vendors }}; do
|
||||
./generate_system_cache -p "$GITHUB_WORKSPACE/resources/profiles" -v "$v" -l 2
|
||||
opc="resources/profiles/$v.opc"
|
||||
[ -f "$opc" ] || { echo "::error::$opc was not generated"; exit 1; }
|
||||
pv="$(awk -F'\t' -v v="$v" '$1==v{print $2}' "$RUNNER_TEMP/pver.tsv")"
|
||||
name="${orca_ver}_${v}_${pv}_${ts}.zip"
|
||||
( cd resources/profiles && zip -q -j "$out/$name" "$v.opc" )
|
||||
done
|
||||
echo "dir=$out" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Mint profiles-repo token
|
||||
id: token
|
||||
if: steps.vendors.outputs.vendors != ''
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.PROFILES_APP_ID }}
|
||||
private-key: ${{ secrets.PROFILES_APP_PRIVATE_KEY }}
|
||||
owner: ${{ env.PROFILES_OWNER }}
|
||||
repositories: ${{ env.PROFILES_REPO }}
|
||||
|
||||
- name: Publish assets to profiles release
|
||||
if: steps.vendors.outputs.vendors != ''
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.token.outputs.token }}
|
||||
RELEASE_TAG: ${{ steps.orca.outputs.release_tag }}
|
||||
ASSET_DIR: ${{ steps.pkg.outputs.dir }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
repo="$PROFILES_OWNER/$PROFILES_REPO"
|
||||
if ! gh release view "$RELEASE_TAG" --repo "$repo" >/dev/null 2>&1; then
|
||||
echo "Creating release $RELEASE_TAG on $repo"
|
||||
gh release create "$RELEASE_TAG" --repo "$repo" \
|
||||
--title "$RELEASE_TAG" --notes "Profile cache assets for Orca $RELEASE_TAG." \
|
||||
--latest=false
|
||||
fi
|
||||
# Asset names are timestamp-unique; a clash means a bug, so don't --clobber.
|
||||
gh release upload "$RELEASE_TAG" --repo "$repo" "$ASSET_DIR"/*.zip
|
||||
|
||||
{
|
||||
echo "### Published to \`$repo\` release \`$RELEASE_TAG\`"
|
||||
for f in "$ASSET_DIR"/*.zip; do echo "- \`$(basename "$f")\`"; done
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Notify OTA auto-publish
|
||||
# Gated on auto_publish specifically, not just "vendor was dispatched":
|
||||
# a maintainer manually dispatching with vendor=OrcaFilamentLibrary (e.g.
|
||||
# to rebuild/republish an asset while debugging) must not silently go
|
||||
# live. Only a caller that explicitly opts in with auto_publish=true
|
||||
# (the OFL nightly cron) skips the human changelog/Publish step.
|
||||
if: >-
|
||||
steps.vendors.outputs.vendors != '' && github.event_name == 'workflow_dispatch'
|
||||
&& (inputs.auto_publish == true || inputs.auto_publish == 'true')
|
||||
shell: bash
|
||||
env:
|
||||
OTA_API_BASE_URL: ${{ vars.OTA_API_BASE_URL }}
|
||||
OTA_API_KEY: ${{ secrets.OFL_OTA_PUBLISH_KEY }}
|
||||
ASSET_DIR: ${{ steps.pkg.outputs.dir }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[ -n "$OTA_API_BASE_URL" ] || { echo "::error::vars.OTA_API_BASE_URL is not set"; exit 1; }
|
||||
[ -n "$OTA_API_KEY" ] || { echo "::error::secrets.OFL_OTA_PUBLISH_KEY is not set"; exit 1; }
|
||||
|
||||
mapfile -t zip_files < <(cd "$ASSET_DIR" && ls -1 *.zip)
|
||||
filenames_json="$(printf '%s\n' "${zip_files[@]}" | jq -R . | jq -s .)"
|
||||
payload="$(jq -n --argjson filenames "$filenames_json" '{filenames: $filenames}')"
|
||||
|
||||
resp_file="$RUNNER_TEMP/ota-auto-publish-response.json"
|
||||
status="$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST \
|
||||
"${OTA_API_BASE_URL%/}/api/v1/ota/auto-publish" \
|
||||
-H "Authorization: Bearer $OTA_API_KEY" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "$payload")"
|
||||
body="$(cat "$resp_file")"
|
||||
echo "$body"
|
||||
|
||||
if [ "$status" != "200" ]; then
|
||||
echo "::error::OTA auto-publish call failed with HTTP $status"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# A 200 can still carry per-file "error" results (e.g. NOT_FOUND); the
|
||||
# asset is already safely published to the profiles release above, but
|
||||
# it never went live, so treat that as a failure worth surfacing loudly.
|
||||
error_count="$(jq '[.results[] | select(.status == "error")] | length' <<< "$body")"
|
||||
if [ "$error_count" != "0" ]; then
|
||||
jq -r '.results[] | select(.status == "error") | "::error::\(.filename): \(.code) - \(.message)"' <<< "$body"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Record OFL pending changes
|
||||
# A real merge, never the cron's explicit-vendor dispatch (that's
|
||||
# automation publishing, not a new merge to report). This covers two
|
||||
# trigger shapes: an ordinary push, and a vendor-less workflow_dispatch
|
||||
# - the latter is exactly what pr-merge-bot.yml's re-dispatch after a
|
||||
# successful /bot merge looks like (a GITHUB_TOKEN-authored merge fires
|
||||
# no push event at all, which is why that re-dispatch exists). Both
|
||||
# land in the same diff-fallback path in "Resolve changed vendors", so
|
||||
# base/head/orca_ver are already correctly populated either way - only
|
||||
# this condition needs widening.
|
||||
# Placed last in the job on purpose: a failure here must never block
|
||||
# the vendor-publish pipeline above, which a step failing earlier in
|
||||
# the job would do (subsequent steps without always() get skipped).
|
||||
if: >-
|
||||
github.event_name == 'push' ||
|
||||
(github.event_name == 'workflow_dispatch' && !inputs.vendor)
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OTA_API_BASE_URL: ${{ vars.OTA_API_BASE_URL }}
|
||||
OTA_API_KEY: ${{ secrets.OFL_OTA_PUBLISH_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base='${{ steps.vendors.outputs.base }}'
|
||||
head='${{ steps.vendors.outputs.head }}'
|
||||
orca_ver='${{ steps.orca.outputs.orca_ver }}'
|
||||
|
||||
# Only real vendor subdirectories under filament/, e.g.
|
||||
# .../filament/Qidi/x.json -> "Qidi". This naturally excludes loose
|
||||
# top-level files (.../filament/Generic PLA @System.json - no further
|
||||
# slash to match) and is further filtered below to drop "base", the
|
||||
# shared @base/@System inheritance folder, not a partner company.
|
||||
mapfile -t ofl_companies < <(
|
||||
git diff --name-only "$base" "$head" -- resources/profiles/OrcaFilamentLibrary/filament \
|
||||
| sed -nE 's#^resources/profiles/OrcaFilamentLibrary/filament/([^/]+)/.*#\1#p' \
|
||||
| grep -vx 'base' \
|
||||
| sort -u
|
||||
)
|
||||
|
||||
if [ "${#ofl_companies[@]}" -eq 0 ]; then
|
||||
echo "No OFL company folders changed in this push."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[ -n "$OTA_API_BASE_URL" ] || { echo "::error::vars.OTA_API_BASE_URL is not set"; exit 1; }
|
||||
[ -n "$OTA_API_KEY" ] || { echo "::error::secrets.OFL_OTA_PUBLISH_KEY is not set"; exit 1; }
|
||||
|
||||
# The head commit's own merged PR, not a per-commit walk: this
|
||||
# assumes the ordinary one-PR-per-push shape every other merge path
|
||||
# in this repo already assumes (pr-merge-bot.yml's re-dispatch logic
|
||||
# does the same). A merge commit's parents don't matter here - this
|
||||
# API call works the same regardless of merge strategy.
|
||||
pr_json="$(gh api "repos/${{ github.repository }}/commits/$head/pulls" \
|
||||
--jq '[.[] | select(.merged_at != null)] | sort_by(.merged_at) | last // empty')"
|
||||
|
||||
if [ -z "$pr_json" ]; then
|
||||
echo "::warning::push $head touches OFL compan(y/ies) (${ofl_companies[*]}) but has no associated merged PR; skipping pending record(s)"
|
||||
exit 0
|
||||
fi
|
||||
pr_number="$(jq -r '.number' <<< "$pr_json")"
|
||||
pr_url="$(jq -r '.html_url' <<< "$pr_json")"
|
||||
pr_title="$(jq -r '.title' <<< "$pr_json")"
|
||||
|
||||
for company in "${ofl_companies[@]}"; do
|
||||
payload="$(jq -n --arg vendor "$company" --arg ver "$orca_ver" --argjson pr "$pr_number" \
|
||||
--arg url "$pr_url" --arg title "$pr_title" \
|
||||
'{vendor: $vendor, orcaSlicerVersion: $ver, prNumber: $pr, prUrl: $url, prTitle: $title}')"
|
||||
|
||||
resp_file="$RUNNER_TEMP/ofl-pending-$company.json"
|
||||
status="$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST \
|
||||
"${OTA_API_BASE_URL%/}/api/v1/ota/ofl/pending" \
|
||||
-H "Authorization: Bearer $OTA_API_KEY" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "$payload")"
|
||||
body="$(cat "$resp_file")"
|
||||
echo "$body"
|
||||
|
||||
if [ "$status" != "200" ]; then
|
||||
echo "::error::OFL pending record failed for vendor=$company (PR #$pr_number): HTTP $status"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
@@ -12,13 +12,6 @@ name: PR Merge Bot
|
||||
# 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
|
||||
@@ -39,18 +32,10 @@ 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 }}
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
@@ -58,7 +43,6 @@ jobs:
|
||||
# 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:
|
||||
@@ -69,7 +53,7 @@ jobs:
|
||||
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.
|
||||
# delegated merge would wait for a human reviewer.
|
||||
environment: merge-delegation
|
||||
steps:
|
||||
- name: Merge PR on behalf of a folder delegate
|
||||
@@ -92,6 +76,7 @@ jobs:
|
||||
const ALLOWED_BASE_BRANCH = /^(?:main|release\/.+)$/;
|
||||
const MERGE_METHOD = 'squash';
|
||||
const REQUIRED_CHECK = 'Check profiles'; // job name in check_profiles.yml
|
||||
const MAX_CHANGED_FILES = 500; // policy cap, well under listFiles' 3000
|
||||
const LISTFILES_CAP = 3000;
|
||||
const MAX_REPORTED_FILES = 12;
|
||||
const MERGEABLE_ATTEMPTS = 5;
|
||||
@@ -319,6 +304,9 @@ jobs:
|
||||
'so the file list is truncated and I cannot verify the folder scope. A maintainer must merge this one.'
|
||||
);
|
||||
}
|
||||
if (pr.changed_files > MAX_CHANGED_FILES) {
|
||||
return refuse(`it changes ${pr.changed_files} files; delegated merges are capped at ${MAX_CHANGED_FILES}.`);
|
||||
}
|
||||
|
||||
const deniedFiles = [];
|
||||
const outsideFiles = [];
|
||||
@@ -520,414 +508,3 @@ jobs:
|
||||
} catch (error) {
|
||||
core.warning(`Merged successfully, but dispatching build_all.yml failed: ${error.message}`);
|
||||
}
|
||||
// ---- re-kick the profile publish ----
|
||||
// Same reason as above: post_merge_profiles.yml is push-triggered, so a
|
||||
// GITHUB_TOKEN merge never starts it. workflow_dispatch skips the paths:
|
||||
// filter, so only dispatch when the PR actually touched profiles.
|
||||
const touchesProfiles = files.some((file) =>
|
||||
[file.filename, file.previous_filename]
|
||||
.filter(Boolean)
|
||||
.some((p) => p.startsWith('resources/profiles/'))
|
||||
);
|
||||
if (touchesProfiles) {
|
||||
try {
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner,
|
||||
repo,
|
||||
workflow_id: 'post_merge_profiles.yml',
|
||||
ref: pr.base.ref
|
||||
});
|
||||
core.info(`Dispatched post_merge_profiles.yml on ${pr.base.ref}.`);
|
||||
} catch (error) {
|
||||
core.warning(`Merged successfully, but dispatching post_merge_profiles.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: write
|
||||
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: write
|
||||
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.');
|
||||
|
||||
@@ -44,14 +44,6 @@ jobs:
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: ${{ inputs.artifact }}
|
||||
# run_unit_tests.sh installs the plugin tests' numpy with the uv the build stages
|
||||
# beside them; the Windows arm64 build bundles none, so put one on PATH there.
|
||||
- name: Install uv
|
||||
if: runner.os == 'Windows' && runner.arch == 'ARM64'
|
||||
uses: astral-sh/setup-uv@v10.2.0
|
||||
with:
|
||||
version: "0.11.21" # ORCA_UV_VERSION in CMakeLists.txt
|
||||
enable-cache: false
|
||||
- uses: lukka/get-cmake@latest
|
||||
with:
|
||||
cmakeVersion: "~4.3.0" # use most recent 4.3.x version
|
||||
|
||||
@@ -54,4 +54,3 @@ __pycache__/
|
||||
*.opc
|
||||
/.test/
|
||||
docs/superpowers/
|
||||
ctest_results.xml
|
||||
|
||||
@@ -56,9 +56,9 @@ You can do this in Environment Variables settings.
|
||||
endif ()
|
||||
|
||||
if (APPLE)
|
||||
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 12.0 (the lowest Xcode 27 accepts)
|
||||
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 11.3
|
||||
if (NOT CMAKE_OSX_DEPLOYMENT_TARGET)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING "Minimum OS X deployment version" FORCE)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.3" CACHE STRING "Minimum OS X deployment version" FORCE)
|
||||
endif ()
|
||||
message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
|
||||
endif ()
|
||||
@@ -70,9 +70,6 @@ if (POLICY CMP0092)
|
||||
cmake_policy(SET CMP0092 NEW)
|
||||
endif ()
|
||||
|
||||
# project() reads this, so set it first.
|
||||
set(CMAKE_USER_MAKE_RULES_OVERRIDE "${CMAKE_CURRENT_LIST_DIR}/cmake/modules/ClangClShowIncludes.cmake")
|
||||
|
||||
project(OrcaSlicer)
|
||||
|
||||
# Backward compatibility for old CMake versions
|
||||
@@ -110,7 +107,6 @@ 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)
|
||||
@@ -279,11 +275,6 @@ if (APPLE)
|
||||
endif()
|
||||
SET(CMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.orcaslicer.OrcaSlicer")
|
||||
|
||||
# The macOS CI jobs build with Ninja (build_release_macos.sh -x), so the Xcode generator
|
||||
# is not covered. Xcode adds -Wshorten-64-to-32 by default ("Implicit Conversion to 32 Bit
|
||||
# Type"); Ninja/-Wall does not, and under -Werror it fails Xcode builds on code CI accepts.
|
||||
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_64_TO_32_BIT_CONVERSION "NO")
|
||||
|
||||
message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}")
|
||||
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
set(CMAKE_INSTALL_RPATH "$ORIGIN")
|
||||
@@ -317,10 +308,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 ()
|
||||
@@ -1089,30 +1076,32 @@ 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
|
||||
@@ -1120,19 +1109,45 @@ function(orcaslicer_copy_dlls target config postfix output_dlls)
|
||||
${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()
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ SCRIPT_PATH=$(dirname "$(readlink -f "${0}")")
|
||||
pushd "${SCRIPT_PATH}" > /dev/null
|
||||
|
||||
function usage() {
|
||||
echo "Usage: ./${SCRIPT_NAME} [-1][-b][-c][-d][-D][-e][-F][-g][-h][-i][-j N][-p][-r][-s][-t][-u][-l][-L]"
|
||||
echo "Usage: ./${SCRIPT_NAME} [-1][-b][-c][-d][-D][-e][-F][-g][-h][-i][-j N][-p][-r][-s][-t][-u][-l][-L [lld|mold]]"
|
||||
echo " -1: limit builds to one core (where possible)"
|
||||
echo " -j N: limit builds to N cores (where possible)"
|
||||
echo " -b: build in Debug mode"
|
||||
@@ -27,7 +27,7 @@ function usage() {
|
||||
echo " -t: build tests (optional), requires -s flag"
|
||||
echo " -u: install system dependencies (asks for sudo password; build prerequisite)"
|
||||
echo " -l: use Clang instead of GCC (default: GCC)"
|
||||
echo " -L: use ld.lld as linker (if available)"
|
||||
echo " -L [lld|mold]: use an alternate linker (if available) (default: lld)"
|
||||
echo "For a first use, you want to './${SCRIPT_NAME} -u'"
|
||||
echo " and then './${SCRIPT_NAME} -dsi'"
|
||||
echo "For a GitHub Actions-like Linux build locally, use './${SCRIPT_NAME} -g -istrlL'"
|
||||
@@ -115,8 +115,24 @@ while getopts ":1j:bcCdDeFghiprstulL" opt ; do
|
||||
FORWARDED_ARGS+=("-l")
|
||||
;;
|
||||
L )
|
||||
USE_LLD="1"
|
||||
FORWARDED_ARGS+=("-L")
|
||||
# -L takes an optional argument. getopts has no native support for
|
||||
# this, so L is declared bare (no ':') in the optstring above, and we
|
||||
# manually peek at the next unconsumed argv token via ${!OPTIND}. If
|
||||
# it's a bare 'lld' or 'mold' (not another option, i.e. doesn't start
|
||||
# with '-'), consume it as the explicit choice and advance OPTIND so
|
||||
# getopts doesn't reprocess it as a new flag. Otherwise, leave it
|
||||
# alone (it isn't meant for -L) and default to lld.
|
||||
LINKER_NAME="lld"
|
||||
next_arg="${!OPTIND-}"
|
||||
if [[ -n "${next_arg}" ]] && [[ "${next_arg}" != -* ]] ; then
|
||||
case "${next_arg}" in
|
||||
lld|mold )
|
||||
LINKER_NAME="${next_arg}"
|
||||
OPTIND=$((OPTIND + 1))
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
FORWARDED_ARGS+=("-L" "${LINKER_NAME}")
|
||||
;;
|
||||
* )
|
||||
echo "Unknown argument '${opt}', aborting."
|
||||
@@ -130,11 +146,23 @@ if [ ${OPTIND} -eq 1 ] ; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
shift $((OPTIND - 1))
|
||||
if [ $# -ne 0 ] ; then
|
||||
echo "Unknown argument '$1', aborting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${CLEAN_DOCKER_IMAGE}" ]] && [[ -z "${USE_DOCKER}" ]] ; then
|
||||
echo "Error: -F requires -g."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${USE_DOCKER}" ]] && [[ "${LINKER_NAME}" == "mold" ]] ; then
|
||||
echo "Error: -L mold is not available in the Docker/Podman build image, so -g and -L mold cannot be combined."
|
||||
echo "Omit -L mold when using -g (the container build defaults to GCC without mold), or drop -g and build with -L mold directly on a host with mold installed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function check_available_memory_and_disk() {
|
||||
FREE_MEM_GB=$(free --gibi --total | grep 'Mem' | rev | cut --delimiter=" " --fields=1 | rev)
|
||||
MIN_MEM_GB=10
|
||||
@@ -492,14 +520,49 @@ if [[ -n "${USE_CLANG}" ]] ; then
|
||||
export CMAKE_C_CXX_COMPILER_CLANG=(-DCMAKE_C_COMPILER=/usr/bin/clang -DCMAKE_CXX_COMPILER=/usr/bin/clang++)
|
||||
fi
|
||||
|
||||
# Configure use of ld.lld as the linker when requested
|
||||
export CMAKE_LLD_LINKER_ARGS=()
|
||||
if [[ -n "${USE_LLD}" ]] ; then
|
||||
if command -v ld.lld >/dev/null 2>&1 ; then
|
||||
LLD_BIN=$(command -v ld.lld)
|
||||
export CMAKE_LLD_LINKER_ARGS=(-DCMAKE_LINKER="${LLD_BIN}" -DCMAKE_EXE_LINKER_FLAGS=-fuse-ld=lld -DCMAKE_SHARED_LINKER_FLAGS=-fuse-ld=lld -DCMAKE_MODULE_LINKER_FLAGS=-fuse-ld=lld)
|
||||
# Configure use of an alternate linker (-L lld or -L mold) when requested
|
||||
export CMAKE_LINKER_ARGS=()
|
||||
if [[ -n "${LINKER_NAME}" ]] ; then
|
||||
case "${LINKER_NAME}" in
|
||||
lld )
|
||||
LINKER_BIN_NAME="ld.lld"
|
||||
;;
|
||||
mold )
|
||||
LINKER_BIN_NAME="mold"
|
||||
# -fuse-ld=mold requires GCC 12.1+. Older GCC (e.g. GCC 11, shipped
|
||||
# for Ubuntu 22.x via scripts/linux.d/debian) doesn't understand the
|
||||
# flag, and cmake's compiler check then fails with a confusing
|
||||
# generic "is not able to compile a simple test program" error
|
||||
# instead of naming the real cause. Catch it here instead.
|
||||
if [[ -z "${USE_CLANG}" ]] ; then
|
||||
GCC_BIN="${CC:-gcc}"
|
||||
if ! command -v "${GCC_BIN}" >/dev/null 2>&1 ; then
|
||||
GCC_BIN="cc"
|
||||
fi
|
||||
if command -v "${GCC_BIN}" >/dev/null 2>&1 ; then
|
||||
GCC_VERSION=$("${GCC_BIN}" -dumpfullversion 2>/dev/null)
|
||||
if [[ -n "${GCC_VERSION}" ]] && [[ "$(printf '%s\n%s\n' "${GCC_VERSION}" "12.1" | sort -V | head -n1)" != "12.1" ]] ; then
|
||||
echo "Error: -L mold requires GCC 12.1 or newer to support -fuse-ld=mold (found GCC ${GCC_VERSION} via '${GCC_BIN}')."
|
||||
echo "Use -l to build with Clang instead, upgrade your GCC toolchain, or omit -L mold."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if command -v "${LINKER_BIN_NAME}" >/dev/null 2>&1 ; then
|
||||
LINKER_BIN=$(command -v "${LINKER_BIN_NAME}")
|
||||
export CMAKE_LINKER_ARGS=(-DCMAKE_LINKER="${LINKER_BIN}" "-DCMAKE_EXE_LINKER_FLAGS=-fuse-ld=${LINKER_NAME}" "-DCMAKE_SHARED_LINKER_FLAGS=-fuse-ld=${LINKER_NAME}" "-DCMAKE_MODULE_LINKER_FLAGS=-fuse-ld=${LINKER_NAME}")
|
||||
else
|
||||
echo "Error: ld.lld not found. Please install the 'lld' package (e.g., sudo apt install lld) or omit -L."
|
||||
case "${LINKER_NAME}" in
|
||||
lld )
|
||||
echo "Error: ld.lld not found. Please install the 'lld' package (e.g., sudo apt install lld) or omit -L lld."
|
||||
;;
|
||||
mold )
|
||||
echo "Error: mold not found. Please install the 'mold' package or omit -L mold."
|
||||
;;
|
||||
esac
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -536,7 +599,7 @@ if [[ -n "${BUILD_DEPS}" ]] ; then
|
||||
BUILD_ARGS+=(-DCMAKE_BUILD_TYPE="${BUILD_CONFIG}")
|
||||
fi
|
||||
|
||||
print_and_run cmake -S deps -B deps/$BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G Ninja "${COLORED_OUTPUT}" "${BUILD_ARGS[@]}"
|
||||
print_and_run cmake -S deps -B deps/$BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G Ninja "${COLORED_OUTPUT}" "${BUILD_ARGS[@]}"
|
||||
print_and_run cmake --build deps/$BUILD_DIR -j1
|
||||
fi
|
||||
|
||||
@@ -556,7 +619,7 @@ if [[ -n "${BUILD_ORCA}" ]] || [[ -n "${BUILD_TESTS}" ]] ; then
|
||||
BUILD_ARGS+=(-DORCA_UPDATER_SIG_KEY="${ORCA_UPDATER_SIG_KEY}")
|
||||
fi
|
||||
|
||||
print_and_run cmake -S . -B $BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G "Ninja Multi-Config" \
|
||||
print_and_run cmake -S . -B $BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G "Ninja Multi-Config" \
|
||||
-DSLIC3R_PCH=${SLIC3R_PRECOMPILED_HEADERS} \
|
||||
-DORCA_TOOLS=ON \
|
||||
"${COLORED_OUTPUT}" \
|
||||
|
||||
@@ -53,7 +53,7 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
|
||||
echo " -s: Build slicer only"
|
||||
echo " -u: Build universal app only (requires existing arm64 and x86_64 app bundles)"
|
||||
echo " -n: Nightly build"
|
||||
echo " -t: Specify minimum version of the target platform, default is 12.0"
|
||||
echo " -t: Specify minimum version of the target platform, default is 11.3"
|
||||
echo " -x: Use Ninja Multi-Config CMake generator, default is Xcode"
|
||||
echo " -b: Build without reconfiguring CMake"
|
||||
echo " -c: Set CMake build configuration, default is Release"
|
||||
@@ -95,7 +95,7 @@ if [ -z "$DEPS_CMAKE_GENERATOR" ]; then
|
||||
fi
|
||||
|
||||
if [ -z "$OSX_DEPLOYMENT_TARGET" ]; then
|
||||
export OSX_DEPLOYMENT_TARGET="12.0"
|
||||
export OSX_DEPLOYMENT_TARGET="11.3"
|
||||
fi
|
||||
|
||||
if [ -z "$CMAKE_IGNORE_PREFIX_PATH" ]; then
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# ccache does not parse the -clang: arguments CMake uses for clang-cl's gcc-style
|
||||
# depfile, so a cache hit writes the object and no depfile, and Ninja then records
|
||||
# no headers for that object. ccache reproduces /showIncludes output on a hit.
|
||||
foreach (_lang C CXX)
|
||||
if (CMAKE_${_lang}_COMPILER_ID STREQUAL "Clang" AND
|
||||
CMAKE_${_lang}_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
|
||||
set(CMAKE_DEPFILE_FLAGS_${_lang} "/showIncludes")
|
||||
set(CMAKE_${_lang}_DEPFILE_FORMAT msvc)
|
||||
endif ()
|
||||
endforeach ()
|
||||
@@ -26,9 +26,9 @@ endif()
|
||||
|
||||
cmake_minimum_required(VERSION 3.2)
|
||||
if (APPLE)
|
||||
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 12.0 (the lowest Xcode 27 accepts)
|
||||
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 11.3
|
||||
if (NOT CMAKE_OSX_DEPLOYMENT_TARGET)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING "Minimum OS X deployment version" FORCE)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.3" CACHE STRING "Minimum OS X deployment version" FORCE)
|
||||
endif ()
|
||||
message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
|
||||
|
||||
@@ -38,14 +38,6 @@ if(POLICY CMP0135) # DOWNLOAD_EXTRACT_TIMESTAMP
|
||||
cmake_policy(SET CMP0135 NEW)
|
||||
endif()
|
||||
|
||||
# project() reads this, so set it first. scripts/flatpak/make_deps_tar.sh packs deps/
|
||||
# without cmake/, so the file is missing in a Flatpak build.
|
||||
set(_rules_override "${CMAKE_CURRENT_LIST_DIR}/../cmake/modules/ClangClShowIncludes.cmake")
|
||||
if (EXISTS "${_rules_override}")
|
||||
set(CMAKE_USER_MAKE_RULES_OVERRIDE "${_rules_override}")
|
||||
endif ()
|
||||
unset(_rules_override)
|
||||
|
||||
project(OrcaSlicer-deps)
|
||||
|
||||
# Backward compatibility for old CMake versions
|
||||
@@ -63,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")
|
||||
@@ -228,7 +219,6 @@ if (NOT IS_CROSS_COMPILE OR NOT APPLE)
|
||||
-DCMAKE_CXX_COMPILER:STRING=${CMAKE_CXX_COMPILER}
|
||||
-DCMAKE_C_COMPILER_LAUNCHER:STRING=${CMAKE_C_COMPILER_LAUNCHER}
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER:STRING=${CMAKE_CXX_COMPILER_LAUNCHER}
|
||||
-DCMAKE_USER_MAKE_RULES_OVERRIDE:STRING=${CMAKE_USER_MAKE_RULES_OVERRIDE}
|
||||
-DCMAKE_TOOLCHAIN_FILE:STRING=${CMAKE_TOOLCHAIN_FILE}
|
||||
-DCMAKE_EXE_LINKER_FLAGS:STRING=${CMAKE_EXE_LINKER_FLAGS}
|
||||
-DCMAKE_SHARED_LINKER_FLAGS:STRING=${CMAKE_SHARED_LINKER_FLAGS}
|
||||
@@ -276,7 +266,6 @@ else()
|
||||
-DCMAKE_IGNORE_PREFIX_PATH:STRING=${CMAKE_IGNORE_PREFIX_PATH}
|
||||
-DCMAKE_C_COMPILER_LAUNCHER:STRING=${CMAKE_C_COMPILER_LAUNCHER}
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER:STRING=${CMAKE_CXX_COMPILER_LAUNCHER}
|
||||
-DCMAKE_USER_MAKE_RULES_OVERRIDE:STRING=${CMAKE_USER_MAKE_RULES_OVERRIDE}
|
||||
-DBUILD_SHARED_LIBS:BOOL=OFF
|
||||
${_cmake_osx_arch}
|
||||
"${_configs_line}"
|
||||
@@ -374,11 +363,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)
|
||||
|
||||
@@ -468,7 +452,6 @@ set(_dep_list
|
||||
dep_NLopt
|
||||
dep_OpenVDB
|
||||
dep_OpenCSG
|
||||
${SLVS_PKG}
|
||||
dep_OpenCV
|
||||
dep_Eigen
|
||||
dep_CGAL
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#if defined (__GNUC__) && ! defined (__cplusplus)
|
||||
typedef unsigned long long t1;typedef t1*t2;
|
||||
-void g(){}
|
||||
+void g(int a,t1 const*b,t1 c,t2 dd,t1 const*e,int ff){}
|
||||
+void g(int,t1 const*,t1,t2,t1 const*,int){}
|
||||
void h(){}
|
||||
static __inline__ t1 e(t2 rp,t2 up,int n,t1 v0)
|
||||
{t1 c,x,r;int i;if(v0){c=1;for(i=1;i<n;i++){x=up[i];r=x+1;rp[i]=r;}}return c;}
|
||||
@@ -17,7 +17,7 @@
|
||||
#if defined (__GNUC__) && ! defined (__cplusplus)
|
||||
typedef unsigned long long t1;typedef t1*t2;
|
||||
-void g(){}
|
||||
+void g(int a,t1 const*b,t1 c,t2 dd,t1 const*e,int ff){}
|
||||
+void g(int,t1 const*,t1,t2,t1 const*,int){}
|
||||
void h(){}
|
||||
static __inline__ t1 e(t2 rp,t2 up,int n,t1 v0)
|
||||
{t1 c,x,r;int i;if(v0){c=1;for(i=1;i<n;i++){x=up[i];r=x+1;rp[i]=r;}}return c;}
|
||||
@@ -26,7 +26,7 @@
|
||||
#if defined (__GNUC__) && ! defined (__cplusplus)
|
||||
typedef unsigned long long t1;typedef t1*t2;
|
||||
-void g(){}
|
||||
+void g(int a,t1 const*b,t1 c,t2 dd,t1 const*e,int ff){}
|
||||
+void g(int,t1 const*,t1,t2,t1 const*,int){}
|
||||
void h(){}
|
||||
static __inline__ t1 e(t2 rp,t2 up,int n,t1 v0)
|
||||
{t1 c,x,r;int i;if(v0){c=1;for(i=1;i<n;i++){x=up[i];r=x+1;rp[i]=r;}}return c;}
|
||||
|
||||
@@ -11,21 +11,6 @@ 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,7 +35,7 @@ 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}
|
||||
|
||||
@@ -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 ()
|
||||
@@ -15,7 +15,6 @@ orcaslicer_add_cmake_project(
|
||||
-DTBB_BUILD_SHARED=OFF
|
||||
-DTBB_BUILD_TESTS=OFF
|
||||
-DTBB_TEST=OFF
|
||||
-DTBB_DISABLE_HWLOC_AUTOMATIC_SEARCH=ON
|
||||
-DTBB_ENABLE_IPO=OFF
|
||||
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF
|
||||
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
|
||||
|
||||
@@ -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
|
||||
@@ -1,12 +0,0 @@
|
||||
// Female half alone, for the legibility test: is a recessed faceted pocket readable in a
|
||||
// shaded view, or does a concave feature just read as a dark hole with no orientation?
|
||||
use <faceted_ridge_key.scad>
|
||||
|
||||
// The plate must be THICKER than the key is tall, or the "pocket" is a through-hole. The
|
||||
// first version used 3 mm against a 4.5 mm key and cut straight through — caught only by
|
||||
// rendering it. Minimum stock = H + clearance + pocket depth + a wall to print against.
|
||||
PLATE = [30, 18, 8];
|
||||
difference() {
|
||||
translate([-PLATE[0]/2, -PLATE[1]/2, -PLATE[2]]) cube(PLATE);
|
||||
ridge_key_female();
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
# Measure the assembled fit between the supplied male and the generated female.
|
||||
# This is the number that matters: the minimum gap in the seated position.
|
||||
# Run: /snap/bin/freecad.cmd fit_check.py
|
||||
import os
|
||||
import Part
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
male = Part.Shape(); male.read(os.path.join(HERE, "bear.step"))
|
||||
fem = Part.Shape(); fem.read(os.path.join(HERE, "BearConnector_Female.step"))
|
||||
male, fem = male.Solids[0], fem.Solids[0]
|
||||
|
||||
d = male.distToShape(fem)
|
||||
print(f"RESULT minimum gap male<->female, seated: {d[0]:.4f} mm (design clearance 0.20)")
|
||||
|
||||
c = male.common(fem)
|
||||
print(f"RESULT interference volume: {(c.Volume if c.Solids else 0.0):.6f} mm3")
|
||||
|
||||
p = d[1][0][0]
|
||||
print(f"RESULT tightest point on the male: ({p.x:.2f}, {p.y:.2f}, {p.z:.2f})")
|
||||
print(f"RESULT male {male.Volume/1000:.2f} cm3 / female {fem.Volume/1000:.2f} cm3")
|
||||
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,99 +0,0 @@
|
||||
"""Render the SIMPLIFIED glyph exactly as render_mate_face() draws it — x0kd.
|
||||
|
||||
This is the panel the study was missing. simplify_study.py measured a FLAT outline and
|
||||
relief_sheet.py measured the FULL 1508-facet part; neither showed the simplified glyph WITH its
|
||||
relief, which is what the code actually draws and the only thing that answers "is the snout still
|
||||
protruding". Same facet list, same painter order, same camera-fixed lambert as the C++.
|
||||
"""
|
||||
import math, os
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
T = open(os.path.join(HERE, "bear_glyph_table.h")).read()
|
||||
def grab(name, n):
|
||||
body = T.split(name + "[] = {")[1].split("};")[0]
|
||||
body = "\n".join(l.split("//")[0] for l in body.splitlines())
|
||||
out = []
|
||||
for tok in body.replace("\n", " ").split("},"):
|
||||
tok = tok.strip().lstrip("{").strip()
|
||||
if not tok: continue
|
||||
v = [float(x) for x in tok.replace("{", "").split(",")[:n]]
|
||||
if len(v) == n: out.append(tuple(v))
|
||||
return out
|
||||
OUT = grab("kBearOutline", 2)
|
||||
CHIN = grab("kBearChin", 2) # NB: this table entry is the CHIN BAR, not the snout
|
||||
MARKS = grab("kBearMarks", 3)
|
||||
CREST = grab("kBearCrest", 3)
|
||||
SBASE = grab("kBearSnoutBase", 2)
|
||||
PLATE = float(T.split("kBearPlateZ = ")[1].split(";")[0])
|
||||
|
||||
|
||||
def facets():
|
||||
F = []
|
||||
n = len(OUT)
|
||||
for i in range(n): # plate sides -> the grazing silhouette
|
||||
a, b = OUT[i], OUT[(i+1) % n]
|
||||
F.append(([(a[0],a[1],0.0),(b[0],b[1],0.0),(b[0],b[1],PLATE),(a[0],a[1],PLATE)], "body", True))
|
||||
F.append(([(x,y,PLATE) for x,y in OUT], "body", True)) # plate top
|
||||
zm = PLATE + 0.004
|
||||
for cx,cy,r in MARKS: # eyes + cheek dot
|
||||
F.append(([(cx+r*math.cos(2*math.pi*i/12), cy+r*math.sin(2*math.pi*i/12), zm) for i in range(12)], "mark", False))
|
||||
F.append(([(x,y,zm) for x,y in CHIN], "mark", False)) # chin bar
|
||||
A, B = CREST # THE MUZZLE: base quad + crest
|
||||
nl=(SBASE[0][0],SBASE[0][1],PLATE); nr=(SBASE[1][0],SBASE[1][1],PLATE)
|
||||
tr=(SBASE[2][0],SBASE[2][1],PLATE); tl=(SBASE[3][0],SBASE[3][1],PLATE)
|
||||
F += [([nl,tl,B,A],"body",True), # left flank
|
||||
([nr,A,B,tr],"body",True), # right flank
|
||||
([nl,A,nr],"body",True), # nose cap, sloping because the base overhangs the crest
|
||||
([tr,B,tl],"body",True)] # tail cap
|
||||
return F
|
||||
FACETS = facets()
|
||||
|
||||
BODY=(0.42,0.46,0.52); MARK=(0.126,0.138,0.156)
|
||||
def render(px, elev_deg, ss=8):
|
||||
S=px*ss; a=math.radians(elev_deg); ca,sa=math.cos(a),math.sin(a)
|
||||
# camera orbits down; the connector's +Z (relief) tips toward the horizon
|
||||
xf=lambda p:(p[0], p[1]*sa + p[2]*ca, -p[1]*ca + p[2]*sa)
|
||||
light=(-0.70,0.30,0.45)
|
||||
img=Image.new("RGB",(S,S),(24,27,32)); d=ImageDraw.Draw(img)
|
||||
tris=[]
|
||||
for pts,kind,shade in FACETS:
|
||||
q=[xf(p) for p in pts]
|
||||
tris.append((sum(v[2] for v in q)/len(q), q, kind, shade))
|
||||
tris.sort(key=lambda t:t[0]) # far first
|
||||
for _,q,kind,shade in tris:
|
||||
(x0,y0,z0),(x1,y1,z1),(x2,y2,z2)=q[0],q[1],q[2]
|
||||
ux,uy,uz=x1-x0,y1-y0,z1-z0; vx,vy,vz=x2-x0,y2-y0,z2-z0
|
||||
nx,ny,nz=uy*vz-uz*vy, uz*vx-ux*vz, ux*vy-uy*vx
|
||||
nn=math.sqrt(nx*nx+ny*ny+nz*nz) or 1.0
|
||||
nx,ny,nz=nx/nn,ny/nn,nz/nn
|
||||
if nz<0: nx,ny,nz=-nx,-ny,-nz
|
||||
base=BODY if kind=="body" else MARK
|
||||
k=(0.42+0.58*max(0.0,nx*light[0]+ny*light[1]+nz*light[2])) if shade else 1.0
|
||||
col=tuple(min(255,int(255*c*k)) for c in base)
|
||||
d.polygon([(S/2+p[0]*S*0.92, S/2-p[1]*S*0.92) for p in q], fill=col)
|
||||
return img.resize((px,px), Image.LANCZOS)
|
||||
|
||||
SIZES=[22,32,48]; ELEVS=[(90,"flat on"),(47,"47"),(16,"16"),(6,"6")]
|
||||
pad,cell=8,58
|
||||
W=pad+len(SIZES)*len(ELEVS)*cell+pad; H=pad+cell+pad
|
||||
sheet=Image.new("RGB",(W,H),(24,27,32))
|
||||
for ci,(e,_) in enumerate(ELEVS):
|
||||
for si,px in enumerate(SIZES):
|
||||
g=render(px,e)
|
||||
sheet.paste(g, (pad+(ci*len(SIZES)+si)*cell+(cell-px)//2, pad+(cell-px)//2))
|
||||
sheet.resize((W*2,H*2), Image.NEAREST).save(os.path.join(HERE,"glyph-preview.png"))
|
||||
|
||||
# how much of the glyph is the snout: render with and without the tent and diff
|
||||
def render_no_tent(px, elev):
|
||||
global FACETS
|
||||
keep=FACETS; FACETS=FACETS[:-4]
|
||||
try: return render(px, elev)
|
||||
finally: FACETS=keep
|
||||
print(f"{'elev':>8} {'lit px@32':>10} {'snout px':>9} {'snout share':>12}")
|
||||
for e,_ in ELEVS:
|
||||
a=render(32,e); b=render_no_tent(32,e)
|
||||
la=sum(1 for p in a.get_flattened_data() if p!=(24,27,32))
|
||||
diff=sum(1 for p,q in zip(a.get_flattened_data(), b.get_flattened_data()) if p!=q)
|
||||
print(f"{e:>8} {la:>10} {diff:>9} {100.0*diff/max(1,la):>11.1f}%")
|
||||
print("WROTE glyph-preview.png")
|
||||
@@ -1,143 +0,0 @@
|
||||
# Mate-connector glyph probe — built as REAL solids on REAL mechanical geometry,
|
||||
# so the shape can be judged in a 3D viewport instead of in a browser mock.
|
||||
#
|
||||
# Four polarity treatments, side by side on one bracket:
|
||||
# A Onshape baseline ...... ring + roll quadrant + three short axis arms
|
||||
# B solid cone ............ ring + quadrant + one-sided Z arrow, filled head (driven)
|
||||
# C hollow collar ......... ring + quadrant + one-sided Z arrow, shell head (fixed)
|
||||
# D pin / cup ............. polarity by RELIEF: a raised pin vs a sunk cup
|
||||
#
|
||||
# D is the one that only a 3D test can settle: in a shaded viewport, solid-vs-hollow is a
|
||||
# weak cue that depends on angle and lighting, while convex-vs-concave is a strong one --
|
||||
# and male/female is the mechanical language for polarity anyway.
|
||||
#
|
||||
# Scale note: in the real viewport gizmos are screen-constant (~15-40 px via upp = 1/zoom).
|
||||
# At a zoom where a 60 mm part fills ~600 px, 40 px is about 4 mm, so R = 4.5 mm here.
|
||||
|
||||
import FreeCAD as App
|
||||
import FreeCADGui as Gui
|
||||
import Part
|
||||
from FreeCAD import Vector
|
||||
|
||||
DOC = "GlyphProbe"
|
||||
for d in list(App.listDocuments()):
|
||||
App.closeDocument(d)
|
||||
doc = App.newDocument(DOC)
|
||||
|
||||
R = 4.5 # disc radius, the module everything scales from
|
||||
GOLD = (0.93, 0.66, 0.09)
|
||||
BLUE = (0.18, 0.44, 0.93)
|
||||
GREY = (0.42, 0.46, 0.52)
|
||||
RED = (0.85, 0.29, 0.24)
|
||||
GREEN = (0.23, 0.65, 0.35)
|
||||
|
||||
def add(name, shape, color, transparency=0):
|
||||
o = doc.addObject("Part::Feature", name)
|
||||
o.Shape = shape
|
||||
o.ViewObject.ShapeColor = color
|
||||
o.ViewObject.LineColor = color
|
||||
o.ViewObject.PointColor = color
|
||||
o.ViewObject.Transparency = transparency
|
||||
return o
|
||||
|
||||
def frame(origin, zdir, xdir):
|
||||
"""Right-handed placement matrix from origin + Z + X (X orthonormalised against Z)."""
|
||||
z = Vector(*zdir); z.normalize()
|
||||
xr = Vector(*xdir)
|
||||
x = xr.sub(Vector(z).multiply(z.dot(xr))); x.normalize()
|
||||
y = z.cross(x)
|
||||
return App.Matrix(x.x, y.x, z.x, origin[0],
|
||||
x.y, y.y, z.y, origin[1],
|
||||
x.z, y.z, z.z, origin[2],
|
||||
0, 0, 0, 1)
|
||||
|
||||
# ---------------------------------------------------------------- the bracket
|
||||
plate = Part.makeBox(120, 46, 8)
|
||||
bore = Part.makeCylinder(7, 40, Vector(96, 23, -6)) # a real bore, curved face
|
||||
boss = Part.makeCylinder(11, 7, Vector(96, 23, 8))
|
||||
part = plate.fuse(boss).cut(bore)
|
||||
add("Bracket", part, (0.60, 0.63, 0.66))
|
||||
|
||||
# ---------------------------------------------------------------- glyph pieces
|
||||
def ring(t=None):
|
||||
t = t or R * 0.10
|
||||
return Part.makeCylinder(R, t).cut(Part.makeCylinder(R * 0.84, t))
|
||||
|
||||
def quadrant(t=None):
|
||||
t = t or R * 0.10
|
||||
return Part.makeCylinder(R * 0.84, t, Vector(0, 0, 0), Vector(0, 0, 1), 90)
|
||||
|
||||
def stem(L=None, r=None):
|
||||
return Part.makeCylinder(r or R * 0.09, L or R * 2.3)
|
||||
|
||||
def solid_head():
|
||||
return Part.makeCone(R * 0.32, 0, R * 0.80, Vector(0, 0, R * 2.3))
|
||||
|
||||
def shell_head():
|
||||
outer = Part.makeCone(R * 0.32, 0, R * 0.80, Vector(0, 0, R * 2.3))
|
||||
inner = Part.makeCone(R * 0.22, 0, R * 0.62, Vector(0, 0, R * 2.3))
|
||||
return outer.cut(inner)
|
||||
|
||||
def short_axis(direction, L=None):
|
||||
L = L or R * 1.15
|
||||
return Part.makeCylinder(R * 0.07, L, Vector(0, 0, 0), Vector(*direction))
|
||||
|
||||
def place(shape, m):
|
||||
s = shape.copy()
|
||||
s.transformShape(m)
|
||||
return s
|
||||
|
||||
# ---------------------------------------------------------------- the variants
|
||||
def variant_A(tag, origin): # Onshape baseline
|
||||
m = frame(origin, (0, 0, 1), (1, 0, 0))
|
||||
add(tag + "_ring", place(ring(), m), GREY)
|
||||
add(tag + "_quad", place(quadrant(), m), GOLD)
|
||||
add(tag + "_x", place(short_axis((1, 0, 0)), m), RED)
|
||||
add(tag + "_y", place(short_axis((0, 1, 0)), m), GREEN)
|
||||
add(tag + "_z", place(short_axis((0, 0, 1), R * 1.6), m), BLUE)
|
||||
|
||||
def variant_B(tag, origin, zdir=(0, 0, 1)): # solid cone = driven
|
||||
m = frame(origin, zdir, (1, 0, 0))
|
||||
add(tag + "_ring", place(ring(), m), BLUE)
|
||||
add(tag + "_quad", place(quadrant(), m), GOLD)
|
||||
add(tag + "_body", place(stem().fuse(solid_head()), m), BLUE)
|
||||
|
||||
def variant_C(tag, origin, zdir=(0, 0, 1)): # hollow collar = fixed
|
||||
m = frame(origin, zdir, (1, 0, 0))
|
||||
add(tag + "_ring", place(ring(), m), GREY)
|
||||
add(tag + "_quad", place(quadrant(), m), GOLD)
|
||||
add(tag + "_body", place(stem().fuse(shell_head()), m), GREY)
|
||||
|
||||
def variant_D_pin(tag, origin, zdir=(0, 0, 1)): # polarity by relief: raised PIN
|
||||
m = frame(origin, zdir, (1, 0, 0))
|
||||
pin = Part.makeCylinder(R * 0.30, R * 1.5).fuse(
|
||||
Part.makeCone(R * 0.30, 0, R * 0.55, Vector(0, 0, R * 1.5)))
|
||||
add(tag + "_ring", place(ring(), m), BLUE)
|
||||
add(tag + "_quad", place(quadrant(), m), GOLD)
|
||||
add(tag + "_pin", place(pin, m), BLUE)
|
||||
|
||||
def variant_D_cup(tag, origin, zdir=(0, 0, 1)): # polarity by relief: sunk CUP
|
||||
m = frame(origin, zdir, (1, 0, 0))
|
||||
cup = Part.makeCylinder(R * 0.62, R * 0.9).cut(
|
||||
Part.makeCylinder(R * 0.40, R * 0.9, Vector(0, 0, -0.01)))
|
||||
add(tag + "_ring", place(ring(), m), GREY)
|
||||
add(tag + "_quad", place(quadrant(), m), GOLD)
|
||||
add(tag + "_cup", place(cup, m), GREY)
|
||||
|
||||
# four treatments across the plate, all on the same flat face, same Z
|
||||
variant_A("A", (14, 30, 8))
|
||||
variant_B("B", (40, 30, 8))
|
||||
variant_C("C", (64, 30, 8))
|
||||
variant_D_pin("Dpin", (14, 10, 8))
|
||||
variant_D_cup("Dcup", (40, 10, 8))
|
||||
|
||||
# the hard cases, which is the whole reason for doing this in 3D:
|
||||
variant_B("Bore", (96, 23, 15)) # on the boss above a bore
|
||||
variant_B("Edge", (64, 0, 8), (0, -0.7071, 0.7071)) # tilted, on an edge, oblique Z
|
||||
|
||||
doc.recompute()
|
||||
|
||||
v = Gui.activeDocument().activeView()
|
||||
v.viewIsometric()
|
||||
Gui.SendMsgToActiveView("ViewFit")
|
||||
App.Console.PrintMessage("glyph probe built: %d objects\n" % len(doc.Objects))
|
||||
|
Before Width: | Height: | Size: 35 KiB |
@@ -1,112 +0,0 @@
|
||||
"""Give the bear a handedness mark that survives rasterisation — wi3z, Tommaso's call 2.
|
||||
|
||||
The study showed the left/right cue lives in sub-millimetre corner radii and is therefore invisible
|
||||
at glyph size: one pixel is 2.6 mm at 32 px. Roll and verse are safe; handedness is not.
|
||||
|
||||
THE MEASURE IS THE QUESTION ITSELF. Render the glyph, render its mirror image, and count how many
|
||||
pixels differ. If a human is to tell left from right, the two must differ on screen; a candidate
|
||||
that scores near zero is invisible however elegant it looks in CAD. Reported as a percentage of the
|
||||
glyph's own lit area, so the sizes are comparable.
|
||||
"""
|
||||
import json, math, os
|
||||
from PIL import Image, ImageDraw, ImageChops
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
D = json.load(open(os.path.join(HERE, "bear_outline.json")))
|
||||
def unit(pts):
|
||||
p = [(x, -z) for x, z in pts]
|
||||
return p
|
||||
outer = unit(D["outer"]); holes = [unit(h["pts"]) for h in D["holes"]]
|
||||
ALL = outer + [p for h in holes for p in h]
|
||||
xs=[p[0] for p in ALL]; ys=[p[1] for p in ALL]
|
||||
CX,CY = (min(xs)+max(xs))/2,(min(ys)+max(ys))/2
|
||||
SPAN = max(max(xs)-min(xs), max(ys)-min(ys))
|
||||
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(pts, eps):
|
||||
if len(pts) < 3: return pts
|
||||
ax,ay=pts[0]; bx,by=pts[-1]; dx,dy=bx-ax,by-ay
|
||||
n=math.hypot(dx,dy); best,bi=-1.0,0
|
||||
for i in range(1,len(pts)-1):
|
||||
px,py=pts[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 [pts[0],pts[-1]]
|
||||
return rdp(pts[:bi+1],eps)[:-1]+rdp(pts[bi:],eps)
|
||||
def simp(pts,eps):
|
||||
r=rdp(pts+[pts[0]],eps); return r[:-1]
|
||||
|
||||
BASE = simp(OUT, .030) # the 22-vertex outline the study settled on
|
||||
def centroid(p): return (sum(q[0] for q in p)/len(p), sum(q[1] for q in p)/len(p))
|
||||
def circ(cx,cy,r,n=16): return [(cx+r*math.cos(2*math.pi*i/n), cy+r*math.sin(2*math.pi*i/n)) for i in range(n)]
|
||||
EYE_D = []
|
||||
for e in EYES:
|
||||
c=centroid(e); r=(max(p[0] for p in e)-min(p[0] for p in e))/2
|
||||
EYE_D.append((c[0],c[1],r))
|
||||
EYE_D.sort() # [0] = left (x<0), [1] = right
|
||||
|
||||
TOP = max(p[1] for p in BASE)
|
||||
H = TOP - min(p[1] for p in BASE)
|
||||
def ear_tip(sign):
|
||||
cands=[p for p in BASE if p[1] > TOP-0.18*H and (p[0]*sign) > 0]
|
||||
return max(cands, key=lambda p: p[0]*sign) if cands else None
|
||||
LT, RT = ear_tip(-1), ear_tip(+1)
|
||||
|
||||
def notch(tip, sign, k=0.085):
|
||||
"""A wedge bitten out of one ear — background-filled, exactly how the eyes are already drawn."""
|
||||
x,y = tip
|
||||
return [(x, y+0.02), (x - sign*k, y - k*0.55), (x + sign*k*0.15, y - k*1.05)]
|
||||
|
||||
CANDS = {
|
||||
"H0 none": dict(cuts=[], eyes=EYE_D),
|
||||
"H1 notch R ear": dict(cuts=[notch(RT, +1)], eyes=EYE_D),
|
||||
"H2 notch both": dict(cuts=[notch(RT, +1), notch(LT, -1, 0.045)], eyes=EYE_D),
|
||||
"H3 cheek dot": dict(cuts=[circ(EYE_D[1][0]+0.085, EYE_D[1][1]-0.10, 0.038)], eyes=EYE_D),
|
||||
"H4 uneven eyes": dict(cuts=[], eyes=[EYE_D[0], (EYE_D[1][0], EYE_D[1][1], EYE_D[1][2]*1.55)]),
|
||||
}
|
||||
|
||||
def render(c, px, ss=8, mirror=False):
|
||||
S=px*ss; img=Image.new("L",(S,S),0); d=ImageDraw.Draw(img)
|
||||
m = lambda p: (S/2 + (-p[0] if mirror else p[0])*S*0.92, S/2 - p[1]*S*0.92)
|
||||
d.polygon([m(p) for p in BASE], fill=255)
|
||||
d.polygon([m(p) for p in MUZ], fill=0)
|
||||
for cx,cy,r in c["eyes"]:
|
||||
a=m((cx-r,cy+r)); b=m((cx+r,cy-r))
|
||||
d.ellipse([min(a[0],b[0]), min(a[1],b[1]), max(a[0],b[0]), max(a[1],b[1])], fill=0)
|
||||
for cut in c["cuts"]:
|
||||
d.polygon([m(p) for p in cut], fill=0)
|
||||
return img.resize((px,px), Image.LANCZOS)
|
||||
|
||||
SIZES=[22,32,48]
|
||||
print(f"{'candidate':16} " + " ".join(f"{s}px" for s in SIZES) + " (pixels differing from own mirror, % of lit area)")
|
||||
print("-"*84)
|
||||
scores={}
|
||||
for name,c in CANDS.items():
|
||||
row=[]
|
||||
for px in SIZES:
|
||||
a=render(c,px); b=render(c,px,mirror=True)
|
||||
diff=ImageChops.difference(a,b)
|
||||
nd=sum(1 for v in diff.getdata() if v>40)
|
||||
lit=sum(1 for v in a.getdata() if v>40) or 1
|
||||
row.append(100.0*nd/lit)
|
||||
scores[name]=row
|
||||
print(f"{name:16} " + " ".join(f"{v:5.1f}" for v in row))
|
||||
|
||||
pad,cell=8,58
|
||||
W=pad+len(SIZES)*2*cell+pad; Hh=pad+len(CANDS)*cell+pad
|
||||
sheet=Image.new("RGB",(W,Hh),(24,27,32))
|
||||
for r,(name,c) in enumerate(CANDS.items()):
|
||||
for mi,mir in enumerate((False,True)):
|
||||
for si,px in enumerate(SIZES):
|
||||
g=render(c,px,mirror=mir)
|
||||
tile=Image.new("RGB",(px,px),(24,27,32))
|
||||
tile.paste(Image.new("RGB",(px,px),(237,168,23)),(0,0),g)
|
||||
x=pad+(mi*len(SIZES)+si)*cell+(cell-px)//2
|
||||
y=pad+r*cell+(cell-px)//2
|
||||
sheet.paste(tile,(x,y))
|
||||
sheet.resize((W*2,Hh*2), Image.NEAREST).save(os.path.join(HERE,"handedness-sheet.png"))
|
||||
print("\nleft block = as drawn, right block = mirrored. rows: " + ", ".join(CANDS))
|
||||
print("WROTE handedness-sheet.png")
|
||||
|
Before Width: | Height: | Size: 20 KiB |
@@ -1,135 +0,0 @@
|
||||
# Build the complementary FEMALE for BearConnector.step.
|
||||
#
|
||||
# Method: take the supplied male B-rep as-is, grow it by a uniform clearance, and subtract that
|
||||
# from a block. Working on the real solid rather than re-modelling the bear is the whole point —
|
||||
# the pocket is then exactly complementary by construction, including every deliberate asymmetry.
|
||||
#
|
||||
# The offset uses join=2 (Intersection), which extends the adjacent planes and meets them at a
|
||||
# sharp corner. For a faceted part that is the correct join: the arc join would round every convex
|
||||
# edge and blunt the very cues the design depends on.
|
||||
#
|
||||
# THE MALE'S NATIVE FRAME: the flat back is the plane Y=0 and the relief rises to Y=+17.27.
|
||||
# X and Z carry the face (83.34 x 66.69). The frame is kept exactly as supplied so that male and
|
||||
# female drop into the same assembly without anyone having to re-orient one of them.
|
||||
# Insertion is therefore along +Y, and the pocket must OPEN on the Y=0 plane.
|
||||
#
|
||||
# A first version of this script assumed the relief ran along +Z, built the block around the wrong
|
||||
# axis, and produced a sealed cavity with no way in. It passed a "male does not intersect female"
|
||||
# check, because that only tests the seated position and says nothing about whether the part can
|
||||
# get there. The straight-pull test below is what catches it.
|
||||
#
|
||||
# Run: /snap/bin/freecad.cmd make_female.py
|
||||
|
||||
import os, sys, math
|
||||
import FreeCAD as App
|
||||
import Part
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
MALE = os.path.join(HERE, "bear.step")
|
||||
OUT_STEP = os.path.join(HERE, "BearConnector_Female.step")
|
||||
|
||||
CLEAR = 0.20 # per-face clearance, mm
|
||||
WALL = 4.0 # material around the pocket, mm
|
||||
FLOOR = 3.0 # material behind the deepest point of the pocket, mm
|
||||
|
||||
male = Part.Shape(); male.read(MALE)
|
||||
if len(male.Solids) != 1:
|
||||
print(f"FAIL: expected 1 solid in the male, found {len(male.Solids)}"); sys.exit(1)
|
||||
male = male.Solids[0]
|
||||
bb = male.BoundBox
|
||||
print(f"male : {bb.XLength:.2f} (X) x {bb.YLength:.2f} (Y) x {bb.ZLength:.2f} (Z) mm, "
|
||||
f"{len(male.Faces)} faces, {male.Volume/1000:.2f} cm3")
|
||||
print(f" relief runs Y {bb.YMin:.2f} .. {bb.YMax:.2f} -> insertion along +Y, mouth at Y={bb.YMin:.2f}")
|
||||
|
||||
# ---- 1. can the male even be withdrawn along the insertion axis? ----------------------
|
||||
# Ray-cast a grid along +Y through the tessellated male and count crossings. A straight pull is
|
||||
# possible only if no ray enters the solid more than once; a second entry is an undercut.
|
||||
verts, facets = male.tessellate(0.15)
|
||||
V = [(v.x, v.y, v.z) for v in verts]
|
||||
worst, undercut_pts = 0, 0
|
||||
NX = NZ = 90
|
||||
for i in range(NX):
|
||||
x = bb.XMin + (i + 0.5) * bb.XLength / NX
|
||||
for j in range(NZ):
|
||||
z = bb.ZMin + (j + 0.5) * bb.ZLength / NZ
|
||||
hits = 0
|
||||
for (ia, ib, ic) in facets: # ray (x, *, z) along +Y vs triangle
|
||||
ax, ay, az = V[ia]; bx, by, bz = V[ib]; cx, cy, cz = V[ic]
|
||||
# 2D point-in-triangle in the XZ plane
|
||||
d = (bz - cz) * (ax - cx) + (cx - bx) * (az - cz)
|
||||
if abs(d) < 1e-12: continue
|
||||
u = ((bz - cz) * (x - cx) + (cx - bx) * (z - cz)) / d
|
||||
v = ((cz - az) * (x - cx) + (ax - cx) * (z - cz)) / d
|
||||
if u < 0 or v < 0 or u + v > 1: continue
|
||||
hits += 1
|
||||
worst = max(worst, hits)
|
||||
if hits > 2: undercut_pts += 1
|
||||
print(f"pull : max crossings along +Y = {worst}, undercut samples = {undercut_pts}/{NX*NZ}")
|
||||
if undercut_pts:
|
||||
print("FAIL: the male has an undercut along +Y; a straight pocket cannot release it")
|
||||
sys.exit(1)
|
||||
print(" no undercut -> a straight-pull pocket works")
|
||||
|
||||
# ---- 2. grow the male by the clearance -----------------------------------------------
|
||||
grown = None
|
||||
for join, name in ((2, "Intersection"), (1, "Tangent"), (0, "Arc")):
|
||||
try:
|
||||
g = male.makeOffsetShape(CLEAR, 1e-6, False, False, 0, join, False)
|
||||
if g.isValid() and g.Solids:
|
||||
grown = g.Solids[0]; print(f"offset: join={name}, {grown.Volume/1000:.2f} cm3"); break
|
||||
except Exception as e:
|
||||
print(f"offset: join={name} failed -- {e}")
|
||||
if grown is None:
|
||||
print("FAIL: could not offset the male; refusing to emit a zero-clearance pocket"); sys.exit(1)
|
||||
|
||||
# ---- 3. the block: walls in X and Z, depth in +Y, OPEN at the Y=0 mouth ---------------
|
||||
gb = grown.BoundBox
|
||||
y_mouth = bb.YMin # the male's flat back plane
|
||||
depth = gb.YMax - y_mouth
|
||||
block = Part.makeBox(gb.XLength + 2*WALL, depth + FLOOR, gb.ZLength + 2*WALL,
|
||||
App.Vector(gb.XMin - WALL, y_mouth, gb.ZMin - WALL))
|
||||
print(f"block : {gb.XLength + 2*WALL:.2f} x {depth + FLOOR:.2f} x {gb.ZLength + 2*WALL:.2f} mm, "
|
||||
f"mouth on the Y={y_mouth:.2f} plane")
|
||||
|
||||
female = block.cut(grown)
|
||||
|
||||
# ---- 4. verify --------------------------------------------------------------------------
|
||||
ok = True
|
||||
if not female.isValid(): print("FAIL: invalid shape"); ok = False
|
||||
if len(female.Solids) != 1: print(f"FAIL: {len(female.Solids)} solids"); ok = False
|
||||
|
||||
clash = male.common(female)
|
||||
cv = clash.Volume if clash.Solids else 0.0
|
||||
print(f"check : male ∩ female = {cv:.6f} mm3 (seated fit, must be ~0)")
|
||||
if cv > 1e-3: print("FAIL: male collides with female"); ok = False
|
||||
|
||||
# the mouth must actually be open: the pocket has to reach the Y=y_mouth face of the block
|
||||
mouth_face_area = 0.0
|
||||
for f in female.Faces:
|
||||
c = f.CenterOfMass
|
||||
if abs(c.y - y_mouth) < 1e-6:
|
||||
mouth_face_area += f.Area
|
||||
solid_mouth = (gb.XLength + 2*WALL) * (gb.ZLength + 2*WALL)
|
||||
open_area = solid_mouth - mouth_face_area
|
||||
print(f"check : mouth plane -- material {mouth_face_area:.1f} mm2, opening {open_area:.1f} mm2 "
|
||||
f"({100*open_area/solid_mouth:.1f}% of the face)")
|
||||
if open_area < 100:
|
||||
print("FAIL: the pocket is sealed -- the male cannot be inserted"); ok = False
|
||||
|
||||
cavity = block.Volume - female.Volume
|
||||
print(f"check : cavity {cavity/1000:.2f} cm3 vs male {male.Volume/1000:.2f} cm3 "
|
||||
f"-> clearance shell {(cavity-male.Volume)/1000:.2f} cm3")
|
||||
if cavity < male.Volume: print("FAIL: cavity smaller than the male"); ok = False
|
||||
|
||||
if not ok:
|
||||
print("\nREFUSING to write the STEP"); sys.exit(1)
|
||||
|
||||
doc = App.newDocument("Female")
|
||||
obj = doc.addObject("Part::Feature", "BearConnector_Female")
|
||||
obj.Shape = female
|
||||
doc.recompute()
|
||||
Part.export([obj], OUT_STEP)
|
||||
fb = female.BoundBox
|
||||
print(f"\nwrote {OUT_STEP}")
|
||||
print(f"female: {fb.XLength:.2f} x {fb.YLength:.2f} x {fb.ZLength:.2f} mm, "
|
||||
f"{len(female.Faces)} faces, {female.Volume/1000:.2f} cm3")
|
||||
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 10 KiB |
@@ -1,71 +0,0 @@
|
||||
"""The muzzle has to READ, not just be present — wi3z.
|
||||
|
||||
Faithfully scaled, the part's ridge is 11.3 mm on an 83 mm face: 13.6 % of the width. At glyph
|
||||
size that is a scratch. A glyph is a symbol, not a scale model, so the question is how much
|
||||
emphasis it takes before the only +Z feature actually reads. Variants, all with the same crest
|
||||
geometry, differing only in width and colour.
|
||||
"""
|
||||
import math, os, importlib.util
|
||||
from PIL import Image, ImageDraw
|
||||
spec=importlib.util.spec_from_file_location("gp","glyph_preview.py")
|
||||
gp=importlib.util.module_from_spec(spec); spec.loader.exec_module(gp)
|
||||
|
||||
OUT, CHIN, MARKS, CREST, SBASE, PLATE = gp.OUT, gp.CHIN, gp.MARKS, gp.CREST, gp.SBASE, gp.PLATE
|
||||
BODY=(0.42,0.46,0.52); MARK=(0.126,0.138,0.156); GOLD=(0.93,0.66,0.09)
|
||||
|
||||
def facets(widen=1.0, muzzle_gold=False):
|
||||
F=[]; n=len(OUT)
|
||||
for i in range(n):
|
||||
a,b=OUT[i],OUT[(i+1)%n]
|
||||
F.append(([(a[0],a[1],0.0),(b[0],b[1],0.0),(b[0],b[1],PLATE),(a[0],a[1],PLATE)],BODY,True))
|
||||
F.append(([(x,y,PLATE) for x,y in OUT],BODY,True))
|
||||
zm=PLATE+0.004
|
||||
for cx,cy,r in MARKS:
|
||||
F.append(([(cx+r*math.cos(2*math.pi*i/12),cy+r*math.sin(2*math.pi*i/12),zm) for i in range(12)],MARK,False))
|
||||
F.append(([(x,y,zm) for x,y in CHIN],MARK,False))
|
||||
A,B=CREST
|
||||
w=lambda p:(p[0]*widen,p[1],PLATE)
|
||||
nl,nr,tr,tl=(w(SBASE[0]),w(SBASE[1]),w(SBASE[2]),w(SBASE[3]))
|
||||
col = GOLD if muzzle_gold else BODY
|
||||
F+=[([nl,tl,B,A],col,True),([nr,A,B,tr],col,True),
|
||||
([nl,A,nr],col,True), ([tr,B,tl],col,True)]
|
||||
return F
|
||||
|
||||
def render(F, px, elev, ss=8):
|
||||
S=px*ss; a=math.radians(elev); ca,sa=math.cos(a),math.sin(a)
|
||||
xf=lambda p:(p[0],p[1]*sa+p[2]*ca,-p[1]*ca+p[2]*sa)
|
||||
light=(-0.70,0.30,0.45)
|
||||
img=Image.new("RGB",(S,S),(24,27,32)); d=ImageDraw.Draw(img)
|
||||
tris=sorted(((sum(v[2] for v in [xf(q) for q in pts])/len(pts),[xf(q) for q in pts],c,sh)
|
||||
for pts,c,sh in F), key=lambda t:t[0])
|
||||
for _,q,base,shade in tris:
|
||||
(x0,y0,z0),(x1,y1,z1),(x2,y2,z2)=q[0],q[1],q[2]
|
||||
ux,uy,uz=x1-x0,y1-y0,z1-z0; vx,vy,vz=x2-x0,y2-y0,z2-z0
|
||||
nx,ny,nz=uy*vz-uz*vy,uz*vx-ux*vz,ux*vy-uy*vx
|
||||
L=math.sqrt(nx*nx+ny*ny+nz*nz) or 1.0; nx,ny,nz=nx/L,ny/L,nz/L
|
||||
if nz<0: nx,ny,nz=-nx,-ny,-nz
|
||||
k=(0.42+0.58*max(0.0,nx*light[0]+ny*light[1]+nz*light[2])) if shade else 1.0
|
||||
d.polygon([(S/2+p[0]*S*0.92,S/2-p[1]*S*0.92) for p in q],
|
||||
fill=tuple(min(255,int(255*c*k)) for c in base))
|
||||
return img.resize((px,px),Image.LANCZOS)
|
||||
|
||||
VAR=[("V1 faithful", 1.0, False),
|
||||
("V2 gold muzzle", 1.0, True),
|
||||
("V3 gold + 1.8x wide",1.8, True),
|
||||
("V4 body + 1.8x wide",1.8, False)]
|
||||
big=Image.new("RGB",(4*250+30,4*140+30),(24,27,32))
|
||||
for r,(name,wd,gold) in enumerate(VAR):
|
||||
F=facets(wd,gold)
|
||||
for c,e in enumerate((90,47,16,6)):
|
||||
big.paste(render(F,120,e),(15+c*250+60,15+r*140+10))
|
||||
big.save("/tmp/muzzle-variants.png")
|
||||
for name,wd,gold in VAR:
|
||||
F=facets(wd,gold); F0=[f for f in F][:-4]
|
||||
row=[]
|
||||
for e in (90,16,6):
|
||||
a=render(F,32,e); b=render(F0,32,e)
|
||||
la=sum(1 for p in a.get_flattened_data() if p!=(24,27,32))
|
||||
df=sum(1 for p,q in zip(a.get_flattened_data(),b.get_flattened_data()) if p!=q)
|
||||
row.append(f"{100.0*df/max(1,la):5.1f}%")
|
||||
print(f"{name:22} muzzle share at 90/16/6 deg: " + " ".join(row))
|
||||
print("WROTE /tmp/muzzle-variants.png")
|
||||
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 24 KiB |
@@ -1,99 +0,0 @@
|
||||
"""Flat glyph vs 3D relief, at the elevations that killed the disc — wi3z.
|
||||
|
||||
The flat study collapsed at 16 deg because anything drawn IN the connector's plane foreshortens by
|
||||
sin(elevation). This renders the SAME bear as its real relief (1508 facets off the supplied male)
|
||||
with a simple lambert shade, so the silhouette does the work at a grazing angle. Two rows, same
|
||||
sizes, same elevations, so the comparison is direct.
|
||||
"""
|
||||
import json, math, os
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
M = json.load(open(os.path.join(HERE, "bear_mesh.json")))
|
||||
V, F = M["v"], M["f"]
|
||||
|
||||
# Part frame: face carried by X (right) and Z (down-negative), relief along +Y.
|
||||
P = [(v[0], -v[2], v[1]) for v in V] # -> (x right, y up, z out of the face)
|
||||
xs=[p[0] for p in P]; ys=[p[1] for p in P]; zs=[p[2] for p in P]
|
||||
CX,CY,CZ = (min(xs)+max(xs))/2, (min(ys)+max(ys))/2, (min(zs)+max(zs))/2
|
||||
SPAN = max(max(xs)-min(xs), max(ys)-min(ys))
|
||||
P = [((x-CX)/SPAN, (y-CY)/SPAN, (z-CZ)/SPAN) for x,y,z in P]
|
||||
|
||||
def shade(px, elev_deg, supersample=8):
|
||||
"""Camera orbits down from straight-on (90) to grazing (small). Rotate about the screen x-axis."""
|
||||
S = px*supersample
|
||||
a = math.radians(elev_deg)
|
||||
ca, sa = math.cos(a), math.sin(a)
|
||||
# view: rotate the model so the face normal tips away from the camera
|
||||
def xf(p):
|
||||
x,y,z = p
|
||||
return (x, y*sa + z*ca, -y*ca + z*sa) # third component = depth toward camera
|
||||
Q = [xf(p) for p in P]
|
||||
img = Image.new("L", (S,S), 0)
|
||||
d = ImageDraw.Draw(img)
|
||||
order = []
|
||||
for tri in F:
|
||||
a3 = [Q[i] for i in tri]
|
||||
order.append((sum(v[2] for v in a3)/3.0, tri, a3))
|
||||
order.sort(key=lambda t: t[0]) # painter: far first
|
||||
light = (-0.35, 0.55, 0.76)
|
||||
for _, tri, a3 in order:
|
||||
(x0,y0,z0),(x1,y1,z1),(x2,y2,z2) = a3
|
||||
ux,uy,uz = x1-x0, y1-y0, z1-z0
|
||||
vx,vy,vz = x2-x0, y2-y0, z2-z0
|
||||
nx,ny,nz = uy*vz-uz*vy, uz*vx-ux*vz, ux*vy-uy*vx
|
||||
n = math.sqrt(nx*nx+ny*ny+nz*nz) or 1.0
|
||||
nx,ny,nz = nx/n, ny/n, nz/n
|
||||
if nz < 0: nx,ny,nz = -nx,-ny,-nz # face the camera
|
||||
lam = max(0.0, nx*light[0] + ny*light[1] + nz*light[2])
|
||||
val = int(70 + 185*lam)
|
||||
pts = [(S/2 + x*S*0.92, S/2 - y*S*0.92) for x,y,_ in a3]
|
||||
d.polygon(pts, fill=val)
|
||||
return img.resize((px,px), Image.LANCZOS)
|
||||
|
||||
# flat outline, for the side-by-side
|
||||
D = json.load(open(os.path.join(HERE, "bear_outline.json")))
|
||||
def unit(pts):
|
||||
p=[(x,-z) for x,z in pts]
|
||||
return [((x-CX)/SPAN,(y-CY)/SPAN) for x,y in p]
|
||||
OUT = unit(D["outer"])
|
||||
HOLES = [unit(h["pts"]) for h in D["holes"]]
|
||||
|
||||
def flat(px, elev_deg, supersample=8):
|
||||
S=px*supersample
|
||||
img=Image.new("L",(S,S),0); d=ImageDraw.Draw(img)
|
||||
k=math.sin(math.radians(elev_deg))
|
||||
m=lambda p:(S/2+p[0]*S*0.92, S/2-p[1]*S*0.92*k)
|
||||
d.polygon([m(p) for p in OUT], fill=255)
|
||||
for h in HOLES: d.polygon([m(p) for p in h], fill=0)
|
||||
return img.resize((px,px), Image.LANCZOS)
|
||||
|
||||
SIZES=[22,32,48]; ELEVS=[(90,"flat on"),(47,"47"),(16,"16"),(6,"6")]
|
||||
pad,cell=8,58
|
||||
W=pad+len(SIZES)*len(ELEVS)*cell+pad; H=pad+2*cell+pad
|
||||
sheet=Image.new("RGB",(W,H),(24,27,32))
|
||||
for r,fn in enumerate((flat, shade)):
|
||||
for ci,(elev,_) in enumerate(ELEVS):
|
||||
for si,px in enumerate(SIZES):
|
||||
g=fn(px,elev)
|
||||
tile=Image.new("RGB",(px,px),(24,27,32))
|
||||
if fn is flat:
|
||||
tile.paste(Image.new("RGB",(px,px),(237,168,23)),(0,0),g)
|
||||
else:
|
||||
gg=g.convert("L")
|
||||
tile=Image.merge("RGB",(gg.point(lambda v:min(255,int(v*1.00))),
|
||||
gg.point(lambda v:int(v*0.71)),
|
||||
gg.point(lambda v:int(v*0.16))))
|
||||
x=pad+(ci*len(SIZES)+si)*cell+(cell-px)//2
|
||||
y=pad+r*cell+(cell-px)//2
|
||||
sheet.paste(tile,(x,y))
|
||||
sheet.resize((W*2,H*2), Image.NEAREST).save(os.path.join(HERE,"relief-sheet.png"))
|
||||
|
||||
# how much ink survives — the same measure used on the disc glyph
|
||||
print(f"{'elev':>6} {'flat px@32':>11} {'relief px@32':>13}")
|
||||
for elev,_ in ELEVS:
|
||||
f32=flat(32,elev); s32=shade(32,elev)
|
||||
fi=sum(1 for v in f32.getdata() if v>40)
|
||||
si=sum(1 for v in s32.getdata() if v>40)
|
||||
print(f"{elev:>6} {fi:>11} {si:>13}")
|
||||
print("WROTE relief-sheet.png")
|
||||
@@ -1,9 +0,0 @@
|
||||
# Export the real male's relief as a triangle mesh, so the grazing test uses the actual geometry.
|
||||
import os, json
|
||||
import Part
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
s = Part.Shape(); s.read(os.path.join(HERE, "bear.step"))
|
||||
verts, facets = s.Solids[0].tessellate(0.25)
|
||||
V = [[round(p.x,4), round(p.y,4), round(p.z,4)] for p in verts]
|
||||
json.dump({"v": V, "f": facets}, open(os.path.join(HERE, "bear_mesh.json"), "w"))
|
||||
print(f"verts {len(V)} facets {len(facets)}")
|
||||
@@ -1,85 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Flat-shade the faceted ridge key from several camera directions.
|
||||
|
||||
The point is not a pretty picture. It is one question: does a low-poly solid, flat-shaded,
|
||||
let a human read its orientation from an arbitrary viewpoint -- and specifically, is the
|
||||
view ALONG the ridge ambiguous between front and back, as the geometry suggests it must be
|
||||
in silhouette?
|
||||
|
||||
Flat shading (one normal per facet, no smoothing) is deliberate: it is what the concept
|
||||
claims to rely on, and it is what a CAD viewport with hard normals actually produces.
|
||||
"""
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
# ---- the key, same numbers as faceted_ridge_key.scad
|
||||
L, W, tf, H, pr, pf, hf = 12.0, 4.0, 0.45, 4.5, 0.22, 0.62, 0.35
|
||||
Wf, xr0, xr1, Hf = W * tf, -L / 2 + L * pr, -L / 2 + L * pf, H * hf
|
||||
|
||||
V = np.array([(-L/2, -W, 0), (-L/2, W, 0), (L/2, Wf, 0), (L/2, -Wf, 0),
|
||||
(xr0, 0, H), (xr1, 0, Hf)], dtype=float)
|
||||
F = [[0, 1, 2, 3], [0, 4, 1], [0, 3, 5], [0, 5, 4], [1, 4, 5], [1, 5, 2], [3, 2, 5]]
|
||||
|
||||
LIGHT = np.array([0.35, -0.5, 0.78]) # a headlight-ish key light
|
||||
LIGHT /= np.linalg.norm(LIGHT)
|
||||
|
||||
|
||||
def look_at(eye, target, up=(0, 0, 1)):
|
||||
f = np.array(target, float) - np.array(eye, float)
|
||||
f /= np.linalg.norm(f)
|
||||
up = np.array(up, float)
|
||||
if abs(np.dot(f, up)) > 0.999:
|
||||
up = np.array([0, 1, 0], float)
|
||||
r = np.cross(f, up); r /= np.linalg.norm(r)
|
||||
u = np.cross(r, f)
|
||||
return r, u, f
|
||||
|
||||
|
||||
def render(eye, target, path, size=(620, 460), scale=26.0, label=""):
|
||||
r, u, f = look_at(eye, target)
|
||||
eye = np.array(eye, float)
|
||||
cam = np.stack([r, u, f]) # world -> camera rows
|
||||
P = (V - eye) @ cam.T # orthographic: x,y screen, z depth
|
||||
|
||||
w, h = size
|
||||
img = Image.new("RGB", size, (238, 240, 243))
|
||||
d = ImageDraw.Draw(img)
|
||||
|
||||
def to_px(p):
|
||||
return (w / 2 + p[0] * scale, h / 2 - p[1] * scale)
|
||||
|
||||
faces = []
|
||||
for face in F:
|
||||
pts = V[face]
|
||||
n = np.cross(pts[1] - pts[0], pts[2] - pts[0])
|
||||
n /= np.linalg.norm(n)
|
||||
centre = pts.mean(axis=0)
|
||||
if np.dot(n, centre - eye) > 0: # back-face cull
|
||||
continue
|
||||
depth = P[face][:, 2].mean()
|
||||
lam = max(0.0, float(np.dot(n, LIGHT)))
|
||||
shade = 0.22 + 0.78 * lam # flat: ONE value for the whole facet
|
||||
col = tuple(int(255 * shade * c) for c in (0.86, 0.72, 0.35))
|
||||
faces.append((depth, [to_px(P[i]) for i in face], col))
|
||||
|
||||
for _, poly, col in sorted(faces, key=lambda t: -t[0]): # painter's algorithm
|
||||
d.polygon(poly, fill=col)
|
||||
|
||||
if label:
|
||||
d.rectangle([8, 8, 8 + 9 * len(label), 30], fill=(255, 255, 255))
|
||||
d.text((14, 14), label, fill=(20, 20, 20))
|
||||
img.save(path)
|
||||
return path
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
t = (0, 0, H * 0.35)
|
||||
views = [
|
||||
((26, -22, 20), "iso: the reference view"),
|
||||
((30, 0, 6), "ALONG +X (from the FRONT, low end)"),
|
||||
((-30, 0, 6), "ALONG -X (from the BACK, tall end)"),
|
||||
((0, 0, 34), "ALONG +Z (straight down the mating axis)"),
|
||||
((2, -32, 5), "ALONG -Y (broadside, grazing)"),
|
||||
]
|
||||
for i, (eye, lab) in enumerate(views):
|
||||
print(render(eye, t, f"rk-{i}.png", label=lab))
|
||||
@@ -1,76 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Flat-shade an ASCII/binary STL from several directions.
|
||||
|
||||
Used to answer one question with a picture instead of an argument: does a RECESSED faceted
|
||||
pocket read as an oriented feature, or does a concave feature collapse into a dark hole?
|
||||
"""
|
||||
import struct
|
||||
import sys
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
LIGHT = np.array([0.35, -0.5, 0.78]); LIGHT /= np.linalg.norm(LIGHT)
|
||||
|
||||
|
||||
def load_stl(path):
|
||||
data = open(path, "rb").read()
|
||||
if data[:5] == b"solid" and b"facet" in data[:2000]:
|
||||
tris, cur = [], []
|
||||
for line in data.decode("ascii", "ignore").splitlines():
|
||||
s = line.split()
|
||||
if s and s[0] == "vertex":
|
||||
cur.append([float(x) for x in s[1:4]])
|
||||
if len(cur) == 3:
|
||||
tris.append(cur); cur = []
|
||||
return np.array(tris, dtype=float)
|
||||
n = struct.unpack("<I", data[80:84])[0]
|
||||
tris = np.empty((n, 3, 3), dtype=float)
|
||||
off = 84
|
||||
for i in range(n):
|
||||
v = struct.unpack("<12f", data[off:off + 48])
|
||||
tris[i] = np.array(v[3:12]).reshape(3, 3)
|
||||
off += 50
|
||||
return tris
|
||||
|
||||
|
||||
def render(tris, eye, target, path, size=(620, 460), scale=14.0, label=""):
|
||||
eye = np.array(eye, float); target = np.array(target, float)
|
||||
f = target - eye; f /= np.linalg.norm(f)
|
||||
up = np.array([0, 0, 1.0])
|
||||
if abs(np.dot(f, up)) > 0.999: up = np.array([0, 1.0, 0])
|
||||
r = np.cross(f, up); r /= np.linalg.norm(r)
|
||||
u = np.cross(r, f)
|
||||
cam = np.stack([r, u, f])
|
||||
|
||||
w, h = size
|
||||
img = Image.new("RGB", size, (238, 240, 243)); d = ImageDraw.Draw(img)
|
||||
faces = []
|
||||
for t in tris:
|
||||
n = np.cross(t[1] - t[0], t[2] - t[0])
|
||||
ln = np.linalg.norm(n)
|
||||
if ln < 1e-12: continue
|
||||
n /= ln
|
||||
c = t.mean(axis=0)
|
||||
if np.dot(n, c - eye) > 0: continue # cull back faces
|
||||
P = (t - eye) @ cam.T
|
||||
lam = max(0.0, float(np.dot(n, LIGHT)))
|
||||
shade = 0.20 + 0.80 * lam
|
||||
col = tuple(int(255 * shade * ch) for ch in (0.86, 0.72, 0.35))
|
||||
poly = [(w / 2 + p[0] * scale, h / 2 - p[1] * scale) for p in P]
|
||||
faces.append((P[:, 2].mean(), poly, col))
|
||||
for _, poly, col in sorted(faces, key=lambda x: -x[0]):
|
||||
d.polygon(poly, fill=col)
|
||||
if label:
|
||||
d.rectangle([8, 8, 8 + 9 * len(label), 30], fill=(255, 255, 255))
|
||||
d.text((14, 14), label, fill=(20, 20, 20))
|
||||
img.save(path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
tris = load_stl(sys.argv[1])
|
||||
print("triangles:", len(tris))
|
||||
views = [((26, -22, 20), "iso"), ((0, 0, 34), "straight down +Z"),
|
||||
((4, -30, 9), "grazing"), ((-28, -10, 12), "from the tall end")]
|
||||
for i, (eye, lab) in enumerate(views):
|
||||
render(tris, eye, (0, 0, 0), f"fem-{i}.png", label=f"FEMALE POCKET — {lab}")
|
||||
print(f"fem-{i}.png")
|
||||
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 49 KiB |