diff --git a/TEXTURE_DISPLACEMENT.md b/TEXTURE_DISPLACEMENT.md new file mode 100644 index 0000000000..b5c69a060a --- /dev/null +++ b/TEXTURE_DISPLACEMENT.md @@ -0,0 +1,581 @@ +# Texture Displacement - Technical Notes + +Branch: `feature/texture_displacement`. Reference for the feature as it stands: what it does, how the +algorithms work, and where the code lives. + +## What it does + +A paint-style gizmo (`GLGizmoTextureDisplacement`) that lets you: +- Paint one or more "layers" onto a model's surface, each a height-map texture with its own + depth/tiling/rotation/offset/invert/tile-mode/projection-mode/blend-mode. +- Pick a texture from a shipped library (`resources/textures/displacement/`) or import your own + (saved into `/textures/displacement/`, kept separate so app updates can't clobber it). +- Combine overlapping layers with image-editor-style blend modes (Add/Subtract/Multiply/Divide). +- Preview the true displaced result live, before baking (background job, not on the UI thread). +- Preview via a fast GPU shader instead (no real geometry movement) for a lighter-weight alternative. +- Bake into real mesh geometry on demand, restricted to the painted area only. +- Remesh and subdivide so a low-poly model has enough vertices to show fine detail. +- Unwrap a painted patch with a real CGAL LSCM parameterization and view it in a dedicated, + dockable 2D "UV Editor" pane. + +## Standard vs Pro mode + +A two-position slider in the panel header, right of the Dock/Undock button. + +**Pro** shows every mesh-preparation control; Remesh, Subdivide and Bake are run separately by the user, +in whatever order they like. + +**Standard** hides all of it and folds one fixed recipe into the Bake button, because a height map only +ever *moves vertices that already exist* - painting onto an imported 12-triangle box and pressing Bake +would otherwise do nothing visible. Standard's Bake is: + +1. `plan_remesh()` + `replace_mesh_keep_all_paint()` - isotropic remesh to 1 mm, sharp edges above 40 + degrees protected. Gives the subdivider an even starting density whatever the input looked like. +2. `plan_adaptive_subdivision()` + `apply_adaptive_subdivision()` - feature-adaptive refinement, max + edge 20 mm, detail 0.02 mm, min edge 0.02 mm. +3. `bake()` - the ordinary background displacement job. + +Both preparation stages are *planned* before the undo snapshot and *applied* after it, so a stage with +nothing to do is skipped without leaving an empty undo step. The standalone Pro buttons share the same +plan/apply split. + +**All three stages sit under one undo step.** `Plater::take_snapshot()` records the state *before* the +change, so a single snapshot taken at the top of `bake_standard()` means one Undo returns the mesh to +exactly what was imported. `TextureDisplacementBakeInput::take_snapshot` lets the caller say who owns +the undo step - true for the Pro-mode button, false for the pipeline, whose background job commits long +after that snapshot's scope has closed. + +The presets live in one place (`STD_*` constants) and `apply_standard_mode_presets()` pins the hidden +controls to them every frame while Standard is active, so the live preview cannot disagree with what +Bake will do. Switching to Standard also closes the subdivision preview, whose controls have just gone. + +One control survives into Standard: **"Added triangles (k)"**, the subdivision budget. It is deliberately +*not* pinned - pinning would fight the user's own slider every frame - because unlike the rest of the +recipe its right value depends on the part rather than on the method (a big model, or a fine texture, +simply needs more triangles). Default 1500. The widget is one lambda shared by both layouts. + +Standard remeshes *after* painting, so the remesh has to preserve paint: `ModelVolume::restore_painting()` +only remaps the four standard channels, so `replace_mesh_keep_all_paint()` additionally runs +`TriangleSelector::remap_painting()` over the eight texture-displacement masks. The Pro Remesh button +goes through the same helper. If the remap comes back empty the pipeline stops with a message rather +than baking a flat mesh. + +## Architecture + +### Data model (per `ModelVolume`) + +Each of up to `TEXTURE_DISPLACEMENT_MAX_LAYERS` (8) layers gets its **own independent +`FacetsAnnotation`** paint mask - the same `TriangleSelector`/`FacetsAnnotation` machinery every other +paint gizmo (FdmSupports, Seam, MMU, FuzzySkin) already uses, just one full instance per layer slot +instead of one per volume. This is what makes layered/blended painting work for free: the same triangle +can be `ENFORCER` in layer 2's mask and layer 5's mask simultaneously, and at bake/preview time each +layer displaces the surface left by the previous one (image-editor-layer semantics). + +Whole-stack settings (border handling, post-process smoothing) live beside the layers in +`texture_displacement_options` (`TextureDisplacementOptions`), since they belong to no single layer. + +### Bake algorithm (`libslic3r/TextureDisplacement.cpp`) + +`build_texture_displacement(base_mesh, layers, facets_data, options)` is **accumulate-then-displace, +and topology-preserving**: the returned mesh has exactly the input's vertices and triangles, in the +same order - only the positions of displaced vertices differ. + +1. `its_compactify_vertices()` on a copy of the input. In practice a no-op (it only drops + *unreferenced* vertices, and preserves the order and indices of the rest). It is there to + guarantee the index alignment step 3 depends on. +2. Area-weighted vertex normals of the **undisplaced** mesh, computed once. Every layer both projects + and displaces along these, so a vertex covered by several layers moves along one single well-defined + direction. Where the paint does *not* cover every triangle around a vertex, the normal is recomputed + from the painted triangles alone (the union over all layers, so it stays one direction per vertex): + on the rim of a fully painted top face the whole-mesh normal is the 45-degree bisector it shares with + the side wall, and displacing along that flares the rim outwards instead of raising it. Interior + vertices are unaffected - all their triangles are painted, so the two normals coincide. Paint + coverage per original triangle comes straight off `TriangleSplittingData::triangles_to_split`. +3. For each layer in slot order: deserialize its stored paint mask into a `TriangleSelector` against + the **base mesh** (never against a previous layer's output), then + `selector.get_facets_strict(ENFORCER)` -> the painted patch. Two facts are exploited: + - `get_facets_strict()` returns the mesh's **entire** referenced vertex array regardless of which + state was asked for - only `.indices` is filtered by state. So `get_facets_strict(ENFORCER)` + and `get_facets_strict(NONE)` share identical vertex indexing, which is what lets boundary + detection be a plain index check instead of a position-hash lookup. + - The selector's vertex array *starts with* the mesh's own vertices (extra ones created where a + brush stroke split a triangle are appended after them), and `get_facets_strict()` emits the + referenced ones in order. Combined with step 1, **selector vertex index `i` is our vertex `i`**. + Split vertices live past the end of our array and are simply skipped - they sit on the paint + boundary anyway (splitting only happens at partial coverage). +4. A vertex used by at least one **unpainted** triangle is a border vertex. Whether it moves is + `TextureDisplacementOptions::displace_border`, and it does by default. Nothing can tear: the bake is + topology-preserving, so a border vertex is *one* vertex shared by both regions and moving it simply + tilts the unpainted triangles that use it. Pinning it instead clamps the outermost ring of relief to + zero, which on a fully painted face collapses the pattern into a ring of steep ramps at the edge; it + is kept as an option for when the relief must not spill past the paint at all. Either way the border + drives the `edge_smoothing` falloff. +5. Per interior vertex: sample the height texture (`sample_layer_height()`, see Projection methods) + and fold `height * depth_mm * (invert ? -1 : 1)` into that vertex's running total via the layer's + `TextureBlendMode` (see Blend modes). A `visited` set makes each layer fold in exactly **once** + per vertex, no matter how many of the patch's triangles share it - otherwise a Multiply/Subtract + layer would apply two or three times over depending on local triangle fan-out. +6. Move each touched vertex along its (step 2) normal by its accumulated total. +7. Optionally (`TextureDisplacementOptions::smooth_*`) relax the result - see Post-process smoothing. + +### Post-process smoothing + +`smooth_mesh_vertices(mesh, movable, strength, iterations)` - Laplacian relaxation, run after all layers +have been folded in, restricted to the vertices flagged in `movable`. Each pass moves a movable vertex a +`strength` fraction of the way to the average of its one-ring, read from a **snapshot** of the previous +pass so the result does not depend on vertex order (a Gauss-Seidel sweep would smooth several times as +hard at the end of the array as at the start). Neighbours come from a CSR-style adjacency built once per +call. Topology-preserving, like the bake. + +Its job is to round off the hard steps a bitmap height map leaves behind - a different knob from +`TextureDisplacementLayer::smoothing`, which blurs the *height map* before it is ever sampled. + +Two ways in, sharing one set of settings on the volume: +- The **"Smooth result"** checkbox + "Smoothing (%)" / "Passes" ride along with Preview and Bake. + `movable` is exactly the set of vertices the displacement moved, so the untouched part of the model + keeps its exact geometry and the ring just outside the displaced set anchors the relaxation (the + relief cannot creep outward). +- **"Smooth baked mesh now"** (`GLGizmoTextureDisplacement::smooth_model()`) applies the same settings to + the volume's *committed* geometry, for relief that is already baked in. `movable` there is the painted + triangles' vertices. Because smoothing never touches the triangle list, this is the one geometry + operation in the gizmo that keeps **every** paint channel verbatim - it saves and restores the eight + texture-displacement masks around `set_mesh()` rather than remapping or dropping them. + +**"Ignore outer ring"** (`smooth_skip_border`, on by default) drops the patch's own outermost ring of +vertices from `movable`. That ring's neighbours *outside* the paint never move, so relaxing it drags the +rim of the relief down toward the flat surface and the pattern comes out half-melted where it meets the +edge. Held out, the border keeps the full depth the texture asked for and only the interior relaxes. +Turning it off softens the outer edge deliberately (a blunter version of the per-layer edge-smoothing +falloff). This is the *smoothing* rim, independent of whether that rim is displaced at all +(`displace_border`, step 4 above); both default to keeping the border sharp. + +### Blend modes + +`TextureBlendMode` {Add, Subtract, Multiply, Divide}, per layer, applied per vertex against the +total accumulated by the layers **below** it (lower slots). The quantity blended is a signed +displacement in **mm**, not a pixel value. + +Add/Subtract are self-explanatory. Multiply/Divide are *scaling* operations and so need a unit +convention: they treat the layer's own value as a **factor relative to 1 mm**. That makes `depth_mm` +a gain, and - the property that makes a Multiply layer usable as a mask - a layer with depth 1 mm +sampling a white (1.0) texel multiplies by exactly 1, i.e. leaves the layers below unchanged. +Divide floors its divisor's magnitude at 0.05: a black texel samples to *exactly* zero, so the divisor +really does hit zero in ordinary use, and an unbounded `1/0` would fling vertices thousands of mm away +and poison the mesh's bounding box (and every plate/print-volume check downstream). The floor doubles as +a cap on how far Divide can amplify the relief beneath it: at most 20x. + +The **lowest painted layer ignores its blend mode**: it has nothing beneath it, and Multiply/Divide +against an implicit zero base would annihilate (or blow up) it. Enforced in +`build_texture_displacement()` (the first layer to reach a given vertex always folds in additively) and +surfaced in the UI, which labels that layer "Base layer" instead of offering a control that does nothing. + +### Projection methods + +Five choices per layer (`TextureProjectionMethod`), all funneling through `apply_uv_transform()` +(scale by `1/tiling_scale`, rotate by `rotation_deg`, add `offset`). They are dispatched by +`sample_layer_height()`, which returns a **height**, not a UV - because Triplanar takes three +texture samples per vertex and so has no single UV that represents it. + +- **Triplanar** (default) - samples the texture on all three world planes (`(y,z)`, `(x,z)`, `(x,y)`) + and blends the three by the vertex's own normal raised to `TRIPLANAR_BLEND_SHARPNESS` (4). Hard-picking + the single axis most aligned with the normal instead is discontinuous wherever that dominant axis + flips: on a +X face the planar coordinate is `(y, z)`, on a -Y face it is `(x, z)`, so at the shared + edge `u` jumps. A weighted blend is continuous across the transition by construction, since the weight + of the axis being left behind falls smoothly to zero. This removes the hard *seam*; some cross-fade + blurring in the band right at a 90° edge is inherent to triplanar mapping. A genuinely seam-free wrap + around a box needs a real unwrap - that is what the LSCM mode is for. +- **Cylindrical** - wraps around an axis through the patch centroid, axis auto-picked as the world + axis *least* aligned with the average normal (perpendicular to the outward radial normal, as a + cylinder's own axis would be). `u = angle * local_radius` (arc length in mm), `v = distance along + axis`. An approximation, not an exact fit for arbitrary geometry, and the axis/centre are not + user-overridable. +- **Spherical** - longitude/latitude around the centroid, scaled by local radius. Same caveat. +- **LSCM** - real UV unwrap via `MeshBoolean::cgal::parameterize_lscm()` (CGAL's + `Surface_mesh_parameterization` package, LSCM algorithm). Computed **once per patch** (not + per-vertex like the others - it's a single global least-squares solve), then each vertex looks up + its precomputed UV. Requires the patch to be a single topological disk (one connected component, + one boundary loop) - `compute_lscm_uvs()` returns empty and the layer falls back to Triplanar if not + (e.g. multiple disconnected painted islands, or a fully closed patch). CGAL's parameterizer needs a + mesh with no isolated/unreferenced vertices, but `get_facets_strict()` returns the *whole* mesh's + vertex array - so `compact_patch_with_map()` builds a clean sub-mesh plus an index map back to the + original vertex numbering, purely local to this file. +- **ViewProjected** ("From view") - a flat projection along a fixed direction captured from the 3D + camera, like a slide projector. `capture_view_projection()` takes the camera's right/up axes, + transforms them into the volume's *local* frame (so the projection rides along if the part is later + moved), and stores them as `TextureDisplacementLayer::view_project_right/up` (unit vectors, so the + projected coordinate stays in mm and `tiling_scale` keeps meaning mm). `sample_layer_height()` + projects `Vec2f(dot(pos, right), dot(pos, up))`. Single-valued per point, so - like LSCM but unlike + blended Triplanar - the fast preview and UV-check overlay precompute it per vertex + (`compute_layer_vertex_uvs()`) and drive the shader's `use_vertex_uv` path. Faces angled away from + the projector smear; that is inherent to view projection. + + Two companions to this mode: + - **Projection frame overlay** (`TextureProjectorFrame`, see below) - a semi-transparent window + dragged over the 3D view whose border becomes the projection's edge. Applying it stores an exact + **projective** map in `view_project_matrix`, which supersedes the affine `right`/`up` axes above + for that layer (`view_project_projective`). + - **"Project only on visible"** (`select_visible_faces()`) - repaints the layer with exactly the + facets the camera can see, so the projected area matches the viewpoint the projector was captured + from. Two tests: a facing test (normal vs. view direction, per triangle - under perspective the + view direction varies across the model, so it is taken from the eye to each centroid), then + `MeshRaycaster::get_unobscured_idxs()` on the survivors to drop facets hidden behind other + geometry, so a concave part's far inner wall is correctly excluded. One ray query per front-facing + facet, hence click-driven (on the checkbox and on each "Capture current view"), never per frame. + It **replaces** the layer's paint rather than adding to it - "project onto what I can see" would + otherwise accumulate every angle the user had ever looked from. + +### Manual seams and island cutting + +`TextureDisplacementLayer::lscm_seam_edges` - undirected mesh-vertex-index edge pairs the unwrap is +forced to cut along, on top of the dihedral-angle seams. `segment_into_charts()` takes a set of these +(translated from mesh -> compacted-patch numbering inside `compute_patch_unwrap()`) and refuses to +union two triangles across a marked edge whatever their angle. Both the unwrap cache key and the +gizmo's `UVEditorState` include the seam list, so marking a seam (which leaves the paint mask +untouched) still forces a re-solve. Like the paint masks, seams are mesh-index-space and so dropped on +any topology change. + +Two ways to write to it: +- **Mark seam (manual)** - a "Mark seams" click mode (`m_seam_edit_mode`) that suppresses painting. A + click raycasts the volume (`m_c->raycaster()->raycasters()[idx]->unproject_on_mesh()`, `idx` = the + volume's slot among model-part volumes), finds the facet's edge nearest the hit point, and toggles it. + Marked edges render as a red overlay (`render_seam_overlay()`), pulled toward the camera so they read + on top. This is the Blender mark-seam workflow. +- **Cut island (auto)** - `cut_island()` takes the selected chart's triangles (back-mapped from the + unwrap via `source_vertex`), finds their 3D bounding box, and marks every edge that straddles the + mid-plane perpendicular to the longest axis. The re-unwrap then splits the chart across its narrow + waist. Exposed as the UV pane's **Cut** button. + +### UV-check overlays (checker / distortion) + +`resources/shaders/{110,140}/texture_displacement_uvcheck.{vs,fs}`, one shader with a `mode` uniform, +drawn over the painted patch (`rebuild_uvcheck_mesh()`/`render_uvcheck_mesh()`, P3N3T2: `normal.x` = +distortion, `tex_coord` = uv), pulled forward with a polygon offset. **Checker** samples a procedural +checkerboard at the layer's uv (per-vertex for LSCM/ViewProjected, in-shader triplanar otherwise) - +squares that stay square mean low distortion. **Distortion** colours each triangle blue->green->red by +`log2(uv_area / surface_area)` centred on the patch's *median* stretch (so a globally-scaled unwrap +reads as uniformly ideal and only relative stretch shows), averaged to vertices. A separate **Show mesh +wireframe** toggle draws the whole volume's triangle edges, rebuilt only when the vertex count changes +(not per stroke). + +### Tiling + +`DecodedHeightTexture::sample(uv, tile_enabled, tile_method)`. Two tile methods when enabled +(Repeat, MirroredRepeat). **When `tile_enabled` is false, sampling outside `[0,1)` returns `0` +directly** rather than clamping the *coordinate* into range, which would smear the border row/column of +pixels outward to infinity in every direction (streaky lines radiating out from the painted patch). + +### Subdivision - two modes + +**Uniform (`subdivide_mesh_uniform()`)** - whole-mesh, 1-to-4 split. Recursive edge-midpoint split with +a shared per-pass midpoint cache (keyed by sorted vertex-index pair) so triangles sharing an edge get +the *same* new vertex - capped at `max_iterations` (default 6). Whole-mesh so it never leaves a +T-junction, at the cost of densifying everywhere. Wired as a "Subdivide steps" slider (**0-5**, 0 = +no subdivision), Apply snaps back to 0. Drops texture-displacement paint (no remap) via the standard +`save_painting()`/`set_mesh()`/`restore_painting()` dance; the other four channels are remapped. + +**Adaptive (`subdivide_mesh_adaptive()`)** - refine **only the painted area**, by **Rivara longest-edge +bisection**, which is *conformal by construction*. Only **terminal** edges are ever bisected - an edge +that is the longest edge of *every* triangle sharing it - which splits both those triangles along one +shared midpoint at once, so a hanging node is never created. The edge to split for a triangle that wants +refining is found by **longest-edge propagation (LEPP)**: walk to the longest edge of ever-longer-edged +neighbours until a terminal one is reached, and bisect that. Edge length strictly increases along the +path (ties broken by mesh-vertex key, which both sides of an edge compute identically), so the walk +cannot cycle, and Rivara's result is that repeating it refines the original triangle in a bounded number +of bisections. The transition triangles it pulls in just outside the painted patch are the graded band +that makes the size change conformal. + +The win: a small decal on a big model no longer quadruples the *whole* model's triangle count. + +**Run to completion, worst-first, against a triangle budget.** The refinement loop is not a fixed number +of sweeps: it holds every triangle that is over its criteria in a max-heap keyed by *how many times over* +it is, pops the worst, walks its LEPP, bisects, and re-scores. Edge adjacency (`nb[e]`, the triangle +across each edge) is built **once** and maintained incrementally through each bisection, so the cost +scales with the refined region rather than with the whole model. `max_triangles` is the only bound; +stopping on it leaves a perfectly valid, still-conformal mesh that spent its budget on the largest +errors. A fixed sweep count instead spends itself grading the *coarse surroundings* - whose edges are +the longest, so they win every terminal-edge contest - and never reaches the painted patch. + +**It carries the paint forward**, which is what makes it usable (uniform subdivide drops paint). Because +the refinement is *driven by* the paint, the remap is trivial: `subdivide_mesh_adaptive()` fills an +`out_source[new_tri] = input_tri` map (children inherit their parent), and the gizmo rebuilds each +layer's mask on the new mesh - a new triangle is painted iff its source was fully painted in that +layer. `collect_paint_region()` derives both: +- the union refine-region: **exactly** the original triangles the brush touched, read straight off + `TriangleSplittingData::triangles_to_split` (`serialize()` records an entry per original triangle that + is either split - i.e. partially painted, the patch boundary - or carries a non-default state). No + dilation: marking every triangle that shares a *vertex* with the patch drags in a whole fan of huge + unpainted neighbours and refines *those* down to the resolution floor, since the height field the + detail test samples is not restricted to the painted area. The conformal closure already grades the + size change outward on its own. +- the per-layer fully-painted-triangle sets (a `get_facets_strict(ENFORCER)` sub-triangle with all three + *original* vertex indices == a whole, fully-painted original triangle; a partial stroke's sub-triangles + always carry a split vertex). + +The other four channels ride the normal `restore_painting()` remap. + +Both modes share the gizmo's Preview/Apply/Done flow; the **"Only painted area (adaptive)"** checkbox +picks the mode, and the adaptive preview follows the paint live (`rebuild_preview()` refreshes the +wireframe while the subdivide preview is open in adaptive mode). The panel shows the previewed triangle +count. + +**Feature-adaptive (follow texture detail).** A sub-mode of adaptive (the **"Follow texture detail"** +checkbox) that puts triangles where the *displaced surface actually bends*, not evenly. A flat region or +a linear **ramp** needs no extra vertices (linear interpolation is exact for a ramp); what needs them is +**curvature** - the *second* derivative, not the gradient. So the extra predicate is a **chord-error** +test: sample the combined displacement at the triangle's three edge midpoints *and its centroid* +(sampling the interior is what catches a hill sitting inside a triangle, the blind spot of an edge-only +test) and take the largest departure from the flat triangle's barycentric interpolation. Refine while +that exceeds `chord_tolerance_mm` ("Detail (mm)"). Zero chord error on a ramp => untouched; high on a +hill/ridge/noise => refined until captured. Same conformal machinery, so still crack-free. The +per-triangle error is cached and recomputed only for the children of a split. + +Four knobs bracket it, and all four matter: +- **"Max edge (mm)"** (`target_edge_length_mm`) is a **baseline that applies in feature mode too**. + Without it the chord test aliases: a big triangle over a fine pattern can sample four points that all + land at similar heights, report no error, and stall before refinement ever starts. The baseline + guarantees a sampling density fine enough for the curvature test to see the texture at all. +- **"Detail (mm)"** is the chord tolerance above. +- **"Min edge (mm)"** is a hard floor under both, and is what guarantees termination across a sharp + texture *step*, where the error never falls however fine the mesh gets. +- **"Added triangles (k)"** is the budget, passed as `max_triangles` (the model's own triangle count plus + the slider, so the control still means something on an already-dense model). + +The height field is `make_combined_displacement_sampler()` - it mirrors `build_texture_displacement()`'s +per-layer setup (decode, patch centroid, cylinder axis, blend order, "lowest layer folds additively") +but evaluated per point. Two deliberate simplifications, both erring toward *more* detail (safe - +over-refinement is never a crack): every sampleable layer is sampled at every point (no per-point paint +test), and edge-smoothing falloff is ignored. The first is *why* the refine region must not be dilated - +outside the paint the sampler still reports full relief. **LSCM layers are skipped** (no per-point UV); a +purely LSCM stack yields a null sampler and the code falls back to the length baseline alone. Per-vertex +heights are sampled lazily, so a small patch on a huge model never pays for the rest of it. + +### Fast preview (GPU-only, no CPU meshing) + +`resources/shaders/{110,140}/texture_displacement_shaded.{vs,fs}`, registered as +`"texture_displacement_shaded"`. Shades the *displaced* surface without moving geometry - active-layer +only, selected from the View row, and the default when the gizmo opens (`m_use_shaded_preview = true`). +Vertex format is `GLModel::Geometry::EVertexLayout::P3N3T2`: `normal.x` carries the per-vertex paint +weight (0/1), `normal.y` flags the UV island currently being dragged, and `tex_coord` carries a +precomputed texture UV, so it can use `GLModel` normally instead of a hand-rolled VBO/VAO manager. + +The mesh is **flat** (vertices not shared between triangles): every corner of a painted triangle gets +weight 1, every corner of an unpainted one weight 0. A coarse mesh needs that - one painted face of a raw +cube has no strictly-interior vertex, so per-vertex weighting would either bleed onto the neighbours or +vanish outright. Duplicating vertices costs no shading quality here because the shader takes its surface +normal from screen-space derivatives of position, not from a per-vertex normal. + +**Both preview meshes work in the patch's vertex space, not the mesh's.** Those agree only until a +*brush* stroke splits a triangle: `get_facets_strict()` then appends the split vertices, so the patch +array is longer. `rebuild_shaded_preview_mesh()` and `rebuild_uvcheck_mesh()` therefore index +`patch.vertices` throughout. The weight buffer is rebuilt at the same cadence as the true-displacement +preview (stroke-end/slider-release) but from the **live** `TriangleSelector` state, not the flushed model +facets, so it does not lag by a full model round-trip. + +The perturbed normal is the analytic one for a height field `H = +/-depth_mm - h(uv)` displaced along +`N` over any orthonormal surface tangent pair `T`/`B`: + + N' = normalize(N - (dH/da)-T - (dH/db)-B), a = dot(p,T), b = dot(p,B) + +The two slopes have to be genuine **mm-per-mm** derivatives for the preview's apparent depth to match +the bake's. + +**Two projection paths (`use_vertex_uv` uniform):** +- **Triplanar (`use_vertex_uv = 0`)** - `uv` and the `T`/`B` axes are both derived in-shader from + the dominant normal component, mirroring `project_planar()`/`apply_uv_transform()`, and the slope is + formed analytically. `T`/`B` are the projection's axis-aligned pair, exact only when the face is + axis-aligned; the shader drops the along-normal component to keep the gradient in the surface. Here + one `uv` unit is exactly `tiling_scale` mm, so the `1/tiling_scale` gradient factor is right. +- **Precomputed UV (`use_vertex_uv = 1`, used for LSCM and ViewProjected)** - `uv` comes per-vertex from + the CPU (`compute_layer_vertex_uvs()`, so island placement + tiling/rotation/offset are already folded + in), and the perturbed normal is built with **Mikkelsen's method** ("Bump Mapping Unparametrized + Surfaces on the GPU"): the surface gradient taken directly from the screen-space derivatives of the + *sampled height* and position. **This makes no uv->mm scale assumption**, which is essential, because an + LSCM map is **conformal, not isometric**: it is globally area-scaled but the *local* mm-per-uv varies + across the chart, so a single global `1/tiling_scale` factor gets the apparent depth wrong. `dFdx(h)` + captures the true on-screen rate of change however the chart is stretched. This path is also what makes + the fast preview follow the UV editor: move an island and its uv - hence its shading - moves with it + (the mesh rebuilds on drag-end, `on_island_edited(finished)` -> `rebuild_preview()` -> + `rebuild_shaded_preview_mesh()`). The branch is uniform and the paint weight gates by multiply, so the + texture derivatives stay well defined. A triangle straddling a seam has a discontinuous uv -> the + `det~0` guard skips it (a localised preview-only artifact, never in the bake). + +**Parallax (triplanar path).** Perturbing the shading normal alone welds the pattern to the base surface: +it does not slide as the camera orbits, and does not get deeper as `depth_mm` grows. The triplanar path +therefore shades at the point the *displaced* surface would show at this pixel, found by **ray marching** +(parallax occlusion mapping). A point at ray parameter `s`, i.e. `P + V-s` (`P` the base point, `V` the +unit direction to the eye), sits at height `s-dot(V,n)` above the undisplaced surface. The displaced +surface lives in a shell between the extreme values of `amp-(h - midlevel)` - taken from both ends of +`h in [0,1]`, so it holds for an inverted layer and a raised midlevel too, where the surface sits *below* +the undisplaced one. The march starts at the top of that shell, where the ray is outside the surface by +construction, and steps inward until the ray height drops below the sampled height. That crossing *is* +the visible point. + +Solving `Q = P + V-(H(Q)/dot(V,n))` by fixed-point iteration instead is geometrically exact but the +divisor goes to zero edge-on; the sample then lands a large fraction of a tile away and the iteration +oscillates, which reads as a second, flat copy of the pattern ghosted over the real one. Clamping the +step to one tile does not help - a tile-sized shift lands on the neighbouring tile, the same pattern +again. Offset limiting (stepping along the tangential part of `V`) is stable but understates parallax +enough that the relief still flattens as soon as the camera tilts. Marching has neither problem. + +The hit is interpolated between the last two samples, which keeps `PARALLAX_STEPS` (24) affordable, and +the whole march is skipped when sweeping the shell would move the sample point less than half a texel - +the head-on case, so the common view pays almost nothing. The 140 variant samples with +`textureLod(..., 0.0)` inside the loop, since implicit derivatives are undefined in non-uniform control +flow. Two uniforms exist for this: `midlevel` (parallax needs the real height, not just its derivative) +and `eye_model_pos` (the camera in the volume's local frame). + +Parallax cannot change the model's silhouette or cast shadows; the View row's Normal mode is one click +away for that. The LSCM path stays plain Mikkelsen normal perturbation - it has no closed-form uv, so there is no cheap +way to re-project a marched position. One further approximation: the GPU sampler's wrap mode stands in +for `tile_enabled`/`tile_method`, so with tiling *off* the GPU repeats where the CPU returns 0 outside +`[0,1)`. + +### On-canvas "Adjust Texture" gizmo + +A per-active-layer toggle ("Adjust placement") that disables painting and shows a flat pan panel (free +2D drag on both axes) plus two arrows along the patch's own U/V axes (constrained single-axis drag). +Anchored to the painted patch's centroid/average-normal (`compute_layer_paint_anchor()`). Hit-testing is +screen-space distance/point-to-segment, not real 3D ray intersection against the handle geometry - simple +and good enough at this handle size. + +### Projection frame overlay (ViewProjected) + +`src/slic3r/GUI/TextureProjectorFrame.hpp/.cpp` - a semi-transparent, resizable `wxFrame` the user +drags **over the 3D view**, like a slide projector's gate. Whatever the model shows through it is what +the texture is projected onto, and the window's border becomes the hard edge of the displacement. +Press **Apply projection frame** and the gizmo reads the window's rectangle and commits it. + +The window is deliberately **dumb**: it owns no placement state and reports nothing continuously. Its +position and size *are* the placement, read on demand at Apply - which is also when the expensive +visible-facet raycast runs. So dragging it is free and nothing recomputes until asked. + +Plain 2D (`wxPaintDC`), not a `wxGLCanvas`: a second GL canvas would have to share the app's one real +`wxGLContext`. It only ever draws a bitmap and a border. + +**The projective mapping (`apply_projection_frame()`)**. The frame defines a **screen-space** rectangle, +but the bake samples from a **local-space** position, so the two have to be reconciled. +`view_project_right/up` can only express an *affine* projection - exact under an orthographic camera, but +wrong under perspective, where the near end of a part projects larger than the far end and no pair of +axes reproduces that. So the layer instead stores a full projective map (`view_project_matrix`, row-major +3x4, `uv = (row0-p/row2-p, row1-p/row2-p)`), built like this: + +- `K = projection - view - (instance - volume)`, i.e. local -> clip, the same product the renderer uses. + Note `Camera::get_projection_matrix()` is typed `Transform3d` (nominally affine) but its perspective + form explicitly writes a `(0, 0, -1, 0)` bottom row into the underlying 4x4, so `clip.w = -z_eye` is + genuinely carried. The build therefore multiplies **`.matrix()` products** (plain `Matrix4d`), never + `Transform3d` products, which would not compose that row correctly. +- Window coordinates follow `igl::project`'s convention (as `CameraUtils::project` does), with y + measured downward. Writing `uv = (win - rect_origin) / rect_size` makes u and v affine in + `ndc = clip.xyz / clip.w`; multiplying through by `clip.w` leaves a plain linear combination of `K`'s + rows, which is exactly the 3x4 matrix - the perspective divide survives intact. +- `w > 0` is checked rather than divided blindly. A point behind the projector has `w < 0` and divides + to a plausible-looking but **mirrored** uv - the classic way a projected decal reappears on the back + of a model. `project_uv_projective()` returns false there and the caller treats it as no height. + +The map already includes placement, so `apply_uv_transform()` is **not** applied on top of it - the +window's own position and size are the placement, and the tiling/rotation/offset sliders would shove +the result off the frame the user just aligned. A "Clear" button drops back to the affine path where +those controls mean something again. + +Apply also sets `tile_enabled = false`, so `DecodedHeightTexture::sample()` returns 0 outside `[0,1)` +and the border is a hard edge rather than the first seam of an endless repeat, and repaints the layer +via `select_visible_faces(&matrix)` - the frame's uv square clips the selection, which both matches the +paint to the border and keeps the ray queries proportional to the framed area instead of the model. + +Owned by the gizmo and **destroyed** (not just hidden) in `on_shutdown()`. Closing it only hides it, so +reopening keeps it where it was left. + +### UV Editor pane + +`UVEditorCanvas` (`src/slic3r/GUI/UVEditorCanvas.hpp/.cpp`) - a standalone `wxGLCanvas` rendering the +flattened LSCM islands (per-island wireframe + outline + fill) over the height texture (background +quad tiled across the whole unwrap), with mouse pan/zoom. It is wrapped in a **`UVEditorPanel`** +(same file) that adds a button row (Frame / Snap / Avg scale / Cut / Join / Unjoin) and a status line +along the bottom naming the current gesture and the shortcuts in play. The *panel* is what is +registered as a `wxAuiPaneInfo` pane on `Plater`'s `m_aui_mgr`; `Plater::show_uv_editor(bool)` +shows/hides it (deferred via `CallAfter`, since the gizmo calls it mid-3D-frame), and +`get_uv_editor_canvas()` returns the inner canvas the gizmo talks to. + +Deliberately **shares the app's one real `wxGLContext`** (`wxGetApp().init_glcontext(*this)`, the +same call `View3D`/`Preview`/`AssembleView` make) rather than creating an independent context like +`SkipPartCanvas` does elsewhere in this codebase - this is what lets it reuse the already-registered +`"flat"`/`"flat_texture"` shaders and `GLModel` as-is, instead of needing its own shader +compilation/VBO management. + +**Geometry is uploaded once, in the unwrap's own (raw, mm) coordinates**, one `GLModel` set per island; +each island is then drawn through its own 2x3 affine (`island_transform_matrix()` composed with the +layer's tiling/rotation/offset) passed as the `flat` shader's `view_model_matrix`. A drag updates one +matrix per island and touches no vertex buffer - `on_island_edited(!finished)` calls only +`set_island_transforms()`, and the full `set_islands()` rebuild happens solely when the unwrap itself +changes (`unwrap_changed` in `update_uv_editor()`). + +**Gestures** (canvas-owned, reported to the gizmo as incremental deltas via `IslandEditFn`): left-drag += move, right-drag or **R** = rotate (hold **Shift** to snap to 15° steps - quantised on the +*cumulative* rotation, not each delta, so it doesn't judder, and accumulated incrementally so it +survives crossing +/-180°), **S** = scale (R/S modal, click/Enter to confirm, Esc to cancel), wheel = +zoom about the cursor, middle-drag = pan, **Home**/**F** = frame all. Scale writes +`TextureIsland::scale`; "Avg scale" (`average_island_scales()`) sets every island to the mean, so +one island scaled by hand can be matched back to its neighbours' texel density. **Snap** (canvas-owned +`m_snap_enabled`, toggled from the toolbar) sticks a dragged island's nearest boundary vertex onto a +neighbouring island's at drag-*end* only - a magnet that re-applies mid-drag is very hard to pull out +of. Toolbar commands the canvas can't service itself (Avg scale, Cut, Join, Unjoin) are forwarded to the +gizmo via `CommandFn`; view-only ones (Frame, Snap) it handles directly. + +## File map + +**libslic3r (core, no GUI dependency):** +- `src/libslic3r/TextureDisplacement.hpp/.cpp` - data model, bake algorithm, projection methods, + tiling, subdivision (uniform + adaptive longest-edge bisection), post-process smoothing + (`smooth_mesh_vertices()`), and `TextureDisplacementOptions` (the whole-stack settings). See doc + comments throughout, they're kept accurate and up to date. +- `src/libslic3r/MeshBoolean.hpp/.cpp` - `parameterize_lscm()` and `remesh_isotropic()` in the `cgal` + sub-namespace, reusing the existing `CGALMesh`/`_EpicMesh`/conversion-helper infrastructure already + there for mesh boolean ops. CGAL includes: `Polygon_mesh_processing/border.h`, + `Polygon_mesh_processing/connected_components.h`, `Surface_mesh_parameterization/{Error_code, + LSCM_parameterizer_3, parameterize}.h`. No new dependency - CGAL 5.6.3 is already vendored and the + `Surface_mesh_parameterization` package headers were already present. +- `src/libslic3r/Model.hpp/.cpp` - the 8 named `FacetsAnnotation` fields + accessor, + `texture_displacement_layers`, `texture_displacement_options`, and all the mirrored touch points + (see Data model above). + +**GUI:** +- `src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp/.cpp` - the gizmo and its whole panel. +- `src/slic3r/GUI/TextureLibrary.hpp/.cpp` - scans the shipped + user texture folders, imports an + arbitrary image into the user folder (converting it to the 8-bit grayscale PNG libslic3r decodes), + and loads a library file's bytes for a layer. The image->grayscale-PNG conversion lives here, on the + GUI side, because libslic3r has no image toolkit; both the import path and the "pick a shipped + texture" path go through the same one function. +- `resources/textures/displacement/*.png` - the 10 shipped height maps (Bricks, Grid, Hexagons, + Knurl, Noise, Quilt, Studs, Waves, Weave, Wood Grain). All 512x512 8-bit grayscale and **seamless** + (each is periodic over the full image in both axes, so tiling shows no seam). Generated + procedurally; the whole `resources/` tree is installed recursively by CMake, so a new folder under + it ships with no build-system change. +- `src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.hpp/.cpp` - background bake commit. +- `src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.hpp/.cpp` - background preview compute + (mirrors the bake job's shape but commits nothing to the Model). +- `src/slic3r/GUI/TextureProjectorFrame.hpp/.cpp` - the semi-transparent projection-frame overlay for + ViewProjected layers (plain 2D `wxPaintDC`, no GL context - see its section above). +- `src/slic3r/GUI/UVEditorCanvas.hpp/.cpp` - the 2D UV unwrap viewer widget. +- `src/slic3r/GUI/Plater.hpp/.cpp` - `uv_editor_canvas` member, AUI pane registration, + `get_uv_editor_canvas()`/`show_uv_editor()`. +- `src/slic3r/GUI/GLShadersManager.cpp` - registers `"texture_displacement_shaded"`. +- `resources/shaders/{110,140}/texture_displacement_shaded.{vs,fs}` - the fast-preview shader. +- `src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp` - `PainterGizmoType::TEXTURE_DISPLACEMENT`. +- `src/slic3r/GUI/Gizmos/GLGizmosManager.hpp/.cpp` - `EType::TextureDisplacement` registration. + +## Tests + +`tests/libslic3r/test_texture_displacement.cpp`. Covers `decode_height_texture` round-trip, empty-layer +no-op, full-cube uniform displacement, a second layer over the same area contributing, all four blend +modes (table-driven), the lowest layer ignoring its blend mode, border displace/pin, post-process +smoothing and its mask guarantees, and adaptive subdivision: conformality (`every_edge_used_twice` on a +partially-refined cube - an exact crack detector for a closed mesh), the target edge length actually +being reached, the triangle budget capping the result without opening a crack, curvature-driven +refinement (a Gaussian hill refines at its centre, a linear ramp adds nothing), and the max-edge +baseline. + +`BUILD_TESTS` is `OFF` in the checked-in build cache; flip it on to run them: + + cmake -S . -B build -DBUILD_TESTS=ON + cmake --build build --config Release --target libslic3r_tests -- -m + ./build/tests/libslic3r/Release/libslic3r_tests.exe "[TextureDisplacement]" --order rand diff --git a/TEXTURE_DISPLACEMENT_GUIDE.md b/TEXTURE_DISPLACEMENT_GUIDE.md new file mode 100644 index 0000000000..5abc9b94e2 --- /dev/null +++ b/TEXTURE_DISPLACEMENT_GUIDE.md @@ -0,0 +1,326 @@ +# Texture Displacement - Feature & Controls Guide + +Texture Displacement is a paint-style gizmo that stamps height-map textures onto a model's surface +and turns them into real relief - engraved or embossed detail - either as a live preview or baked +into actual mesh geometry. You paint where the texture applies, stack multiple textures as blended +layers, choose how each is projected onto the surface, and (for the unwrap projection) lay the result +out by hand in a dedicated 2D **UV Editor** pane. + +This document describes every feature and control. For the internal architecture and algorithms, see +`TEXTURE_DISPLACEMENT.md`. + +--- + +## Table of contents + +1. [Quick start](#quick-start) +2. [Entering the tool](#entering-the-tool) +3. [Selection modes](#selection-modes) +4. [View modes](#view-modes) +5. [Auto update](#auto-update) +6. [Texture layers](#texture-layers) +7. [Per-layer settings](#per-layer-settings) +8. [Projection methods](#projection-methods) +9. [The UV Editor](#the-uv-editor) +10. [Seams](#seams) +11. [Adjust placement (on-model)](#adjust-placement-on-model) +12. [Preparing the mesh: Subdivide & Remesh](#preparing-the-mesh-subdivide--remesh) +13. [Baking & resetting](#baking--resetting) +14. [Controls reference](#controls-reference) +15. [Tips & limitations](#tips--limitations) + +--- + +## Quick start + +1. Select an object and open the **Texture displacement** gizmo from the left toolbar. +2. A texture layer is added automatically. Pick a texture from the layer's picker, or import your own. +3. **Paint** the area you want the texture to affect (or press **Select whole model**). +4. The relief appears live on the model. Tune **Depth**, **Tile size**, **Rotation**, etc. +5. If the model is low-poly, use **Subdivide** or **Remesh** so there are enough vertices for detail. +6. Press **Bake** to convert the preview into real geometry, or leave it as a live preview. + +> The tool only ever affects the **painted** area. Everything you don't paint keeps its original +> surface, and bake blends the relief seamlessly into it. + +--- + +## Entering the tool + +The gizmo lives on the left gizmo toolbar (icon: `toolbar_texture_displacement.svg`). Its settings +panel opens beside the toolbar. You can **Dock panel / Undock panel** (top of the panel) to pin it or +float it freely over the 3D view, and **Close** at the bottom exits the gizmo. + +When you first open the tool on a never-textured object it starts with **one texture layer already +added**, so you can paint straight away. + +--- + +## Selection modes + +Choose *how* you paint. All three write into the **active layer's** mask. + +| Mode | What it does | +|------|--------------| +| **Brush** | Free-hand painting with a round brush. Shows a **Brush size** slider and a **Circle / Sphere** choice (circle = surface disc, sphere = 3D ball that also paints around curves). | +| **Face** | Click a single triangle to paint it. | +| **Connected area** | Click to flood-fill a region; the **Angle threshold** slider limits how far the fill spreads across changes in surface angle. | + +- **Select whole model** - marks the entire model as painted for the active layer, instead of + brushing it by hand. + +--- + +## View modes + +A row of icon buttons labelled **View** controls how the painted area is shown. The first four are a +radio group; **Wireframe** is an independent toggle. Hover any icon for its tooltip. + +| View | Meaning | +|------|---------| +| **Normal** | The true displaced geometry - exactly what **Bake** produces. Rebuilt in the background. | +| **Fast** | A GPU shaded approximation of the *active layer only*. No real geometry movement - quick to update, not exact. Best while tuning or dragging islands. | +| **Checker** | A test grid painted over the unwrap so you can see stretching (squares stay square where the map isn't distorted). | +| **Distortion** | A blue->green->red heatmap of how much each area is compressed or stretched in UV space. Needs the **Unwrap (LSCM)** projection. | +| **Wireframe** | Overlays the mesh edges (white). Independent of the view above; in **Normal** view it sits on the displaced surface. | + +--- + +## Auto update + +**Auto update** (on by default) rebuilds the true displaced geometry as soon as *anything* changes - +painting, swapping textures, moving sliders. Turn it off on very heavy models to only rebuild when you +release a slider (painting still updates on stroke end). + +--- + +## Texture layers + +You can stack up to **8** texture layers. Each has its own independent paint mask, its own texture, +and its own parameters, and they combine in slot order like layers in an image editor. + +- **Add a layer** - the **+ icon** to the right of the *Texture layers* heading (reuses the tool icon + for now). +- **Remove** - the button on each layer's header row. +- **Active layer** - click a layer's header (or anywhere in its block) to make it active. The active + layer is the one you paint into and the one whose block is tinted. Only one layer is active at a time. +- **Erase all** - clears the active layer's paint. + +Each layer shows a texture **picker** (large preview + name). Open it to choose from the shipped +library or import your own image (any png/jpg/bmp; it's converted to an 8-bit grayscale height map and +copied into your user texture folder so app updates can't overwrite it). + +--- + +## Per-layer settings + +| Control | Range / options | What it does | +|---------|-----------------|--------------| +| **Depth (mm)** | 0.01-10 (log) | Maximum displacement along the surface normal. | +| **Tile size (mm)** | 0.2-200 (log) | Physical size of one texture tile on the surface. | +| **Rotation** | 0-360° | Rotates the texture on the surface. | +| **Midlevel** | 0-10 | The grey level that means "don't move". At 0 the texture only pushes outward; raise it and darker texels cut *inward* (one map both embosses and engraves). 0.5 makes mid-grey neutral. | +| **Smoothing** | 0-1 | Blurs the height texture before it displaces - rounds hard edges and removes speckle without needing a softer source image. | +| **Edge smoothing** | checkbox + **Edge amount** 0-1 | Fades the relief to flat toward the *edge of the painted area*, so it blends into the surrounding surface. A small amount softens only a thin band at the very edge; the maximum flattens the whole painted face. | +| **Invert** | checkbox | Flips the height map (peaks become valleys). | +| **Blend** | Add / Subtract / Multiply / Divide | How this layer combines with the layers **below** it where they overlap. Add/Subtract pile relief on or carve it away; Multiply/Divide scale the relief underneath (a mask). The lowest painted layer is the **Base** and always behaves additively. | +| **Tile** | checkbox + **Repeat / Mirrored repeat** | When off, the texture is placed once (a decal) instead of repeating. Mirrored repeat flips every other tile to hide seams. | +| **Projection** | see below | How the texture is mapped onto the painted surface. | + +> **Midlevel warning:** cutting inward can fold the surface through itself in sharp concave corners or +> thin walls. Keep Depth small relative to the feature you're cutting into; the panel warns when a deep +> inward setting is risky. + +--- + +## Projection methods + +How the 2D texture is wrapped onto the 3D painted area. + +| Method | Best for | Notes | +|--------|----------|-------| +| **Triplanar (blended)** | Patches wrapping around edges | Projects from all three axes at once and blends, so there's no seam across a sharp edge. | +| **Cylindrical** | Round, tube-like selections | Wraps the texture around the patch's own centre/axis. | +| **Spherical** | Ball-like selections | Longitude/latitude wrap around the patch centre. | +| **Unwrap (LSCM)** | Flat, controlled layout | A real conformal unwrap. Cuts the area into pieces at sharp edges (see **Seam angle**), flattens each, and lets you lay them out by hand in the **UV Editor**. Unlocks Checker/Distortion, seams, and island editing. | +| **From view** | Decals / slide-projector look | Projects straight onto the surface from the current camera direction. Use **Capture current view** to re-lay it from wherever you're looking. | + +### LSCM-only controls + +These appear when a layer uses **Unwrap (LSCM)**: + +- **Seam angle** (5-90°) - edges sharper than this are cut so each piece lies flat. Lower cuts more + (less stretching, more seams); raise to keep more in one piece. A box's 90° corners are cut by + default. *Ignored once you've marked any seam by hand* (your seams then define the pieces). +- **Connect islands** (on by default) - lays the unwrap out as a **connected net**: pieces that share + an edge are unfolded next to each other (a cube becomes a joined net instead of six loose squares). + They stay separate islands, so you can still move any of them by hand. Turn off for the classic + packed-grid layout. +- **Open UV editor** - shows the flattened unwrap in a side pane (see below). Opens *only* when you + turn this on - it never pops up on its own. +- **Mark seams** / **Path** / **Clear seams** - see [Seams](#seams). +- An **Unwrap: N islands, F faces, V verts** read-out tells you what the unwrap actually produced. + +--- + +## The UV Editor + +A dockable 2D pane (enable **Open UV editor** on an LSCM layer) showing the flattened unwrap over the +height texture. Islands are the flattened pieces; you can rearrange them freely - nothing re-packs them +behind your back. Moving an island updates the model **live** (in Fast view it tracks the cursor +smoothly, via a shader uniform - no rebuild until you release). + +### Navigation + +| Action | Control | +|--------|---------| +| Pan | Middle-drag | +| Zoom | Mouse wheel (zooms about the cursor) | +| Frame everything | **Home** or **F**, or the **Frame** toolbar button | + +### Editing an island + +| Action | Control | +|--------|---------| +| Select | Left-click an island | +| Move | Left-drag | +| Rotate | Right-drag, or press **R** then move the mouse (click/Enter to confirm, Esc to cancel) | +| Rotate snapped | Hold **Shift** while rotating - snaps to **global** 15° marks (0/15/30...). A protractor dial with tick marks and the current angle is shown. | +| Scale | Press **S** then move the mouse (click/Enter to confirm, Esc to cancel) | +| Undo / Redo | **Ctrl+Z** / **Ctrl+Shift+Z** or **Ctrl+Y** | + +The **selected** island gets a bold light-green outline and a brighter wireframe; unselected islands +are a translucent light-green wash. The texture underneath repeats exactly as it will when baked. +A **status line** along the bottom always names the current gesture and the shortcuts in play. + +### Toolbar + +| Button | Action | +|--------|--------| +| **Frame** | Frame all islands (same as Home). | +| **Snap** | Toggle magnetic snapping - a dragged island sticks its boundary to a neighbour's when they come close. | +| **Avg scale** | Give every island the same texel density (Blender's "Average Islands Scale"). | +| **Cut** | Split the selected island across its long axis (useful for very long islands). | +| **Join** | Unfold the selected island onto its nearest neighbour along their shared edge - keeps both as separate islands with their own borders. | +| **Unjoin** | Send the selected island back to its own packed position. | + +> **Checker / Distortion in the UV editor:** selecting those View modes also colours the UV pane - a +> checker background, or a per-island distortion heatmap - so you can judge stretch in 2D as well as +> on the model. + +--- + +## Seams + +Seams are edges the unwrap is forced to cut along, on top of whatever the Seam angle cuts - the +Blender "mark seam" workflow. They let you control exactly where the unwrap splits. + +Enable **Mark seams** on an active LSCM layer, then: + +- **Click an edge** on the model to mark it (it turns **red**); click a red edge again to unmark it. + The edge under the cursor is highlighted **yellow** so you can see what a click will toggle. +- **Path mode** (the **Path** checkbox) - for dense meshes where clicking each edge is tedious: click a + start point, then an end point, and the whole **shortest path** between them is seamed at once. It + chains (each click extends from the last point); the start vertex is shown in **green**. +- **Ctrl+drag** rotates/pans the camera while in seam mode. +- **Clear seams** removes them all. + +Once any seam is marked, the automatic Seam-angle cutting is disabled so *your* seams define the +islands - pieces you leave un-seamed merge together. + +--- + +## Adjust placement (on-model) + +**Adjust placement** (on an active layer) lets you position the texture by dragging a handle on the +model instead of nudging the Rotation/offset numbers. The handle is a flat panel in the patch's +tangent plane (drag anywhere on it to move freely) plus U/V arrows for single-axis nudges. It's +anchored to the painted patch, so paint something first. + +--- + +## Preparing the mesh: Subdivide & Remesh + +Displacement can only move vertices that exist, so a coarse model needs more of them first. + +### Subdivide + +Splits every triangle into four, **1-5 times** (each step roughly quadruples the triangle count). + +- **Subdivide steps** (1-5) - how many times to split. +- **Preview subdivision** - shows the result as a **cyan wireframe** without changing the model. +- **Apply** - commits the subdivision to the geometry. +- **Done** - ends the preview and leaves the model as it is. + +### Remesh + +Rebuilds the whole model with triangles close to a target edge length - evens out a mesh with wildly +varying triangle sizes (CGAL isotropic remeshing). + +- **Target edge (mm)** - desired triangle edge length (seeded to the model's current average). +- **Remesh** - splits the big triangles and merges the small ones to that size. + +> Both Subdivide-Apply and Remesh **replace the geometry** and clear any *not-yet-baked* paint on it +> (already-baked relief is kept). If you had the mesh **Wireframe** on before, it stays on afterward. + +--- + +## Baking & resetting + +- **Bake** - converts the current preview into real, permanent mesh geometry, restricted to the + painted area. Runs in the background; the button shows *Baking...* while it works. +- **Erase all** - clears the active layer's paint. + +Baking is the exact same algorithm as the **Normal** preview, so what you see is what you get. + +--- + +## Controls reference + +### Mouse - 3D view (while painting) + +| Input | Action | +|-------|--------| +| Left-drag | Paint the active layer | +| Ctrl + drag | Rotate / pan the camera (works in seam mode too) | +| Wheel | Zoom | + +### Mouse & keys - UV Editor + +| Input | Action | +|-------|--------| +| Left-click | Select island | +| Left-drag | Move island | +| Right-drag | Rotate island | +| **R** / **S** | Modal rotate / scale (mouse drives it, click or Enter confirms, Esc cancels) | +| **Shift** (while rotating) | Snap to global 15° marks | +| Middle-drag | Pan | +| Wheel | Zoom about cursor | +| **Home** / **F** | Frame all islands | +| **Ctrl+Z** / **Ctrl+Shift+Z** / **Ctrl+Y** | Undo / redo | + +### Seam mode + +| Input | Action | +|-------|--------| +| Click edge | Mark / unmark a seam (yellow = hover, red = marked) | +| Click (Path mode) | Set start, then seam the shortest path to the next click | +| Ctrl + drag | Rotate / pan camera | + +--- + +## Tips & limitations + +- **Paint first, then bake.** The preview is free to explore; only Bake changes the real mesh. +- **Not enough detail?** Subdivide or Remesh before painting fine textures. +- **Inward cuts** (high Midlevel + big Depth) can self-intersect on thin walls or sharp concave + corners - keep Depth modest there. +- **Fast vs Normal:** Fast preview only shades the relief and shows only the active layer; use it for quick + tuning and smooth UV dragging, but trust **Normal**/**Bake** for the exact result. +- **Topology changes drop unbaked paint.** Subdivide-Apply, Remesh, and Simplify replace the mesh, and + texture-displacement paint isn't remapped across that change (already-baked relief is unaffected). +- **Island placements** are tied to the current unwrap. Re-painting or changing the Seam angle can + re-segment the charts and renumber them, so a re-unwrap re-lays the connected net and discards + hand placements made before it. +- **Connect islands** is on by default; turn it off (per layer) for the classic packed-grid layout, or + if an unfold looks wrong on an unusual mesh. diff --git a/deps/OCCT/OCCT.cmake b/deps/OCCT/OCCT.cmake index 45263d6ae3..e1a65beef7 100644 --- a/deps/OCCT/OCCT.cmake +++ b/deps/OCCT/OCCT.cmake @@ -24,7 +24,7 @@ endif() # 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. +# not a number to quote. See docs/HLSD/design-tab.md. if (IN_GIT_REPO) set(OCCT_DIRECTORY_FLAG --directory ${BINARY_DIR_REL}/dep_OCCT-prefix/src/dep_OCCT) diff --git a/deps/python3/python3.cmake b/deps/python3/python3.cmake index 3e063fac4e..6b4443ef3a 100644 --- a/deps/python3/python3.cmake +++ b/deps/python3/python3.cmake @@ -151,6 +151,12 @@ elseif(APPLE) # the post-install -add_rpath below. set(_python_ldflags "${_python_arch_flags} -Wl,-headerpad_max_install_names") + # The macOS 27 SDK declares pipe2() and dup3() as available from macOS 27, so + # configure finds them and CPython 3.12 calls them without a runtime check. + # Below a macOS 27 deployment target they are weak-linked and resolve to NULL + # on older systems, where os.pipe() then segfaults -- in `make install` + # (compileall, ensurepip) and in the shipped app alike. Every configure below + # keeps the pipe()/dup2() fallbacks (python/cpython#153711). if(IS_CROSS_COMPILE) set(_python_build_tgt --build=${_python_build_arch}-apple-darwin --host=${_python_host_arch}-apple-darwin) set(_python_build_arch_flags "-arch ${_python_build_arch_flag} -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") @@ -174,7 +180,8 @@ elseif(APPLE) --enable-shared \ --without-static-libpython \ --disable-test-modules \ - --build=${_python_build_arch}-apple-darwin && \ + --build=${_python_build_arch}-apple-darwin \ + ac_cv_func_pipe2=no ac_cv_func_dup3=no && \ make -j${NPROC} python && \ cd '' && \ env \ @@ -191,6 +198,7 @@ elseif(APPLE) --without-static-libpython \ --with-openssl='${DESTDIR}' \ --disable-test-modules \ + ac_cv_func_pipe2=no ac_cv_func_dup3=no \ ${_python_build_tgt} \ --with-build-python='${_python_build_python}' \ py_cv_module__tkinter=n/a" @@ -213,6 +221,8 @@ elseif(APPLE) --with-openssl=${DESTDIR} --disable-test-modules ${_python_build_tgt} + ac_cv_func_pipe2=no + ac_cv_func_dup3=no # Tcl/Tk 9.0 (e.g. from Homebrew) is incompatible with CPython 3.12's # _tkinter; OrcaSlicer's embedded Python does not need tkinter anyway. py_cv_module__tkinter=n/a diff --git a/docs/CAD/GAP_ANALYSIS_vs_ONSHAPE.md b/docs/CAD/GAP_ANALYSIS_vs_ONSHAPE.md deleted file mode 100644 index bf603cf2c1..0000000000 --- a/docs/CAD/GAP_ANALYSIS_vs_ONSHAPE.md +++ /dev/null @@ -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. diff --git a/docs/CAD/cad_dependency_weight.md b/docs/CAD/cad_dependency_weight.md deleted file mode 100644 index 0b64ac8097..0000000000 --- a/docs/CAD/cad_dependency_weight.md +++ /dev/null @@ -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//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= -cat $R/adm/MODULES # module -> toolkits -cat $R/src//EXTERNLIB # toolkit -> its dependencies - -# archive sizes -ls -l /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 -``` diff --git a/docs/CAD/cad_ux_guidelines.md b/docs/CAD/cad_ux_guidelines.md deleted file mode 100644 index d0290a1d3d..0000000000 --- a/docs/CAD/cad_ux_guidelines.md +++ /dev/null @@ -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. diff --git a/docs/CAD/design/mate-connectors/BEAR_CONNECTOR_REVIEW.md b/docs/CAD/design/mate-connectors/BEAR_CONNECTOR_REVIEW.md deleted file mode 100644 index cc2d4b22aa..0000000000 --- a/docs/CAD/design/mate-connectors/BEAR_CONNECTOR_REVIEW.md +++ /dev/null @@ -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. diff --git a/docs/CAD/design/mate-connectors/BearConnector_Cutter.step b/docs/CAD/design/mate-connectors/BearConnector_Cutter.step deleted file mode 100644 index c78e19e536..0000000000 --- a/docs/CAD/design/mate-connectors/BearConnector_Cutter.step +++ /dev/null @@ -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; diff --git a/docs/CAD/design/mate-connectors/BearConnector_Female.step b/docs/CAD/design/mate-connectors/BearConnector_Female.step deleted file mode 100644 index 18e31c4b47..0000000000 --- a/docs/CAD/design/mate-connectors/BearConnector_Female.step +++ /dev/null @@ -1,1173 +0,0 @@ -ISO-10303-21; -HEADER; -FILE_DESCRIPTION(('FreeCAD Model'),'2;1'); -FILE_NAME('Open CASCADE Shape Model','2026-08-05T10:54: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),#1118); -#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,#57,#222,#246,#270,#287,#299,#330,#354,#378, - #402,#426,#451,#475,#500,#524,#548,#573,#597,#622,#646,#670,#687, - #817,#848,#872,#896,#920,#944,#961,#986,#1017,#1033,#1050,#1061, - #1086,#1100,#1109)); -#17 = ADVANCED_FACE('',(#18),#52,.F.); -#18 = FACE_BOUND('',#19,.F.); -#19 = EDGE_LOOP('',(#20,#30,#38,#46)); -#20 = ORIENTED_EDGE('',*,*,#21,.F.); -#21 = EDGE_CURVE('',#22,#24,#26,.T.); -#22 = VERTEX_POINT('',#23); -#23 = CARTESIAN_POINT('',(-45.46495478095,0.,-70.97315781796)); -#24 = VERTEX_POINT('',#25); -#25 = CARTESIAN_POINT('',(-45.46495478095,0.,4.2)); -#26 = LINE('',#27,#28); -#27 = CARTESIAN_POINT('',(-45.46495478095,0.,-70.97315781796)); -#28 = VECTOR('',#29,1.); -#29 = DIRECTION('',(0.,0.,1.)); -#30 = ORIENTED_EDGE('',*,*,#31,.T.); -#31 = EDGE_CURVE('',#22,#32,#34,.T.); -#32 = VERTEX_POINT('',#33); -#33 = CARTESIAN_POINT('',(-45.46495478095,20.8572529153,-70.97315781796) - ); -#34 = LINE('',#35,#36); -#35 = CARTESIAN_POINT('',(-45.46495478095,0.,-70.97315781796)); -#36 = VECTOR('',#37,1.); -#37 = DIRECTION('',(0.,1.,0.)); -#38 = ORIENTED_EDGE('',*,*,#39,.T.); -#39 = EDGE_CURVE('',#32,#40,#42,.T.); -#40 = VERTEX_POINT('',#41); -#41 = CARTESIAN_POINT('',(-45.46495478095,20.8572529153,4.2)); -#42 = LINE('',#43,#44); -#43 = CARTESIAN_POINT('',(-45.46495478095,20.8572529153,-70.97315781796) - ); -#44 = VECTOR('',#45,1.); -#45 = DIRECTION('',(0.,0.,1.)); -#46 = ORIENTED_EDGE('',*,*,#47,.F.); -#47 = EDGE_CURVE('',#24,#40,#48,.T.); -#48 = LINE('',#49,#50); -#49 = CARTESIAN_POINT('',(-45.46495478095,0.,4.2)); -#50 = VECTOR('',#51,1.); -#51 = DIRECTION('',(0.,1.,0.)); -#52 = PLANE('',#53); -#53 = AXIS2_PLACEMENT_3D('',#54,#55,#56); -#54 = CARTESIAN_POINT('',(-45.46495478095,0.,-70.97315781796)); -#55 = DIRECTION('',(1.,0.,0.)); -#56 = DIRECTION('',(0.,0.,1.)); -#57 = ADVANCED_FACE('',(#58,#83),#217,.F.); -#58 = FACE_BOUND('',#59,.F.); -#59 = EDGE_LOOP('',(#60,#68,#69,#77)); -#60 = ORIENTED_EDGE('',*,*,#61,.F.); -#61 = EDGE_CURVE('',#22,#62,#64,.T.); -#62 = VERTEX_POINT('',#63); -#63 = CARTESIAN_POINT('',(46.298608189024,0.,-70.97315781796)); -#64 = LINE('',#65,#66); -#65 = CARTESIAN_POINT('',(-45.46495478095,0.,-70.97315781796)); -#66 = VECTOR('',#67,1.); -#67 = DIRECTION('',(1.,0.,0.)); -#68 = ORIENTED_EDGE('',*,*,#21,.T.); -#69 = ORIENTED_EDGE('',*,*,#70,.T.); -#70 = EDGE_CURVE('',#24,#71,#73,.T.); -#71 = VERTEX_POINT('',#72); -#72 = CARTESIAN_POINT('',(46.298608189024,0.,4.2)); -#73 = LINE('',#74,#75); -#74 = CARTESIAN_POINT('',(-45.46495478095,0.,4.2)); -#75 = VECTOR('',#76,1.); -#76 = DIRECTION('',(1.,0.,0.)); -#77 = ORIENTED_EDGE('',*,*,#78,.F.); -#78 = EDGE_CURVE('',#62,#71,#79,.T.); -#79 = LINE('',#80,#81); -#80 = CARTESIAN_POINT('',(46.298608189024,0.,-70.97315781796)); -#81 = VECTOR('',#82,1.); -#82 = DIRECTION('',(0.,0.,1.)); -#83 = FACE_BOUND('',#84,.F.); -#84 = EDGE_LOOP('',(#85,#95,#103,#111,#119,#128,#136,#145,#153,#161,#170 - ,#178,#187,#195,#203,#211)); -#85 = ORIENTED_EDGE('',*,*,#86,.T.); -#86 = EDGE_CURVE('',#87,#89,#91,.T.); -#87 = VERTEX_POINT('',#88); -#88 = CARTESIAN_POINT('',(32.703857168398,0.,-60.78483712899)); -#89 = VERTEX_POINT('',#90); -#90 = CARTESIAN_POINT('',(26.938753236523,0.,-55.20570756731)); -#91 = LINE('',#92,#93); -#92 = CARTESIAN_POINT('',(13.567306766147,0.,-42.26560567724)); -#93 = VECTOR('',#94,1.); -#94 = DIRECTION('',(-0.718602345805,0.,0.695421216677)); -#95 = ORIENTED_EDGE('',*,*,#96,.T.); -#96 = EDGE_CURVE('',#89,#97,#99,.T.); -#97 = VERTEX_POINT('',#98); -#98 = CARTESIAN_POINT('',(42.298608189024,0.,-7.015765476549)); -#99 = LINE('',#100,#101); -#100 = CARTESIAN_POINT('',(25.140315874087,-4.440892098501E-16, - -60.84811712245)); -#101 = VECTOR('',#102,1.); -#102 = DIRECTION('',(0.30368282823,0.,0.952773183837)); -#103 = ORIENTED_EDGE('',*,*,#104,.T.); -#104 = EDGE_CURVE('',#97,#105,#107,.T.); -#105 = VERTEX_POINT('',#106); -#106 = CARTESIAN_POINT('',(35.082842712475,0.,0.2)); -#107 = LINE('',#108,#109); -#108 = CARTESIAN_POINT('',(34.536239068456,0.,0.746603644019)); -#109 = VECTOR('',#110,1.); -#110 = DIRECTION('',(-0.707106781187,0.,0.707106781187)); -#111 = ORIENTED_EDGE('',*,*,#112,.F.); -#112 = EDGE_CURVE('',#113,#105,#115,.T.); -#113 = VERTEX_POINT('',#114); -#114 = CARTESIAN_POINT('',(-32.57517518159,0.,0.2)); -#115 = LINE('',#116,#117); -#116 = CARTESIAN_POINT('',(-30.87627118587,0.,0.2)); -#117 = VECTOR('',#118,1.); -#118 = DIRECTION('',(1.,0.,0.)); -#119 = ORIENTED_EDGE('',*,*,#120,.T.); -#120 = EDGE_CURVE('',#113,#121,#123,.T.); -#121 = VERTEX_POINT('',#122); -#122 = CARTESIAN_POINT('',(-36.85603142851,0.,-1.573188716044)); -#123 = CIRCLE('',#124,6.054044962965); -#124 = AXIS2_PLACEMENT_3D('',#125,#126,#127); -#125 = CARTESIAN_POINT('',(-32.57517518159,0.,-5.854044962965)); -#126 = DIRECTION('',(0.,-1.,0.)); -#127 = DIRECTION('',(-1.,0.,0.)); -#128 = ORIENTED_EDGE('',*,*,#129,.F.); -#129 = EDGE_CURVE('',#130,#121,#132,.T.); -#130 = VERTEX_POINT('',#131); -#131 = CARTESIAN_POINT('',(-39.69176606491,0.,-4.408923352436)); -#132 = LINE('',#133,#134); -#133 = CARTESIAN_POINT('',(-57.0671882012,0.,-21.78434548873)); -#134 = VECTOR('',#135,1.); -#135 = DIRECTION('',(0.707106781187,0.,0.707106781187)); -#136 = ORIENTED_EDGE('',*,*,#137,.T.); -#137 = EDGE_CURVE('',#130,#138,#140,.T.); -#138 = VERTEX_POINT('',#139); -#139 = CARTESIAN_POINT('',(-41.17904151244,0.,-10.52828909594)); -#140 = CIRCLE('',#141,6.054044962965); -#141 = AXIS2_PLACEMENT_3D('',#142,#143,#144); -#142 = CARTESIAN_POINT('',(-35.41090981799,0.,-8.689779599357)); -#143 = DIRECTION('',(0.,-1.,0.)); -#144 = DIRECTION('',(-1.,0.,0.)); -#145 = ORIENTED_EDGE('',*,*,#146,.F.); -#146 = EDGE_CURVE('',#147,#138,#149,.T.); -#147 = VERTEX_POINT('',#148); -#148 = CARTESIAN_POINT('',(-26.93875323652,0.,-55.20570756731)); -#149 = LINE('',#150,#151); -#150 = CARTESIAN_POINT('',(-29.06259699304,-4.440892098501E-16, - -48.54236940733)); -#151 = VECTOR('',#152,1.); -#152 = DIRECTION('',(-0.30368282823,0.,0.952773183837)); -#153 = ORIENTED_EDGE('',*,*,#154,.F.); -#154 = EDGE_CURVE('',#155,#147,#157,.T.); -#155 = VERTEX_POINT('',#156); -#156 = CARTESIAN_POINT('',(-28.96712497021,0.,-57.16864680227)); -#157 = LINE('',#158,#159); -#158 = CARTESIAN_POINT('',(-36.14667143517,0.,-64.11659091489)); -#159 = VECTOR('',#160,1.); -#160 = DIRECTION('',(0.718602345805,0.,0.695421216677)); -#161 = ORIENTED_EDGE('',*,*,#162,.T.); -#162 = EDGE_CURVE('',#155,#163,#165,.T.); -#163 = VERTEX_POINT('',#164); -#164 = CARTESIAN_POINT('',(-29.08766684168,0.,-64.52156932717)); -#165 = CIRCLE('',#166,5.2); -#166 = AXIS2_PLACEMENT_3D('',#167,#168,#169); -#167 = CARTESIAN_POINT('',(-25.35093464349,0.,-60.90537900045)); -#168 = DIRECTION('',(0.,-1.,0.)); -#169 = DIRECTION('',(-1.,0.,0.)); -#170 = ORIENTED_EDGE('',*,*,#171,.F.); -#171 = EDGE_CURVE('',#172,#163,#174,.T.); -#172 = VERTEX_POINT('',#173); -#173 = CARTESIAN_POINT('',(-27.86158468659,0.,-65.78852163128)); -#174 = LINE('',#175,#176); -#175 = CARTESIAN_POINT('',(-31.12923147938,0.,-62.41195129628)); -#176 = VECTOR('',#177,1.); -#177 = DIRECTION('',(-0.695421216677,0.,0.718602345805)); -#178 = ORIENTED_EDGE('',*,*,#179,.T.); -#179 = EDGE_CURVE('',#172,#180,#182,.T.); -#180 = VERTEX_POINT('',#181); -#181 = CARTESIAN_POINT('',(-25.53052705686,0.,-65.82673637491)); -#182 = CIRCLE('',#183,1.648528137424); -#183 = AXIS2_PLACEMENT_3D('',#184,#185,#186); -#184 = CARTESIAN_POINT('',(-26.67694849991,0.,-64.64210018823)); -#185 = DIRECTION('',(0.,-1.,0.)); -#186 = DIRECTION('',(-1.,0.,0.)); -#187 = ORIENTED_EDGE('',*,*,#188,.F.); -#188 = EDGE_CURVE('',#189,#180,#191,.T.); -#189 = VERTEX_POINT('',#190); -#190 = CARTESIAN_POINT('',(-20.53201900137,0.,-60.98947335481)); -#191 = LINE('',#192,#193); -#192 = CARTESIAN_POINT('',(-30.69923804215,0.,-70.82871181101)); -#193 = VECTOR('',#194,1.); -#194 = DIRECTION('',(-0.718602345805,0.,-0.695421216677)); -#195 = ORIENTED_EDGE('',*,*,#196,.T.); -#196 = EDGE_CURVE('',#189,#197,#199,.T.); -#197 = VERTEX_POINT('',#198); -#198 = CARTESIAN_POINT('',(20.532019001374,0.,-60.98947335481)); -#199 = LINE('',#200,#201); -#200 = CARTESIAN_POINT('',(-17.57924046663,0.,-60.98947335481)); -#201 = VECTOR('',#202,1.); -#202 = DIRECTION('',(1.,0.,0.)); -#203 = ORIENTED_EDGE('',*,*,#204,.T.); -#204 = EDGE_CURVE('',#197,#205,#207,.T.); -#205 = VERTEX_POINT('',#206); -#206 = CARTESIAN_POINT('',(26.715163243538,0.,-66.97315781796)); -#207 = LINE('',#208,#209); -#208 = CARTESIAN_POINT('',(7.481849370247,-4.440892098501E-16, - -48.36028435244)); -#209 = VECTOR('',#210,1.); -#210 = DIRECTION('',(0.718602345805,0.,-0.695421216677)); -#211 = ORIENTED_EDGE('',*,*,#212,.T.); -#212 = EDGE_CURVE('',#205,#87,#213,.T.); -#213 = LINE('',#214,#215); -#214 = CARTESIAN_POINT('',(9.75933652063,0.,-84.4941890519)); -#215 = VECTOR('',#216,1.); -#216 = DIRECTION('',(0.695421216677,0.,0.718602345805)); -#217 = PLANE('',#218); -#218 = AXIS2_PLACEMENT_3D('',#219,#220,#221); -#219 = CARTESIAN_POINT('',(-45.46495478095,0.,-70.97315781796)); -#220 = DIRECTION('',(0.,1.,0.)); -#221 = DIRECTION('',(0.,0.,1.)); -#222 = ADVANCED_FACE('',(#223),#241,.T.); -#223 = FACE_BOUND('',#224,.T.); -#224 = EDGE_LOOP('',(#225,#226,#227,#235)); -#225 = ORIENTED_EDGE('',*,*,#47,.F.); -#226 = ORIENTED_EDGE('',*,*,#70,.T.); -#227 = ORIENTED_EDGE('',*,*,#228,.T.); -#228 = EDGE_CURVE('',#71,#229,#231,.T.); -#229 = VERTEX_POINT('',#230); -#230 = CARTESIAN_POINT('',(46.298608189024,20.8572529153,4.2)); -#231 = LINE('',#232,#233); -#232 = CARTESIAN_POINT('',(46.298608189024,0.,4.2)); -#233 = VECTOR('',#234,1.); -#234 = DIRECTION('',(0.,1.,0.)); -#235 = ORIENTED_EDGE('',*,*,#236,.F.); -#236 = EDGE_CURVE('',#40,#229,#237,.T.); -#237 = LINE('',#238,#239); -#238 = CARTESIAN_POINT('',(-45.46495478095,20.8572529153,4.2)); -#239 = VECTOR('',#240,1.); -#240 = DIRECTION('',(1.,0.,0.)); -#241 = PLANE('',#242); -#242 = AXIS2_PLACEMENT_3D('',#243,#244,#245); -#243 = CARTESIAN_POINT('',(-45.46495478095,0.,4.2)); -#244 = DIRECTION('',(0.,0.,1.)); -#245 = DIRECTION('',(1.,0.,0.)); -#246 = ADVANCED_FACE('',(#247),#265,.T.); -#247 = FACE_BOUND('',#248,.T.); -#248 = EDGE_LOOP('',(#249,#257,#258,#259)); -#249 = ORIENTED_EDGE('',*,*,#250,.F.); -#250 = EDGE_CURVE('',#32,#251,#253,.T.); -#251 = VERTEX_POINT('',#252); -#252 = CARTESIAN_POINT('',(46.298608189024,20.8572529153,-70.97315781796 - )); -#253 = LINE('',#254,#255); -#254 = CARTESIAN_POINT('',(-45.46495478095,20.8572529153,-70.97315781796 - )); -#255 = VECTOR('',#256,1.); -#256 = DIRECTION('',(1.,0.,0.)); -#257 = ORIENTED_EDGE('',*,*,#39,.T.); -#258 = ORIENTED_EDGE('',*,*,#236,.T.); -#259 = ORIENTED_EDGE('',*,*,#260,.F.); -#260 = EDGE_CURVE('',#251,#229,#261,.T.); -#261 = LINE('',#262,#263); -#262 = CARTESIAN_POINT('',(46.298608189024,20.8572529153,-70.97315781796 - )); -#263 = VECTOR('',#264,1.); -#264 = DIRECTION('',(0.,0.,1.)); -#265 = PLANE('',#266); -#266 = AXIS2_PLACEMENT_3D('',#267,#268,#269); -#267 = CARTESIAN_POINT('',(-45.46495478095,20.8572529153,-70.97315781796 - )); -#268 = DIRECTION('',(0.,1.,0.)); -#269 = DIRECTION('',(0.,0.,1.)); -#270 = ADVANCED_FACE('',(#271),#282,.F.); -#271 = FACE_BOUND('',#272,.F.); -#272 = EDGE_LOOP('',(#273,#274,#275,#281)); -#273 = ORIENTED_EDGE('',*,*,#31,.F.); -#274 = ORIENTED_EDGE('',*,*,#61,.T.); -#275 = ORIENTED_EDGE('',*,*,#276,.T.); -#276 = EDGE_CURVE('',#62,#251,#277,.T.); -#277 = LINE('',#278,#279); -#278 = CARTESIAN_POINT('',(46.298608189024,0.,-70.97315781796)); -#279 = VECTOR('',#280,1.); -#280 = DIRECTION('',(0.,1.,0.)); -#281 = ORIENTED_EDGE('',*,*,#250,.F.); -#282 = PLANE('',#283); -#283 = AXIS2_PLACEMENT_3D('',#284,#285,#286); -#284 = CARTESIAN_POINT('',(-45.46495478095,0.,-70.97315781796)); -#285 = DIRECTION('',(0.,0.,1.)); -#286 = DIRECTION('',(1.,0.,0.)); -#287 = ADVANCED_FACE('',(#288),#294,.T.); -#288 = FACE_BOUND('',#289,.T.); -#289 = EDGE_LOOP('',(#290,#291,#292,#293)); -#290 = ORIENTED_EDGE('',*,*,#78,.F.); -#291 = ORIENTED_EDGE('',*,*,#276,.T.); -#292 = ORIENTED_EDGE('',*,*,#260,.T.); -#293 = ORIENTED_EDGE('',*,*,#228,.F.); -#294 = PLANE('',#295); -#295 = AXIS2_PLACEMENT_3D('',#296,#297,#298); -#296 = CARTESIAN_POINT('',(46.298608189024,0.,-70.97315781796)); -#297 = DIRECTION('',(1.,0.,0.)); -#298 = DIRECTION('',(0.,0.,1.)); -#299 = ADVANCED_FACE('',(#300),#325,.T.); -#300 = FACE_BOUND('',#301,.T.); -#301 = EDGE_LOOP('',(#302,#310,#311,#319)); -#302 = ORIENTED_EDGE('',*,*,#303,.F.); -#303 = EDGE_CURVE('',#87,#304,#306,.T.); -#304 = VERTEX_POINT('',#305); -#305 = CARTESIAN_POINT('',(32.703857168398,3.2,-60.78483712899)); -#306 = LINE('',#307,#308); -#307 = CARTESIAN_POINT('',(32.703857168398,3.,-60.78483712899)); -#308 = VECTOR('',#309,1.); -#309 = DIRECTION('',(0.,1.,0.)); -#310 = ORIENTED_EDGE('',*,*,#86,.T.); -#311 = ORIENTED_EDGE('',*,*,#312,.F.); -#312 = EDGE_CURVE('',#313,#89,#315,.T.); -#313 = VERTEX_POINT('',#314); -#314 = CARTESIAN_POINT('',(26.938753236523,3.2,-55.20570756731)); -#315 = LINE('',#316,#317); -#316 = CARTESIAN_POINT('',(26.938753236523,3.,-55.20570756731)); -#317 = VECTOR('',#318,1.); -#318 = DIRECTION('',(0.,-1.,0.)); -#319 = ORIENTED_EDGE('',*,*,#320,.F.); -#320 = EDGE_CURVE('',#304,#313,#321,.T.); -#321 = LINE('',#322,#323); -#322 = CARTESIAN_POINT('',(16.008242280412,3.2,-44.62779994931)); -#323 = VECTOR('',#324,1.); -#324 = DIRECTION('',(-0.718602345805,0.,0.695421216677)); -#325 = PLANE('',#326); -#326 = AXIS2_PLACEMENT_3D('',#327,#328,#329); -#327 = CARTESIAN_POINT('',(29.704873980143,3.,-57.88259703786)); -#328 = DIRECTION('',(-0.695421216677,0.,-0.718602345805)); -#329 = DIRECTION('',(-0.718602345805,0.,0.695421216677)); -#330 = ADVANCED_FACE('',(#331),#349,.T.); -#331 = FACE_BOUND('',#332,.T.); -#332 = EDGE_LOOP('',(#333,#341,#342,#343)); -#333 = ORIENTED_EDGE('',*,*,#334,.F.); -#334 = EDGE_CURVE('',#205,#335,#337,.T.); -#335 = VERTEX_POINT('',#336); -#336 = CARTESIAN_POINT('',(26.715163243538,3.2,-66.97315781796)); -#337 = LINE('',#338,#339); -#338 = CARTESIAN_POINT('',(26.715163243538,3.,-66.97315781796)); -#339 = VECTOR('',#340,1.); -#340 = DIRECTION('',(0.,1.,0.)); -#341 = ORIENTED_EDGE('',*,*,#212,.T.); -#342 = ORIENTED_EDGE('',*,*,#303,.T.); -#343 = ORIENTED_EDGE('',*,*,#344,.F.); -#344 = EDGE_CURVE('',#335,#304,#345,.T.); -#345 = LINE('',#346,#347); -#346 = CARTESIAN_POINT('',(30.25240665457,3.2,-63.31800414721)); -#347 = VECTOR('',#348,1.); -#348 = DIRECTION('',(0.695421216677,0.,0.718602345805)); -#349 = PLANE('',#350); -#350 = AXIS2_PLACEMENT_3D('',#351,#352,#353); -#351 = CARTESIAN_POINT('',(29.709510205968,3.,-63.87899747347)); -#352 = DIRECTION('',(-0.718602345805,0.,0.695421216677)); -#353 = DIRECTION('',(0.695421216677,0.,0.718602345805)); -#354 = ADVANCED_FACE('',(#355),#373,.T.); -#355 = FACE_BOUND('',#356,.T.); -#356 = EDGE_LOOP('',(#357,#365,#371,#372)); -#357 = ORIENTED_EDGE('',*,*,#358,.F.); -#358 = EDGE_CURVE('',#359,#335,#361,.T.); -#359 = VERTEX_POINT('',#360); -#360 = CARTESIAN_POINT('',(20.532019001374,3.2,-60.98947335481)); -#361 = LINE('',#362,#363); -#362 = CARTESIAN_POINT('',(9.922784884512,3.2,-50.72247862452)); -#363 = VECTOR('',#364,1.); -#364 = DIRECTION('',(0.718602345805,0.,-0.695421216677)); -#365 = ORIENTED_EDGE('',*,*,#366,.T.); -#366 = EDGE_CURVE('',#359,#197,#367,.T.); -#367 = LINE('',#368,#369); -#368 = CARTESIAN_POINT('',(20.532019001374,3.,-60.98947335481)); -#369 = VECTOR('',#370,1.); -#370 = DIRECTION('',(0.,-1.,0.)); -#371 = ORIENTED_EDGE('',*,*,#204,.T.); -#372 = ORIENTED_EDGE('',*,*,#334,.T.); -#373 = PLANE('',#374); -#374 = AXIS2_PLACEMENT_3D('',#375,#376,#377); -#375 = CARTESIAN_POINT('',(23.522653113203,3.,-63.8836336993)); -#376 = DIRECTION('',(0.695421216677,0.,0.718602345805)); -#377 = DIRECTION('',(0.718602345805,0.,-0.695421216677)); -#378 = ADVANCED_FACE('',(#379),#397,.T.); -#379 = FACE_BOUND('',#380,.T.); -#380 = EDGE_LOOP('',(#381,#389,#390,#391)); -#381 = ORIENTED_EDGE('',*,*,#382,.F.); -#382 = EDGE_CURVE('',#189,#383,#385,.T.); -#383 = VERTEX_POINT('',#384); -#384 = CARTESIAN_POINT('',(-20.53201900137,3.2,-60.98947335481)); -#385 = LINE('',#386,#387); -#386 = CARTESIAN_POINT('',(-20.53201900137,3.,-60.98947335481)); -#387 = VECTOR('',#388,1.); -#388 = DIRECTION('',(0.,1.,0.)); -#389 = ORIENTED_EDGE('',*,*,#196,.T.); -#390 = ORIENTED_EDGE('',*,*,#366,.F.); -#391 = ORIENTED_EDGE('',*,*,#392,.F.); -#392 = EDGE_CURVE('',#383,#359,#393,.T.); -#393 = LINE('',#394,#395); -#394 = CARTESIAN_POINT('',(5.354765181569,3.2,-60.98947335481)); -#395 = VECTOR('',#396,1.); -#396 = DIRECTION('',(1.,0.,0.)); -#397 = PLANE('',#398); -#398 = AXIS2_PLACEMENT_3D('',#399,#400,#401); -#399 = CARTESIAN_POINT('',(10.306473847682,3.,-60.98947335481)); -#400 = DIRECTION('',(0.,0.,1.)); -#401 = DIRECTION('',(0.,-1.,0.)); -#402 = ADVANCED_FACE('',(#403),#421,.F.); -#403 = FACE_BOUND('',#404,.F.); -#404 = EDGE_LOOP('',(#405,#413,#419,#420)); -#405 = ORIENTED_EDGE('',*,*,#406,.F.); -#406 = EDGE_CURVE('',#407,#180,#409,.T.); -#407 = VERTEX_POINT('',#408); -#408 = CARTESIAN_POINT('',(-25.53052705686,3.2,-65.82673637491)); -#409 = LINE('',#410,#411); -#410 = CARTESIAN_POINT('',(-25.53052705686,3.,-65.82673637491)); -#411 = VECTOR('',#412,1.); -#412 = DIRECTION('',(0.,-1.,0.)); -#413 = ORIENTED_EDGE('',*,*,#414,.F.); -#414 = EDGE_CURVE('',#383,#407,#415,.T.); -#415 = LINE('',#416,#417); -#416 = CARTESIAN_POINT('',(-9.454421870603,3.2,-50.26922436104)); -#417 = VECTOR('',#418,1.); -#418 = DIRECTION('',(-0.718602345805,0.,-0.695421216677)); -#419 = ORIENTED_EDGE('',*,*,#382,.F.); -#420 = ORIENTED_EDGE('',*,*,#188,.T.); -#421 = PLANE('',#422); -#422 = AXIS2_PLACEMENT_3D('',#423,#424,#425); -#423 = CARTESIAN_POINT('',(-23.00219525444,3.,-63.37996509944)); -#424 = DIRECTION('',(0.695421216677,0.,-0.718602345805)); -#425 = DIRECTION('',(-0.718602345805,0.,-0.695421216677)); -#426 = ADVANCED_FACE('',(#427),#446,.F.); -#427 = FACE_BOUND('',#428,.F.); -#428 = EDGE_LOOP('',(#429,#437,#444,#445)); -#429 = ORIENTED_EDGE('',*,*,#430,.F.); -#430 = EDGE_CURVE('',#431,#172,#433,.T.); -#431 = VERTEX_POINT('',#432); -#432 = CARTESIAN_POINT('',(-27.86158468659,3.2,-65.78852163128)); -#433 = LINE('',#434,#435); -#434 = CARTESIAN_POINT('',(-27.86158468659,3.,-65.78852163128)); -#435 = VECTOR('',#436,1.); -#436 = DIRECTION('',(0.,-1.,0.)); -#437 = ORIENTED_EDGE('',*,*,#438,.T.); -#438 = EDGE_CURVE('',#431,#407,#439,.T.); -#439 = CIRCLE('',#440,1.648528137424); -#440 = AXIS2_PLACEMENT_3D('',#441,#442,#443); -#441 = CARTESIAN_POINT('',(-26.67694849991,3.2,-64.64210018823)); -#442 = DIRECTION('',(0.,-1.,0.)); -#443 = DIRECTION('',(-1.,0.,0.)); -#444 = ORIENTED_EDGE('',*,*,#406,.T.); -#445 = ORIENTED_EDGE('',*,*,#179,.F.); -#446 = CYLINDRICAL_SURFACE('',#447,1.648528137424); -#447 = AXIS2_PLACEMENT_3D('',#448,#449,#450); -#448 = CARTESIAN_POINT('',(-26.67694849991,3.,-64.64210018823)); -#449 = DIRECTION('',(0.,-1.,0.)); -#450 = DIRECTION('',(-1.,0.,0.)); -#451 = ADVANCED_FACE('',(#452),#470,.F.); -#452 = FACE_BOUND('',#453,.F.); -#453 = EDGE_LOOP('',(#454,#462,#468,#469)); -#454 = ORIENTED_EDGE('',*,*,#455,.F.); -#455 = EDGE_CURVE('',#456,#163,#458,.T.); -#456 = VERTEX_POINT('',#457); -#457 = CARTESIAN_POINT('',(-29.08766684168,3.2,-64.52156932717)); -#458 = LINE('',#459,#460); -#459 = CARTESIAN_POINT('',(-29.08766684168,3.,-64.52156932717)); -#460 = VECTOR('',#461,1.); -#461 = DIRECTION('',(0.,-1.,0.)); -#462 = ORIENTED_EDGE('',*,*,#463,.F.); -#463 = EDGE_CURVE('',#431,#456,#464,.T.); -#464 = LINE('',#465,#466); -#465 = CARTESIAN_POINT('',(-29.44004200272,3.2,-64.15744811364)); -#466 = VECTOR('',#467,1.); -#467 = DIRECTION('',(-0.695421216677,0.,0.718602345805)); -#468 = ORIENTED_EDGE('',*,*,#430,.T.); -#469 = ORIENTED_EDGE('',*,*,#171,.T.); -#470 = PLANE('',#471); -#471 = AXIS2_PLACEMENT_3D('',#472,#473,#474); -#472 = CARTESIAN_POINT('',(-28.47462576413,3.,-65.15504547923)); -#473 = DIRECTION('',(-0.718602345805,0.,-0.695421216677)); -#474 = DIRECTION('',(-0.695421216677,0.,0.718602345805)); -#475 = ADVANCED_FACE('',(#476),#495,.F.); -#476 = FACE_BOUND('',#477,.F.); -#477 = EDGE_LOOP('',(#478,#486,#493,#494)); -#478 = ORIENTED_EDGE('',*,*,#479,.F.); -#479 = EDGE_CURVE('',#480,#155,#482,.T.); -#480 = VERTEX_POINT('',#481); -#481 = CARTESIAN_POINT('',(-28.96712497021,3.2,-57.16864680227)); -#482 = LINE('',#483,#484); -#483 = CARTESIAN_POINT('',(-28.96712497021,3.,-57.16864680227)); -#484 = VECTOR('',#485,1.); -#485 = DIRECTION('',(0.,-1.,0.)); -#486 = ORIENTED_EDGE('',*,*,#487,.T.); -#487 = EDGE_CURVE('',#480,#456,#488,.T.); -#488 = CIRCLE('',#489,5.2); -#489 = AXIS2_PLACEMENT_3D('',#490,#491,#492); -#490 = CARTESIAN_POINT('',(-25.35093464349,3.2,-60.90537900045)); -#491 = DIRECTION('',(0.,-1.,0.)); -#492 = DIRECTION('',(-1.,0.,0.)); -#493 = ORIENTED_EDGE('',*,*,#455,.T.); -#494 = ORIENTED_EDGE('',*,*,#162,.F.); -#495 = CYLINDRICAL_SURFACE('',#496,5.2); -#496 = AXIS2_PLACEMENT_3D('',#497,#498,#499); -#497 = CARTESIAN_POINT('',(-25.35093464349,3.,-60.90537900045)); -#498 = DIRECTION('',(0.,-1.,0.)); -#499 = DIRECTION('',(-1.,0.,0.)); -#500 = ADVANCED_FACE('',(#501),#519,.F.); -#501 = FACE_BOUND('',#502,.F.); -#502 = EDGE_LOOP('',(#503,#511,#517,#518)); -#503 = ORIENTED_EDGE('',*,*,#504,.F.); -#504 = EDGE_CURVE('',#505,#147,#507,.T.); -#505 = VERTEX_POINT('',#506); -#506 = CARTESIAN_POINT('',(-26.93875323652,3.2,-55.20570756731)); -#507 = LINE('',#508,#509); -#508 = CARTESIAN_POINT('',(-26.93875323652,3.,-55.20570756731)); -#509 = VECTOR('',#510,1.); -#510 = DIRECTION('',(0.,-1.,0.)); -#511 = ORIENTED_EDGE('',*,*,#512,.F.); -#512 = EDGE_CURVE('',#480,#505,#513,.T.); -#513 = LINE('',#514,#515); -#514 = CARTESIAN_POINT('',(-14.90185526362,3.2,-43.55710346492)); -#515 = VECTOR('',#516,1.); -#516 = DIRECTION('',(0.718602345805,0.,0.695421216677)); -#517 = ORIENTED_EDGE('',*,*,#479,.T.); -#518 = ORIENTED_EDGE('',*,*,#154,.T.); -#519 = PLANE('',#520); -#520 = AXIS2_PLACEMENT_3D('',#521,#522,#523); -#521 = CARTESIAN_POINT('',(-27.90836811563,3.,-56.14404399617)); -#522 = DIRECTION('',(-0.695421216677,0.,0.718602345805)); -#523 = DIRECTION('',(0.718602345805,0.,0.695421216677)); -#524 = ADVANCED_FACE('',(#525),#543,.F.); -#525 = FACE_BOUND('',#526,.F.); -#526 = EDGE_LOOP('',(#527,#535,#541,#542)); -#527 = ORIENTED_EDGE('',*,*,#528,.F.); -#528 = EDGE_CURVE('',#529,#138,#531,.T.); -#529 = VERTEX_POINT('',#530); -#530 = CARTESIAN_POINT('',(-41.17904151244,3.2,-10.52828909594)); -#531 = LINE('',#532,#533); -#532 = CARTESIAN_POINT('',(-41.17904151244,3.,-10.52828909594)); -#533 = VECTOR('',#534,1.); -#534 = DIRECTION('',(0.,-1.,0.)); -#535 = ORIENTED_EDGE('',*,*,#536,.F.); -#536 = EDGE_CURVE('',#505,#529,#537,.T.); -#537 = LINE('',#538,#539); -#538 = CARTESIAN_POINT('',(-32.39120436163,3.2,-38.09921113329)); -#539 = VECTOR('',#540,1.); -#540 = DIRECTION('',(-0.30368282823,0.,0.952773183837)); -#541 = ORIENTED_EDGE('',*,*,#504,.T.); -#542 = ORIENTED_EDGE('',*,*,#146,.T.); -#543 = PLANE('',#544); -#544 = AXIS2_PLACEMENT_3D('',#545,#546,#547); -#545 = CARTESIAN_POINT('',(-34.04006158346,3.,-32.92609366041)); -#546 = DIRECTION('',(-0.952773183837,0.,-0.30368282823)); -#547 = DIRECTION('',(-0.30368282823,0.,0.952773183837)); -#548 = ADVANCED_FACE('',(#549),#568,.F.); -#549 = FACE_BOUND('',#550,.F.); -#550 = EDGE_LOOP('',(#551,#559,#566,#567)); -#551 = ORIENTED_EDGE('',*,*,#552,.F.); -#552 = EDGE_CURVE('',#553,#130,#555,.T.); -#553 = VERTEX_POINT('',#554); -#554 = CARTESIAN_POINT('',(-39.69176606491,3.2,-4.408923352436)); -#555 = LINE('',#556,#557); -#556 = CARTESIAN_POINT('',(-39.69176606491,3.,-4.408923352436)); -#557 = VECTOR('',#558,1.); -#558 = DIRECTION('',(0.,-1.,0.)); -#559 = ORIENTED_EDGE('',*,*,#560,.T.); -#560 = EDGE_CURVE('',#553,#529,#561,.T.); -#561 = CIRCLE('',#562,6.054044962965); -#562 = AXIS2_PLACEMENT_3D('',#563,#564,#565); -#563 = CARTESIAN_POINT('',(-35.41090981799,3.2,-8.689779599357)); -#564 = DIRECTION('',(0.,-1.,0.)); -#565 = DIRECTION('',(-1.,0.,0.)); -#566 = ORIENTED_EDGE('',*,*,#528,.T.); -#567 = ORIENTED_EDGE('',*,*,#137,.F.); -#568 = CYLINDRICAL_SURFACE('',#569,6.054044962965); -#569 = AXIS2_PLACEMENT_3D('',#570,#571,#572); -#570 = CARTESIAN_POINT('',(-35.41090981799,3.,-8.689779599357)); -#571 = DIRECTION('',(0.,-1.,0.)); -#572 = DIRECTION('',(-1.,0.,0.)); -#573 = ADVANCED_FACE('',(#574),#592,.F.); -#574 = FACE_BOUND('',#575,.F.); -#575 = EDGE_LOOP('',(#576,#584,#590,#591)); -#576 = ORIENTED_EDGE('',*,*,#577,.F.); -#577 = EDGE_CURVE('',#578,#121,#580,.T.); -#578 = VERTEX_POINT('',#579); -#579 = CARTESIAN_POINT('',(-36.85603142851,3.2,-1.573188716044)); -#580 = LINE('',#581,#582); -#581 = CARTESIAN_POINT('',(-36.85603142851,3.,-1.573188716044)); -#582 = VECTOR('',#583,1.); -#583 = DIRECTION('',(0.,-1.,0.)); -#584 = ORIENTED_EDGE('',*,*,#585,.F.); -#585 = EDGE_CURVE('',#553,#578,#586,.T.); -#586 = LINE('',#587,#588); -#587 = CARTESIAN_POINT('',(-36.19319006079,3.2,-0.910347348321)); -#588 = VECTOR('',#589,1.); -#589 = DIRECTION('',(0.707106781187,0.,0.707106781187)); -#590 = ORIENTED_EDGE('',*,*,#552,.T.); -#591 = ORIENTED_EDGE('',*,*,#129,.T.); -#592 = PLANE('',#593); -#593 = AXIS2_PLACEMENT_3D('',#594,#595,#596); -#594 = CARTESIAN_POINT('',(-38.27389874671,3.,-2.99105603424)); -#595 = DIRECTION('',(-0.707106781187,0.,0.707106781187)); -#596 = DIRECTION('',(0.707106781187,0.,0.707106781187)); -#597 = ADVANCED_FACE('',(#598),#617,.F.); -#598 = FACE_BOUND('',#599,.F.); -#599 = EDGE_LOOP('',(#600,#608,#615,#616)); -#600 = ORIENTED_EDGE('',*,*,#601,.F.); -#601 = EDGE_CURVE('',#602,#113,#604,.T.); -#602 = VERTEX_POINT('',#603); -#603 = CARTESIAN_POINT('',(-32.57517518159,3.2,0.2)); -#604 = LINE('',#605,#606); -#605 = CARTESIAN_POINT('',(-32.57517518159,3.,0.2)); -#606 = VECTOR('',#607,1.); -#607 = DIRECTION('',(0.,-1.,0.)); -#608 = ORIENTED_EDGE('',*,*,#609,.T.); -#609 = EDGE_CURVE('',#602,#578,#610,.T.); -#610 = CIRCLE('',#611,6.054044962965); -#611 = AXIS2_PLACEMENT_3D('',#612,#613,#614); -#612 = CARTESIAN_POINT('',(-32.57517518159,3.2,-5.854044962965)); -#613 = DIRECTION('',(0.,-1.,0.)); -#614 = DIRECTION('',(-1.,0.,0.)); -#615 = ORIENTED_EDGE('',*,*,#577,.T.); -#616 = ORIENTED_EDGE('',*,*,#120,.F.); -#617 = CYLINDRICAL_SURFACE('',#618,6.054044962965); -#618 = AXIS2_PLACEMENT_3D('',#619,#620,#621); -#619 = CARTESIAN_POINT('',(-32.57517518159,3.,-5.854044962965)); -#620 = DIRECTION('',(0.,-1.,0.)); -#621 = DIRECTION('',(-1.,0.,0.)); -#622 = ADVANCED_FACE('',(#623),#641,.F.); -#623 = FACE_BOUND('',#624,.F.); -#624 = EDGE_LOOP('',(#625,#633,#639,#640)); -#625 = ORIENTED_EDGE('',*,*,#626,.F.); -#626 = EDGE_CURVE('',#627,#105,#629,.T.); -#627 = VERTEX_POINT('',#628); -#628 = CARTESIAN_POINT('',(35.082842712475,3.2,0.2)); -#629 = LINE('',#630,#631); -#630 = CARTESIAN_POINT('',(35.082842712475,3.,0.2)); -#631 = VECTOR('',#632,1.); -#632 = DIRECTION('',(0.,-1.,0.)); -#633 = ORIENTED_EDGE('',*,*,#634,.F.); -#634 = EDGE_CURVE('',#602,#627,#635,.T.); -#635 = LINE('',#636,#637); -#636 = CARTESIAN_POINT('',(-7.942265537672,3.2,0.2)); -#637 = VECTOR('',#638,1.); -#638 = DIRECTION('',(1.,0.,0.)); -#639 = ORIENTED_EDGE('',*,*,#601,.T.); -#640 = ORIENTED_EDGE('',*,*,#112,.T.); -#641 = PLANE('',#642); -#642 = AXIS2_PLACEMENT_3D('',#643,#644,#645); -#643 = CARTESIAN_POINT('',(-16.28758759079,3.,0.2)); -#644 = DIRECTION('',(0.,0.,1.)); -#645 = DIRECTION('',(0.,-1.,0.)); -#646 = ADVANCED_FACE('',(#647),#665,.T.); -#647 = FACE_BOUND('',#648,.T.); -#648 = EDGE_LOOP('',(#649,#657,#658,#659)); -#649 = ORIENTED_EDGE('',*,*,#650,.T.); -#650 = EDGE_CURVE('',#651,#97,#653,.T.); -#651 = VERTEX_POINT('',#652); -#652 = CARTESIAN_POINT('',(42.298608189024,3.2,-7.015765476549)); -#653 = LINE('',#654,#655); -#654 = CARTESIAN_POINT('',(42.298608189024,3.,-7.015765476549)); -#655 = VECTOR('',#656,1.); -#656 = DIRECTION('',(0.,-1.,0.)); -#657 = ORIENTED_EDGE('',*,*,#104,.T.); -#658 = ORIENTED_EDGE('',*,*,#626,.F.); -#659 = ORIENTED_EDGE('',*,*,#660,.F.); -#660 = EDGE_CURVE('',#651,#627,#661,.T.); -#661 = LINE('',#662,#663); -#662 = CARTESIAN_POINT('',(36.596246576251,3.2,-1.313403863776)); -#663 = VECTOR('',#664,1.); -#664 = DIRECTION('',(-0.707106781187,0.,0.707106781187)); -#665 = PLANE('',#666); -#666 = AXIS2_PLACEMENT_3D('',#667,#668,#669); -#667 = CARTESIAN_POINT('',(38.67695526217,3.,-3.394112549695)); -#668 = DIRECTION('',(-0.707106781187,0.,-0.707106781187)); -#669 = DIRECTION('',(-0.707106781187,0.,0.707106781187)); -#670 = ADVANCED_FACE('',(#671),#682,.T.); -#671 = FACE_BOUND('',#672,.T.); -#672 = EDGE_LOOP('',(#673,#679,#680,#681)); -#673 = ORIENTED_EDGE('',*,*,#674,.F.); -#674 = EDGE_CURVE('',#313,#651,#675,.T.); -#675 = LINE('',#676,#677); -#676 = CARTESIAN_POINT('',(32.699020781567,3.2,-37.13346923684)); -#677 = VECTOR('',#678,1.); -#678 = DIRECTION('',(0.30368282823,0.,0.952773183837)); -#679 = ORIENTED_EDGE('',*,*,#312,.T.); -#680 = ORIENTED_EDGE('',*,*,#96,.T.); -#681 = ORIENTED_EDGE('',*,*,#650,.F.); -#682 = PLANE('',#683); -#683 = AXIS2_PLACEMENT_3D('',#684,#685,#686); -#684 = CARTESIAN_POINT('',(34.581352051556,3.,-31.22785130119)); -#685 = DIRECTION('',(-0.952773183837,0.,0.30368282823)); -#686 = DIRECTION('',(0.30368282823,0.,0.952773183837)); -#687 = ADVANCED_FACE('',(#688,#738,#749,#767,#801),#812,.F.); -#688 = FACE_BOUND('',#689,.F.); -#689 = EDGE_LOOP('',(#690,#700,#708,#716,#724,#732)); -#690 = ORIENTED_EDGE('',*,*,#691,.F.); -#691 = EDGE_CURVE('',#692,#694,#696,.T.); -#692 = VERTEX_POINT('',#693); -#693 = CARTESIAN_POINT('',(16.626582997737,3.2,-8.940188245231)); -#694 = VERTEX_POINT('',#695); -#695 = CARTESIAN_POINT('',(4.383041634064E-04,3.2,-8.903751615529)); -#696 = LINE('',#697,#698); -#697 = CARTESIAN_POINT('',(4.347099726942,3.2,-8.913277437397)); -#698 = VECTOR('',#699,1.); -#699 = DIRECTION('',(-0.999997598615,0.,2.191520817069E-03)); -#700 = ORIENTED_EDGE('',*,*,#701,.F.); -#701 = EDGE_CURVE('',#702,#692,#704,.T.); -#702 = VERTEX_POINT('',#703); -#703 = CARTESIAN_POINT('',(19.029295926024,3.2,-17.63009960955)); -#704 = LINE('',#705,#706); -#705 = CARTESIAN_POINT('',(19.849519003668,3.2,-20.59660707692)); -#706 = VECTOR('',#707,1.); -#707 = DIRECTION('',(-0.26649542889,0.,0.963836182336)); -#708 = ORIENTED_EDGE('',*,*,#709,.F.); -#709 = EDGE_CURVE('',#710,#702,#712,.T.); -#710 = VERTEX_POINT('',#711); -#711 = CARTESIAN_POINT('',(22.059435554995,3.2,-3.734519760785)); -#712 = LINE('',#713,#714); -#713 = CARTESIAN_POINT('',(17.698510515043,3.2,-23.73280021221)); -#714 = VECTOR('',#715,1.); -#715 = DIRECTION('',(-0.213058124893,0.,-0.977039526026)); -#716 = ORIENTED_EDGE('',*,*,#717,.F.); -#717 = EDGE_CURVE('',#718,#710,#720,.T.); -#718 = VERTEX_POINT('',#719); -#719 = CARTESIAN_POINT('',(-21.72552223146,3.2,-3.734519760785)); -#720 = LINE('',#721,#722); -#721 = CARTESIAN_POINT('',(0.297084840953,3.2,-3.734519760785)); -#722 = VECTOR('',#723,1.); -#723 = DIRECTION('',(1.,0.,0.)); -#724 = ORIENTED_EDGE('',*,*,#725,.F.); -#725 = EDGE_CURVE('',#726,#718,#728,.T.); -#726 = VERTEX_POINT('',#727); -#727 = CARTESIAN_POINT('',(-21.72552223146,3.2,-8.903751135252)); -#728 = LINE('',#729,#730); -#729 = CARTESIAN_POINT('',(-21.72552223146,3.2,-19.83215600037)); -#730 = VECTOR('',#731,1.); -#731 = DIRECTION('',(0.,0.,1.)); -#732 = ORIENTED_EDGE('',*,*,#733,.F.); -#733 = EDGE_CURVE('',#694,#726,#734,.T.); -#734 = LINE('',#735,#736); -#735 = CARTESIAN_POINT('',(-5.279852300138,3.2,-8.903751135252)); -#736 = VECTOR('',#737,1.); -#737 = DIRECTION('',(-1.,0.,0.)); -#738 = FACE_BOUND('',#739,.F.); -#739 = EDGE_LOOP('',(#740)); -#740 = ORIENTED_EDGE('',*,*,#741,.F.); -#741 = EDGE_CURVE('',#742,#742,#744,.T.); -#742 = VERTEX_POINT('',#743); -#743 = CARTESIAN_POINT('',(21.163799345768,3.2,-48.00951684793)); -#744 = CIRCLE('',#745,4.735522705283); -#745 = AXIS2_PLACEMENT_3D('',#746,#747,#748); -#746 = CARTESIAN_POINT('',(16.428276640485,3.2,-48.00951684793)); -#747 = DIRECTION('',(-0.,1.,0.)); -#748 = DIRECTION('',(1.,0.,0.)); -#749 = FACE_BOUND('',#750,.F.); -#750 = EDGE_LOOP('',(#751,#752,#753,#754,#755,#756,#757,#758,#759,#760, - #761,#762,#763,#764,#765,#766)); -#751 = ORIENTED_EDGE('',*,*,#536,.T.); -#752 = ORIENTED_EDGE('',*,*,#560,.F.); -#753 = ORIENTED_EDGE('',*,*,#585,.T.); -#754 = ORIENTED_EDGE('',*,*,#609,.F.); -#755 = ORIENTED_EDGE('',*,*,#634,.T.); -#756 = ORIENTED_EDGE('',*,*,#660,.F.); -#757 = ORIENTED_EDGE('',*,*,#674,.F.); -#758 = ORIENTED_EDGE('',*,*,#320,.F.); -#759 = ORIENTED_EDGE('',*,*,#344,.F.); -#760 = ORIENTED_EDGE('',*,*,#358,.F.); -#761 = ORIENTED_EDGE('',*,*,#392,.F.); -#762 = ORIENTED_EDGE('',*,*,#414,.T.); -#763 = ORIENTED_EDGE('',*,*,#438,.F.); -#764 = ORIENTED_EDGE('',*,*,#463,.T.); -#765 = ORIENTED_EDGE('',*,*,#487,.F.); -#766 = ORIENTED_EDGE('',*,*,#512,.T.); -#767 = FACE_BOUND('',#768,.F.); -#768 = EDGE_LOOP('',(#769,#779,#787,#795)); -#769 = ORIENTED_EDGE('',*,*,#770,.F.); -#770 = EDGE_CURVE('',#771,#773,#775,.T.); -#771 = VERTEX_POINT('',#772); -#772 = CARTESIAN_POINT('',(5.809375885494,3.2,-13.06417917474)); -#773 = VERTEX_POINT('',#774); -#774 = CARTESIAN_POINT('',(2.688069798796,3.2,-49.64588621989)); -#775 = LINE('',#776,#777); -#776 = CARTESIAN_POINT('',(4.914157977861,3.2,-23.55613296875)); -#777 = VECTOR('',#778,1.); -#778 = DIRECTION('',(-8.501532861635E-02,0.,-0.996379643459)); -#779 = ORIENTED_EDGE('',*,*,#780,.F.); -#780 = EDGE_CURVE('',#781,#771,#783,.T.); -#781 = VERTEX_POINT('',#782); -#782 = CARTESIAN_POINT('',(-5.809375885494,3.2,-13.06417917474)); -#783 = LINE('',#784,#785); -#784 = CARTESIAN_POINT('',(1.615747408047,3.2,-13.06417917474)); -#785 = VECTOR('',#786,1.); -#786 = DIRECTION('',(1.,0.,-3.066574716487E-16)); -#787 = ORIENTED_EDGE('',*,*,#788,.F.); -#788 = EDGE_CURVE('',#789,#781,#791,.T.); -#789 = VERTEX_POINT('',#790); -#790 = CARTESIAN_POINT('',(-2.688069798796,3.2,-49.64588621989)); -#791 = LINE('',#792,#793); -#792 = CARTESIAN_POINT('',(-4.890802006217,3.2,-23.82986495424)); -#793 = VECTOR('',#794,1.); -#794 = DIRECTION('',(-8.501532861635E-02,0.,0.996379643459)); -#795 = ORIENTED_EDGE('',*,*,#796,.F.); -#796 = EDGE_CURVE('',#773,#789,#797,.T.); -#797 = LINE('',#798,#799); -#798 = CARTESIAN_POINT('',(1.615747408047,3.2,-49.64588621989)); -#799 = VECTOR('',#800,1.); -#800 = DIRECTION('',(-1.,0.,0.)); -#801 = FACE_BOUND('',#802,.F.); -#802 = EDGE_LOOP('',(#803)); -#803 = ORIENTED_EDGE('',*,*,#804,.F.); -#804 = EDGE_CURVE('',#805,#805,#807,.T.); -#805 = VERTEX_POINT('',#806); -#806 = CARTESIAN_POINT('',(-11.6927539352,3.2,-48.00951684793)); -#807 = CIRCLE('',#808,4.735522705283); -#808 = AXIS2_PLACEMENT_3D('',#809,#810,#811); -#809 = CARTESIAN_POINT('',(-16.42827664048,3.2,-48.00951684793)); -#810 = DIRECTION('',(-0.,1.,0.)); -#811 = DIRECTION('',(1.,0.,0.)); -#812 = PLANE('',#813); -#813 = AXIS2_PLACEMENT_3D('',#814,#815,#816); -#814 = CARTESIAN_POINT('',(0.403056515455,3.2,-33.34517655273)); -#815 = DIRECTION('',(0.,1.,0.)); -#816 = DIRECTION('',(1.,0.,0.)); -#817 = ADVANCED_FACE('',(#818),#843,.T.); -#818 = FACE_BOUND('',#819,.T.); -#819 = EDGE_LOOP('',(#820,#828,#836,#842)); -#820 = ORIENTED_EDGE('',*,*,#821,.F.); -#821 = EDGE_CURVE('',#822,#692,#824,.T.); -#822 = VERTEX_POINT('',#823); -#823 = CARTESIAN_POINT('',(16.626582997737,0.,-8.940188245231)); -#824 = LINE('',#825,#826); -#825 = CARTESIAN_POINT('',(16.626582997737,-22.,-8.940188245231)); -#826 = VECTOR('',#827,1.); -#827 = DIRECTION('',(0.,1.,0.)); -#828 = ORIENTED_EDGE('',*,*,#829,.T.); -#829 = EDGE_CURVE('',#822,#830,#832,.T.); -#830 = VERTEX_POINT('',#831); -#831 = CARTESIAN_POINT('',(-5.329070518201E-15,0.,-8.903751135252)); -#832 = LINE('',#833,#834); -#833 = CARTESIAN_POINT('',(-18.54556462154,1.7763568394E-15, - -8.863107566442)); -#834 = VECTOR('',#835,1.); -#835 = DIRECTION('',(-0.999997598615,0.,2.191520817069E-03)); -#836 = ORIENTED_EDGE('',*,*,#837,.T.); -#837 = EDGE_CURVE('',#830,#694,#838,.T.); -#838 = LINE('',#839,#840); -#839 = CARTESIAN_POINT('',(-5.329070518201E-15,-22.,-8.903751135252)); -#840 = VECTOR('',#841,1.); -#841 = DIRECTION('',(0.,1.,0.)); -#842 = ORIENTED_EDGE('',*,*,#691,.F.); -#843 = PLANE('',#844); -#844 = AXIS2_PLACEMENT_3D('',#845,#846,#847); -#845 = CARTESIAN_POINT('',(8.237581109188,-22.,-8.921803528809)); -#846 = DIRECTION('',(-2.191520817069E-03,0.,-0.999997598615)); -#847 = DIRECTION('',(-0.999997598615,0.,2.191520817069E-03)); -#848 = ADVANCED_FACE('',(#849),#867,.T.); -#849 = FACE_BOUND('',#850,.T.); -#850 = EDGE_LOOP('',(#851,#859,#865,#866)); -#851 = ORIENTED_EDGE('',*,*,#852,.F.); -#852 = EDGE_CURVE('',#853,#702,#855,.T.); -#853 = VERTEX_POINT('',#854); -#854 = CARTESIAN_POINT('',(19.029295926024,0.,-17.63009960955)); -#855 = LINE('',#856,#857); -#856 = CARTESIAN_POINT('',(19.029295926024,-22.,-17.63009960955)); -#857 = VECTOR('',#858,1.); -#858 = DIRECTION('',(0.,1.,0.)); -#859 = ORIENTED_EDGE('',*,*,#860,.T.); -#860 = EDGE_CURVE('',#853,#822,#861,.T.); -#861 = LINE('',#862,#863); -#862 = CARTESIAN_POINT('',(23.053273013696,0.,-32.18365022821)); -#863 = VECTOR('',#864,1.); -#864 = DIRECTION('',(-0.26649542889,0.,0.963836182336)); -#865 = ORIENTED_EDGE('',*,*,#821,.T.); -#866 = ORIENTED_EDGE('',*,*,#701,.F.); -#867 = PLANE('',#868); -#868 = AXIS2_PLACEMENT_3D('',#869,#870,#871); -#869 = CARTESIAN_POINT('',(17.956031892536,-22.,-13.7484168618)); -#870 = DIRECTION('',(-0.963836182336,0.,-0.26649542889)); -#871 = DIRECTION('',(-0.26649542889,0.,0.963836182336)); -#872 = ADVANCED_FACE('',(#873),#891,.T.); -#873 = FACE_BOUND('',#874,.T.); -#874 = EDGE_LOOP('',(#875,#883,#884,#885)); -#875 = ORIENTED_EDGE('',*,*,#876,.T.); -#876 = EDGE_CURVE('',#877,#853,#879,.T.); -#877 = VERTEX_POINT('',#878); -#878 = CARTESIAN_POINT('',(22.059435554995,0.,-3.734519760785)); -#879 = LINE('',#880,#881); -#880 = CARTESIAN_POINT('',(12.741012548745,1.7763568394E-15, - -46.46683800444)); -#881 = VECTOR('',#882,1.); -#882 = DIRECTION('',(-0.213058124893,0.,-0.977039526026)); -#883 = ORIENTED_EDGE('',*,*,#852,.T.); -#884 = ORIENTED_EDGE('',*,*,#709,.F.); -#885 = ORIENTED_EDGE('',*,*,#886,.F.); -#886 = EDGE_CURVE('',#877,#710,#887,.T.); -#887 = LINE('',#888,#889); -#888 = CARTESIAN_POINT('',(22.059435554995,-22.,-3.734519760785)); -#889 = VECTOR('',#890,1.); -#890 = DIRECTION('',(0.,1.,0.)); -#891 = PLANE('',#892); -#892 = AXIS2_PLACEMENT_3D('',#893,#894,#895); -#893 = CARTESIAN_POINT('',(20.484588228021,-22.,-10.95643672209)); -#894 = DIRECTION('',(0.977039526026,0.,-0.213058124893)); -#895 = DIRECTION('',(-0.213058124893,0.,-0.977039526026)); -#896 = ADVANCED_FACE('',(#897),#915,.T.); -#897 = FACE_BOUND('',#898,.T.); -#898 = EDGE_LOOP('',(#899,#907,#908,#909)); -#899 = ORIENTED_EDGE('',*,*,#900,.T.); -#900 = EDGE_CURVE('',#901,#877,#903,.T.); -#901 = VERTEX_POINT('',#902); -#902 = CARTESIAN_POINT('',(-21.72552223146,0.,-3.734519760785)); -#903 = LINE('',#904,#905); -#904 = CARTESIAN_POINT('',(-22.63692080725,0.,-3.734519760785)); -#905 = VECTOR('',#906,1.); -#906 = DIRECTION('',(1.,0.,0.)); -#907 = ORIENTED_EDGE('',*,*,#886,.T.); -#908 = ORIENTED_EDGE('',*,*,#717,.F.); -#909 = ORIENTED_EDGE('',*,*,#910,.F.); -#910 = EDGE_CURVE('',#901,#718,#911,.T.); -#911 = LINE('',#912,#913); -#912 = CARTESIAN_POINT('',(-21.72552223146,-22.,-3.734519760785)); -#913 = VECTOR('',#914,1.); -#914 = DIRECTION('',(0.,1.,0.)); -#915 = PLANE('',#916); -#916 = AXIS2_PLACEMENT_3D('',#917,#918,#919); -#917 = CARTESIAN_POINT('',(0.19111316645,-22.,-3.734519760785)); -#918 = DIRECTION('',(0.,0.,1.)); -#919 = DIRECTION('',(0.,-1.,0.)); -#920 = ADVANCED_FACE('',(#921),#939,.T.); -#921 = FACE_BOUND('',#922,.T.); -#922 = EDGE_LOOP('',(#923,#931,#932,#933)); -#923 = ORIENTED_EDGE('',*,*,#924,.T.); -#924 = EDGE_CURVE('',#925,#901,#927,.T.); -#925 = VERTEX_POINT('',#926); -#926 = CARTESIAN_POINT('',(-21.72552223146,0.,-8.903751135252)); -#927 = LINE('',#928,#929); -#928 = CARTESIAN_POINT('',(-21.72552223146,0.,-38.64614663299)); -#929 = VECTOR('',#930,1.); -#930 = DIRECTION('',(0.,0.,1.)); -#931 = ORIENTED_EDGE('',*,*,#910,.T.); -#932 = ORIENTED_EDGE('',*,*,#725,.F.); -#933 = ORIENTED_EDGE('',*,*,#934,.F.); -#934 = EDGE_CURVE('',#925,#726,#935,.T.); -#935 = LINE('',#936,#937); -#936 = CARTESIAN_POINT('',(-21.72552223146,-22.,-8.903751135252)); -#937 = VECTOR('',#938,1.); -#938 = DIRECTION('',(0.,1.,0.)); -#939 = PLANE('',#940); -#940 = AXIS2_PLACEMENT_3D('',#941,#942,#943); -#941 = CARTESIAN_POINT('',(-21.72552223146,-22.,-6.319135448019)); -#942 = DIRECTION('',(-1.,0.,0.)); -#943 = DIRECTION('',(0.,1.,0.)); -#944 = ADVANCED_FACE('',(#945),#956,.T.); -#945 = FACE_BOUND('',#946,.T.); -#946 = EDGE_LOOP('',(#947,#953,#954,#955)); -#947 = ORIENTED_EDGE('',*,*,#948,.T.); -#948 = EDGE_CURVE('',#830,#925,#949,.T.); -#949 = LINE('',#950,#951); -#950 = CARTESIAN_POINT('',(-28.21385794834,0.,-8.903751135252)); -#951 = VECTOR('',#952,1.); -#952 = DIRECTION('',(-1.,0.,0.)); -#953 = ORIENTED_EDGE('',*,*,#934,.T.); -#954 = ORIENTED_EDGE('',*,*,#733,.F.); -#955 = ORIENTED_EDGE('',*,*,#837,.F.); -#956 = PLANE('',#957); -#957 = AXIS2_PLACEMENT_3D('',#958,#959,#960); -#958 = CARTESIAN_POINT('',(-10.96276111573,-22.,-8.903751135252)); -#959 = DIRECTION('',(0.,0.,-1.)); -#960 = DIRECTION('',(0.,1.,0.)); -#961 = ADVANCED_FACE('',(#962),#981,.T.); -#962 = FACE_BOUND('',#963,.T.); -#963 = EDGE_LOOP('',(#964,#973,#979,#980)); -#964 = ORIENTED_EDGE('',*,*,#965,.T.); -#965 = EDGE_CURVE('',#966,#966,#968,.T.); -#966 = VERTEX_POINT('',#967); -#967 = CARTESIAN_POINT('',(21.163799345768,0.,-48.00951684793)); -#968 = CIRCLE('',#969,4.735522705283); -#969 = AXIS2_PLACEMENT_3D('',#970,#971,#972); -#970 = CARTESIAN_POINT('',(16.428276640485,0.,-48.00951684793)); -#971 = DIRECTION('',(-0.,1.,0.)); -#972 = DIRECTION('',(1.,0.,0.)); -#973 = ORIENTED_EDGE('',*,*,#974,.T.); -#974 = EDGE_CURVE('',#966,#742,#975,.T.); -#975 = LINE('',#976,#977); -#976 = CARTESIAN_POINT('',(21.163799345768,-22.,-48.00951684793)); -#977 = VECTOR('',#978,1.); -#978 = DIRECTION('',(0.,1.,0.)); -#979 = ORIENTED_EDGE('',*,*,#741,.F.); -#980 = ORIENTED_EDGE('',*,*,#974,.F.); -#981 = CYLINDRICAL_SURFACE('',#982,4.735522705283); -#982 = AXIS2_PLACEMENT_3D('',#983,#984,#985); -#983 = CARTESIAN_POINT('',(16.428276640485,-22.,-48.00951684793)); -#984 = DIRECTION('',(0.,1.,0.)); -#985 = DIRECTION('',(1.,0.,0.)); -#986 = ADVANCED_FACE('',(#987),#1012,.F.); -#987 = FACE_BOUND('',#988,.F.); -#988 = EDGE_LOOP('',(#989,#999,#1005,#1006)); -#989 = ORIENTED_EDGE('',*,*,#990,.T.); -#990 = EDGE_CURVE('',#991,#993,#995,.T.); -#991 = VERTEX_POINT('',#992); -#992 = CARTESIAN_POINT('',(1.480297366167E-15,11.242400581089, - -46.71869179633)); -#993 = VERTEX_POINT('',#994); -#994 = CARTESIAN_POINT('',(3.256654205567E-15,17.8572529153, - -18.39898295202)); -#995 = LINE('',#996,#997); -#996 = CARTESIAN_POINT('',(2.6645352591E-15,18.133069549222, - -17.21814831317)); -#997 = VECTOR('',#998,1.); -#998 = DIRECTION('',(5.275122655166E-17,0.227455280238,0.97378852709)); -#999 = ORIENTED_EDGE('',*,*,#1000,.F.); -#1000 = EDGE_CURVE('',#771,#993,#1001,.T.); -#1001 = LINE('',#1002,#1003); -#1002 = CARTESIAN_POINT('',(5.649679875255,3.602918464526, - -13.21082950266)); -#1003 = VECTOR('',#1004,1.); -#1004 = DIRECTION('',(-0.349023821871,0.880598971639,-0.320511814002)); -#1005 = ORIENTED_EDGE('',*,*,#770,.T.); -#1006 = ORIENTED_EDGE('',*,*,#1007,.T.); -#1007 = EDGE_CURVE('',#773,#991,#1008,.T.); -#1008 = LINE('',#1009,#1010); -#1009 = CARTESIAN_POINT('',(1.103762571829,7.940067898719, - -47.92064259636)); -#1010 = VECTOR('',#1011,1.); -#1011 = DIRECTION('',(-0.299648208284,0.896513522642,0.326304236859)); -#1012 = PLANE('',#1013); -#1013 = AXIS2_PLACEMENT_3D('',#1014,#1015,#1016); -#1014 = CARTESIAN_POINT('',(5.823691883056,3.068404028665, - -13.45978839622)); -#1015 = DIRECTION('',(0.93629059846,0.342020143326,-7.988827695448E-02) - ); -#1016 = DIRECTION('',(-0.340781908463,0.939692620786,2.907695487824E-02) - ); -#1017 = ADVANCED_FACE('',(#1018),#1028,.F.); -#1018 = FACE_BOUND('',#1019,.F.); -#1019 = EDGE_LOOP('',(#1020,#1021,#1027)); -#1020 = ORIENTED_EDGE('',*,*,#1000,.T.); -#1021 = ORIENTED_EDGE('',*,*,#1022,.T.); -#1022 = EDGE_CURVE('',#993,#781,#1023,.T.); -#1023 = LINE('',#1024,#1025); -#1024 = CARTESIAN_POINT('',(-5.275833888477,4.546144602338, - -13.55413574101)); -#1025 = VECTOR('',#1026,1.); -#1026 = DIRECTION('',(-0.349023821871,-0.880598971639,0.320511814002)); -#1027 = ORIENTED_EDGE('',*,*,#780,.T.); -#1028 = PLANE('',#1029); -#1029 = AXIS2_PLACEMENT_3D('',#1030,#1031,#1032); -#1030 = CARTESIAN_POINT('',(2.828438300639,3.068404028665, - -13.01628215822)); -#1031 = DIRECTION('',(2.881637632171E-16,0.342020143326,0.939692620786) - ); -#1032 = DIRECTION('',(-1.048830324052E-16,0.939692620786,-0.342020143326 - )); -#1033 = ADVANCED_FACE('',(#1034),#1045,.F.); -#1034 = FACE_BOUND('',#1035,.F.); -#1035 = EDGE_LOOP('',(#1036,#1037,#1038,#1039)); -#1036 = ORIENTED_EDGE('',*,*,#788,.T.); -#1037 = ORIENTED_EDGE('',*,*,#1022,.F.); -#1038 = ORIENTED_EDGE('',*,*,#990,.F.); -#1039 = ORIENTED_EDGE('',*,*,#1040,.F.); -#1040 = EDGE_CURVE('',#789,#991,#1041,.T.); -#1041 = LINE('',#1042,#1043); -#1042 = CARTESIAN_POINT('',(-0.871390517001,8.635298785064, - -47.66759924779)); -#1043 = VECTOR('',#1044,1.); -#1044 = DIRECTION('',(0.299648208284,0.896513522642,0.326304236859)); -#1045 = PLANE('',#1046); -#1046 = AXIS2_PLACEMENT_3D('',#1047,#1048,#1049); -#1047 = CARTESIAN_POINT('',(-5.782806207227,3.068404028665, - -13.93896851312)); -#1048 = DIRECTION('',(-0.93629059846,0.342020143326,-7.988827695448E-02) - ); -#1049 = DIRECTION('',(0.340781908463,0.939692620786,2.907695487824E-02) - ); -#1050 = ADVANCED_FACE('',(#1051),#1056,.F.); -#1051 = FACE_BOUND('',#1052,.F.); -#1052 = EDGE_LOOP('',(#1053,#1054,#1055)); -#1053 = ORIENTED_EDGE('',*,*,#1040,.T.); -#1054 = ORIENTED_EDGE('',*,*,#1007,.F.); -#1055 = ORIENTED_EDGE('',*,*,#796,.T.); -#1056 = PLANE('',#1057); -#1057 = AXIS2_PLACEMENT_3D('',#1058,#1059,#1060); -#1058 = CARTESIAN_POINT('',(2.828438300639,3.068404028665, - -49.69378323641)); -#1059 = DIRECTION('',(0.,0.342020143326,-0.939692620786)); -#1060 = DIRECTION('',(0.,0.939692620786,0.342020143326)); -#1061 = ADVANCED_FACE('',(#1062),#1081,.T.); -#1062 = FACE_BOUND('',#1063,.T.); -#1063 = EDGE_LOOP('',(#1064,#1073,#1079,#1080)); -#1064 = ORIENTED_EDGE('',*,*,#1065,.T.); -#1065 = EDGE_CURVE('',#1066,#1066,#1068,.T.); -#1066 = VERTEX_POINT('',#1067); -#1067 = CARTESIAN_POINT('',(-11.6927539352,0.,-48.00951684793)); -#1068 = CIRCLE('',#1069,4.735522705283); -#1069 = AXIS2_PLACEMENT_3D('',#1070,#1071,#1072); -#1070 = CARTESIAN_POINT('',(-16.42827664048,0.,-48.00951684793)); -#1071 = DIRECTION('',(-0.,1.,0.)); -#1072 = DIRECTION('',(1.,0.,0.)); -#1073 = ORIENTED_EDGE('',*,*,#1074,.T.); -#1074 = EDGE_CURVE('',#1066,#805,#1075,.T.); -#1075 = LINE('',#1076,#1077); -#1076 = CARTESIAN_POINT('',(-11.6927539352,-22.,-48.00951684793)); -#1077 = VECTOR('',#1078,1.); -#1078 = DIRECTION('',(0.,1.,0.)); -#1079 = ORIENTED_EDGE('',*,*,#804,.F.); -#1080 = ORIENTED_EDGE('',*,*,#1074,.F.); -#1081 = CYLINDRICAL_SURFACE('',#1082,4.735522705283); -#1082 = AXIS2_PLACEMENT_3D('',#1083,#1084,#1085); -#1083 = CARTESIAN_POINT('',(-16.42827664048,-22.,-48.00951684793)); -#1084 = DIRECTION('',(0.,1.,0.)); -#1085 = DIRECTION('',(1.,0.,0.)); -#1086 = ADVANCED_FACE('',(#1087),#1095,.F.); -#1087 = FACE_BOUND('',#1088,.F.); -#1088 = EDGE_LOOP('',(#1089,#1090,#1091,#1092,#1093,#1094)); -#1089 = ORIENTED_EDGE('',*,*,#860,.T.); -#1090 = ORIENTED_EDGE('',*,*,#829,.T.); -#1091 = ORIENTED_EDGE('',*,*,#948,.T.); -#1092 = ORIENTED_EDGE('',*,*,#924,.T.); -#1093 = ORIENTED_EDGE('',*,*,#900,.T.); -#1094 = ORIENTED_EDGE('',*,*,#876,.T.); -#1095 = PLANE('',#1096); -#1096 = AXIS2_PLACEMENT_3D('',#1097,#1098,#1099); -#1097 = CARTESIAN_POINT('',(-45.46495478095,0.,-70.97315781796)); -#1098 = DIRECTION('',(0.,1.,0.)); -#1099 = DIRECTION('',(0.,0.,1.)); -#1100 = ADVANCED_FACE('',(#1101),#1104,.F.); -#1101 = FACE_BOUND('',#1102,.F.); -#1102 = EDGE_LOOP('',(#1103)); -#1103 = ORIENTED_EDGE('',*,*,#965,.T.); -#1104 = PLANE('',#1105); -#1105 = AXIS2_PLACEMENT_3D('',#1106,#1107,#1108); -#1106 = CARTESIAN_POINT('',(-45.46495478095,0.,-70.97315781796)); -#1107 = DIRECTION('',(0.,1.,0.)); -#1108 = DIRECTION('',(0.,0.,1.)); -#1109 = ADVANCED_FACE('',(#1110),#1113,.F.); -#1110 = FACE_BOUND('',#1111,.F.); -#1111 = EDGE_LOOP('',(#1112)); -#1112 = ORIENTED_EDGE('',*,*,#1065,.T.); -#1113 = PLANE('',#1114); -#1114 = AXIS2_PLACEMENT_3D('',#1115,#1116,#1117); -#1115 = CARTESIAN_POINT('',(-45.46495478095,0.,-70.97315781796)); -#1116 = DIRECTION('',(0.,1.,0.)); -#1117 = DIRECTION('',(0.,0.,1.)); -#1118 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#1122)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#1119,#1120,#1121)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#1119 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#1120 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#1121 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#1122 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-05),#1119, - 'distance_accuracy_value','confusion accuracy'); -#1123 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7)); -ENDSEC; -END-ISO-10303-21; diff --git a/docs/CAD/design/mate-connectors/BearConnector_Female_Trimmed.step b/docs/CAD/design/mate-connectors/BearConnector_Female_Trimmed.step deleted file mode 100644 index e6575701e4..0000000000 --- a/docs/CAD/design/mate-connectors/BearConnector_Female_Trimmed.step +++ /dev/null @@ -1,1048 +0,0 @@ -ISO-10303-21; -HEADER; -FILE_DESCRIPTION(('FreeCAD Model'),'2;1'); -FILE_NAME('Open CASCADE Shape Model','2026-08-05T11:06:23',('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),#976); -#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,#157,#188,#212,#236,#261,#285,#310,#334,#358, - #383,#407,#432,#456,#480,#504,#528,#545,#675,#706,#730,#754,#778, - #802,#819,#850,#866,#882,#894,#919,#944,#958,#967)); -#17 = ADVANCED_FACE('',(#18),#152,.T.); -#18 = FACE_BOUND('',#19,.T.); -#19 = EDGE_LOOP('',(#20,#30,#38,#46,#54,#62,#70,#79,#87,#96,#104,#112, - #121,#129,#138,#146)); -#20 = ORIENTED_EDGE('',*,*,#21,.F.); -#21 = EDGE_CURVE('',#22,#24,#26,.T.); -#22 = VERTEX_POINT('',#23); -#23 = CARTESIAN_POINT('',(26.824640127118,20.8572529153,-55.23443461327) - ); -#24 = VERTEX_POINT('',#25); -#25 = CARTESIAN_POINT('',(42.184838000445,20.8572529153,-7.043416644207) - ); -#26 = LINE('',#27,#28); -#27 = CARTESIAN_POINT('',(28.89442018934,20.8572529153,-48.74071565254) - ); -#28 = VECTOR('',#29,1.); -#29 = DIRECTION('',(0.30368282823,0.,0.952773183837)); -#30 = ORIENTED_EDGE('',*,*,#31,.F.); -#31 = EDGE_CURVE('',#32,#22,#34,.T.); -#32 = VERTEX_POINT('',#33); -#33 = CARTESIAN_POINT('',(32.56245481215,20.8572529153,-60.7871552419)); -#34 = LINE('',#35,#36); -#35 = CARTESIAN_POINT('',(12.092418778801,20.8572529153,-40.97745458223) - ); -#36 = VECTOR('',#37,1.); -#37 = DIRECTION('',(-0.718602345805,0.,0.695421216677)); -#38 = ORIENTED_EDGE('',*,*,#39,.F.); -#39 = EDGE_CURVE('',#40,#32,#42,.T.); -#40 = VERTEX_POINT('',#41); -#41 = CARTESIAN_POINT('',(26.712845130625,20.8572529153,-66.83175546171) - ); -#42 = LINE('',#43,#44); -#43 = CARTESIAN_POINT('',(11.149878706433,20.8572529153,-82.91349687528) - ); -#44 = VECTOR('',#45,1.); -#45 = DIRECTION('',(0.695421216677,0.,0.718602345805)); -#46 = ORIENTED_EDGE('',*,*,#47,.F.); -#47 = EDGE_CURVE('',#48,#40,#50,.T.); -#48 = VERTEX_POINT('',#49); -#49 = CARTESIAN_POINT('',(20.57248334837,20.8572529153,-60.88947335481) - ); -#50 = LINE('',#51,#52); -#51 = CARTESIAN_POINT('',(9.111716439792,20.8572529153,-49.79841511636) - ); -#52 = VECTOR('',#53,1.); -#53 = DIRECTION('',(0.718602345805,0.,-0.695421216677)); -#54 = ORIENTED_EDGE('',*,*,#55,.F.); -#55 = EDGE_CURVE('',#56,#48,#58,.T.); -#56 = VERTEX_POINT('',#57); -#57 = CARTESIAN_POINT('',(-20.57248334837,20.8572529153,-60.88947335481) - ); -#58 = LINE('',#59,#60); -#59 = CARTESIAN_POINT('',(-12.44623571629,20.8572529153,-60.88947335481) - ); -#60 = VECTOR('',#61,1.); -#61 = DIRECTION('',(1.,0.,0.)); -#62 = ORIENTED_EDGE('',*,*,#63,.T.); -#63 = EDGE_CURVE('',#56,#64,#66,.T.); -#64 = VERTEX_POINT('',#65); -#65 = CARTESIAN_POINT('',(-25.60006917852,20.8572529153,-65.75487614033) - ); -#66 = LINE('',#67,#68); -#67 = CARTESIAN_POINT('',(-32.03294606502,20.8572529153,-71.98023721416) - ); -#68 = VECTOR('',#69,1.); -#69 = DIRECTION('',(-0.718602345805,0.,-0.695421216677)); -#70 = ORIENTED_EDGE('',*,*,#71,.F.); -#71 = EDGE_CURVE('',#72,#64,#74,.T.); -#72 = VERTEX_POINT('',#73); -#73 = CARTESIAN_POINT('',(-27.78972445201,20.8572529153,-65.71897950961) - ); -#74 = CIRCLE('',#75,1.548528137424); -#75 = AXIS2_PLACEMENT_3D('',#76,#77,#78); -#76 = CARTESIAN_POINT('',(-26.67694849991,20.8572529153,-64.64210018823) - ); -#77 = DIRECTION('',(0.,-1.,0.)); -#78 = DIRECTION('',(1.,0.,0.)); -#79 = ORIENTED_EDGE('',*,*,#80,.T.); -#80 = EDGE_CURVE('',#72,#81,#83,.T.); -#81 = VERTEX_POINT('',#82); -#82 = CARTESIAN_POINT('',(-29.01580660709,20.8572529153,-64.4520272055) - ); -#83 = LINE('',#84,#85); -#84 = CARTESIAN_POINT('',(-31.36389178357,20.8572529153,-62.02567109858) - ); -#85 = VECTOR('',#86,1.); -#86 = DIRECTION('',(-0.695421216677,0.,0.718602345805)); -#87 = ORIENTED_EDGE('',*,*,#88,.F.); -#88 = EDGE_CURVE('',#89,#81,#91,.T.); -#89 = VERTEX_POINT('',#90); -#90 = CARTESIAN_POINT('',(-28.89758284854,20.8572529153,-57.24050703685) - ); -#91 = CIRCLE('',#92,5.1); -#92 = AXIS2_PLACEMENT_3D('',#93,#94,#95); -#93 = CARTESIAN_POINT('',(-25.35093464349,20.8572529153,-60.90537900045) - ); -#94 = DIRECTION('',(0.,-1.,0.)); -#95 = DIRECTION('',(1.,0.,0.)); -#96 = ORIENTED_EDGE('',*,*,#97,.T.); -#97 = EDGE_CURVE('',#89,#98,#100,.T.); -#98 = VERTEX_POINT('',#99); -#99 = CARTESIAN_POINT('',(-26.82464012711,20.8572529153,-55.23443461327) - ); -#100 = LINE('',#101,#102); -#101 = CARTESIAN_POINT('',(-35.57003638008,20.8572529153,-63.69771634073 - )); -#102 = VECTOR('',#103,1.); -#103 = DIRECTION('',(0.718602345805,0.,0.695421216677)); -#104 = ORIENTED_EDGE('',*,*,#105,.T.); -#105 = EDGE_CURVE('',#98,#106,#108,.T.); -#106 = VERTEX_POINT('',#107); -#107 = CARTESIAN_POINT('',(-41.08376419406,20.8572529153,-10.49792081311 - )); -#108 = LINE('',#109,#110); -#109 = CARTESIAN_POINT('',(-32.53680963915,20.8572529153,-37.31309884227 - )); -#110 = VECTOR('',#111,1.); -#111 = DIRECTION('',(-0.30368282823,0.,0.952773183837)); -#112 = ORIENTED_EDGE('',*,*,#113,.F.); -#113 = EDGE_CURVE('',#114,#106,#116,.T.); -#114 = VERTEX_POINT('',#115); -#115 = CARTESIAN_POINT('',(-39.62105538679,20.8572529153,-4.479634030555 - )); -#116 = CIRCLE('',#117,5.954044962965); -#117 = AXIS2_PLACEMENT_3D('',#118,#119,#120); -#118 = CARTESIAN_POINT('',(-35.41090981799,20.8572529153,-8.689779599357 - )); -#119 = DIRECTION('',(0.,-1.,0.)); -#120 = DIRECTION('',(1.,0.,0.)); -#121 = ORIENTED_EDGE('',*,*,#122,.T.); -#122 = EDGE_CURVE('',#114,#123,#125,.T.); -#123 = VERTEX_POINT('',#124); -#124 = CARTESIAN_POINT('',(-36.7853207504,20.8572529153,-1.643899394163) - ); -#125 = LINE('',#126,#127); -#126 = CARTESIAN_POINT('',(-56.28754386398,20.8572529153,-21.14612250775 - )); -#127 = VECTOR('',#128,1.); -#128 = DIRECTION('',(0.707106781187,0.,0.707106781187)); -#129 = ORIENTED_EDGE('',*,*,#130,.F.); -#130 = EDGE_CURVE('',#131,#123,#133,.T.); -#131 = VERTEX_POINT('',#132); -#132 = CARTESIAN_POINT('',(-32.57517518159,20.8572529153, - 9.999999999998E-02)); -#133 = CIRCLE('',#134,5.954044962965); -#134 = AXIS2_PLACEMENT_3D('',#135,#136,#137); -#135 = CARTESIAN_POINT('',(-32.57517518159,20.8572529153,-5.854044962965 - )); -#136 = DIRECTION('',(0.,-1.,0.)); -#137 = DIRECTION('',(1.,0.,0.)); -#138 = ORIENTED_EDGE('',*,*,#139,.T.); -#139 = EDGE_CURVE('',#131,#140,#142,.T.); -#140 = VERTEX_POINT('',#141); -#141 = CARTESIAN_POINT('',(35.041421356237,20.8572529153,0.1)); -#142 = LINE('',#143,#144); -#143 = CARTESIAN_POINT('',(-5.211766712356,20.8572529153,0.1)); -#144 = VECTOR('',#145,1.); -#145 = DIRECTION('',(1.,0.,0.)); -#146 = ORIENTED_EDGE('',*,*,#147,.F.); -#147 = EDGE_CURVE('',#24,#140,#148,.T.); -#148 = LINE('',#149,#150); -#149 = CARTESIAN_POINT('',(32.68311677643,20.8572529153,2.458304579807) - ); -#150 = VECTOR('',#151,1.); -#151 = DIRECTION('',(-0.707106781187,0.,0.707106781187)); -#152 = PLANE('',#153); -#153 = AXIS2_PLACEMENT_3D('',#154,#155,#156); -#154 = CARTESIAN_POINT('',(-45.46495478095,20.8572529153,-70.97315781796 - )); -#155 = DIRECTION('',(0.,1.,0.)); -#156 = DIRECTION('',(0.,0.,1.)); -#157 = ADVANCED_FACE('',(#158),#183,.F.); -#158 = FACE_BOUND('',#159,.F.); -#159 = EDGE_LOOP('',(#160,#170,#176,#177)); -#160 = ORIENTED_EDGE('',*,*,#161,.T.); -#161 = EDGE_CURVE('',#162,#164,#166,.T.); -#162 = VERTEX_POINT('',#163); -#163 = CARTESIAN_POINT('',(26.824640127118,3.2,-55.23443461327)); -#164 = VERTEX_POINT('',#165); -#165 = CARTESIAN_POINT('',(42.184838000445,3.2,-7.043416644207)); -#166 = LINE('',#167,#168); -#167 = CARTESIAN_POINT('',(36.45312509682,3.2,-25.02606776693)); -#168 = VECTOR('',#169,1.); -#169 = DIRECTION('',(0.30368282823,0.,0.952773183837)); -#170 = ORIENTED_EDGE('',*,*,#171,.T.); -#171 = EDGE_CURVE('',#164,#24,#172,.T.); -#172 = LINE('',#173,#174); -#173 = CARTESIAN_POINT('',(42.184838000445,-5.,-7.043416644207)); -#174 = VECTOR('',#175,1.); -#175 = DIRECTION('',(0.,1.,0.)); -#176 = ORIENTED_EDGE('',*,*,#21,.F.); -#177 = ORIENTED_EDGE('',*,*,#178,.F.); -#178 = EDGE_CURVE('',#162,#22,#179,.T.); -#179 = LINE('',#180,#181); -#180 = CARTESIAN_POINT('',(26.824640127118,-5.,-55.23443461327)); -#181 = VECTOR('',#182,1.); -#182 = DIRECTION('',(0.,1.,0.)); -#183 = PLANE('',#184); -#184 = AXIS2_PLACEMENT_3D('',#185,#186,#187); -#185 = CARTESIAN_POINT('',(42.184838000445,-5.,-7.043416644207)); -#186 = DIRECTION('',(-0.952773183837,0.,0.30368282823)); -#187 = DIRECTION('',(-0.30368282823,0.,-0.952773183837)); -#188 = ADVANCED_FACE('',(#189),#207,.F.); -#189 = FACE_BOUND('',#190,.F.); -#190 = EDGE_LOOP('',(#191,#199,#205,#206)); -#191 = ORIENTED_EDGE('',*,*,#192,.T.); -#192 = EDGE_CURVE('',#164,#193,#195,.T.); -#193 = VERTEX_POINT('',#194); -#194 = CARTESIAN_POINT('',(35.041421356237,3.2,0.1)); -#195 = LINE('',#196,#197); -#196 = CARTESIAN_POINT('',(34.743124284224,3.2,0.398297072013)); -#197 = VECTOR('',#198,1.); -#198 = DIRECTION('',(-0.707106781187,0.,0.707106781187)); -#199 = ORIENTED_EDGE('',*,*,#200,.T.); -#200 = EDGE_CURVE('',#193,#140,#201,.T.); -#201 = LINE('',#202,#203); -#202 = CARTESIAN_POINT('',(35.041421356237,-5.,0.1)); -#203 = VECTOR('',#204,1.); -#204 = DIRECTION('',(0.,1.,0.)); -#205 = ORIENTED_EDGE('',*,*,#147,.F.); -#206 = ORIENTED_EDGE('',*,*,#171,.F.); -#207 = PLANE('',#208); -#208 = AXIS2_PLACEMENT_3D('',#209,#210,#211); -#209 = CARTESIAN_POINT('',(35.041421356237,-5.,9.999999999998E-02)); -#210 = DIRECTION('',(-0.707106781187,0.,-0.707106781187)); -#211 = DIRECTION('',(0.707106781187,0.,-0.707106781187)); -#212 = ADVANCED_FACE('',(#213),#231,.T.); -#213 = FACE_BOUND('',#214,.T.); -#214 = EDGE_LOOP('',(#215,#223,#224,#225)); -#215 = ORIENTED_EDGE('',*,*,#216,.T.); -#216 = EDGE_CURVE('',#217,#193,#219,.T.); -#217 = VERTEX_POINT('',#218); -#218 = CARTESIAN_POINT('',(-32.57517518159,3.2,9.999999999998E-02)); -#219 = LINE('',#220,#221); -#220 = CARTESIAN_POINT('',(17.722238935846,3.2,0.1)); -#221 = VECTOR('',#222,1.); -#222 = DIRECTION('',(1.,0.,0.)); -#223 = ORIENTED_EDGE('',*,*,#200,.T.); -#224 = ORIENTED_EDGE('',*,*,#139,.F.); -#225 = ORIENTED_EDGE('',*,*,#226,.F.); -#226 = EDGE_CURVE('',#217,#131,#227,.T.); -#227 = LINE('',#228,#229); -#228 = CARTESIAN_POINT('',(-32.57517518159,-5.,9.999999999998E-02)); -#229 = VECTOR('',#230,1.); -#230 = DIRECTION('',(0.,1.,0.)); -#231 = PLANE('',#232); -#232 = AXIS2_PLACEMENT_3D('',#233,#234,#235); -#233 = CARTESIAN_POINT('',(35.041421356237,-5.,0.1)); -#234 = DIRECTION('',(0.,0.,1.)); -#235 = DIRECTION('',(-1.,0.,0.)); -#236 = ADVANCED_FACE('',(#237),#256,.T.); -#237 = FACE_BOUND('',#238,.F.); -#238 = EDGE_LOOP('',(#239,#248,#254,#255)); -#239 = ORIENTED_EDGE('',*,*,#240,.T.); -#240 = EDGE_CURVE('',#217,#241,#243,.T.); -#241 = VERTEX_POINT('',#242); -#242 = CARTESIAN_POINT('',(-36.7853207504,3.2,-1.643899394163)); -#243 = CIRCLE('',#244,5.954044962965); -#244 = AXIS2_PLACEMENT_3D('',#245,#246,#247); -#245 = CARTESIAN_POINT('',(-32.57517518159,3.2,-5.854044962965)); -#246 = DIRECTION('',(0.,-1.,0.)); -#247 = DIRECTION('',(1.,0.,0.)); -#248 = ORIENTED_EDGE('',*,*,#249,.T.); -#249 = EDGE_CURVE('',#241,#123,#250,.T.); -#250 = LINE('',#251,#252); -#251 = CARTESIAN_POINT('',(-36.7853207504,-5.,-1.643899394163)); -#252 = VECTOR('',#253,1.); -#253 = DIRECTION('',(0.,1.,0.)); -#254 = ORIENTED_EDGE('',*,*,#130,.F.); -#255 = ORIENTED_EDGE('',*,*,#226,.F.); -#256 = CYLINDRICAL_SURFACE('',#257,5.954044962965); -#257 = AXIS2_PLACEMENT_3D('',#258,#259,#260); -#258 = CARTESIAN_POINT('',(-32.57517518159,-5.,-5.854044962965)); -#259 = DIRECTION('',(0.,-1.,0.)); -#260 = DIRECTION('',(1.,0.,0.)); -#261 = ADVANCED_FACE('',(#262),#280,.T.); -#262 = FACE_BOUND('',#263,.T.); -#263 = EDGE_LOOP('',(#264,#272,#273,#274)); -#264 = ORIENTED_EDGE('',*,*,#265,.T.); -#265 = EDGE_CURVE('',#266,#241,#268,.T.); -#266 = VERTEX_POINT('',#267); -#267 = CARTESIAN_POINT('',(-39.62105538679,3.2,-4.479634030555)); -#268 = LINE('',#269,#270); -#269 = CARTESIAN_POINT('',(-35.41354572357,3.2,-0.272124367341)); -#270 = VECTOR('',#271,1.); -#271 = DIRECTION('',(0.707106781187,0.,0.707106781187)); -#272 = ORIENTED_EDGE('',*,*,#249,.T.); -#273 = ORIENTED_EDGE('',*,*,#122,.F.); -#274 = ORIENTED_EDGE('',*,*,#275,.F.); -#275 = EDGE_CURVE('',#266,#114,#276,.T.); -#276 = LINE('',#277,#278); -#277 = CARTESIAN_POINT('',(-39.62105538679,-5.,-4.479634030555)); -#278 = VECTOR('',#279,1.); -#279 = DIRECTION('',(0.,1.,0.)); -#280 = PLANE('',#281); -#281 = AXIS2_PLACEMENT_3D('',#282,#283,#284); -#282 = CARTESIAN_POINT('',(-36.7853207504,-5.,-1.643899394163)); -#283 = DIRECTION('',(-0.707106781187,0.,0.707106781187)); -#284 = DIRECTION('',(-0.707106781187,0.,-0.707106781187)); -#285 = ADVANCED_FACE('',(#286),#305,.T.); -#286 = FACE_BOUND('',#287,.F.); -#287 = EDGE_LOOP('',(#288,#297,#303,#304)); -#288 = ORIENTED_EDGE('',*,*,#289,.T.); -#289 = EDGE_CURVE('',#266,#290,#292,.T.); -#290 = VERTEX_POINT('',#291); -#291 = CARTESIAN_POINT('',(-41.08376419406,3.2,-10.49792081311)); -#292 = CIRCLE('',#293,5.954044962965); -#293 = AXIS2_PLACEMENT_3D('',#294,#295,#296); -#294 = CARTESIAN_POINT('',(-35.41090981799,3.2,-8.689779599357)); -#295 = DIRECTION('',(0.,-1.,0.)); -#296 = DIRECTION('',(1.,0.,0.)); -#297 = ORIENTED_EDGE('',*,*,#298,.T.); -#298 = EDGE_CURVE('',#290,#106,#299,.T.); -#299 = LINE('',#300,#301); -#300 = CARTESIAN_POINT('',(-41.08376419406,-5.,-10.49792081311)); -#301 = VECTOR('',#302,1.); -#302 = DIRECTION('',(0.,1.,0.)); -#303 = ORIENTED_EDGE('',*,*,#113,.F.); -#304 = ORIENTED_EDGE('',*,*,#275,.F.); -#305 = CYLINDRICAL_SURFACE('',#306,5.954044962965); -#306 = AXIS2_PLACEMENT_3D('',#307,#308,#309); -#307 = CARTESIAN_POINT('',(-35.41090981799,-5.,-8.689779599357)); -#308 = DIRECTION('',(0.,-1.,0.)); -#309 = DIRECTION('',(1.,0.,0.)); -#310 = ADVANCED_FACE('',(#311),#329,.T.); -#311 = FACE_BOUND('',#312,.T.); -#312 = EDGE_LOOP('',(#313,#321,#322,#323)); -#313 = ORIENTED_EDGE('',*,*,#314,.T.); -#314 = EDGE_CURVE('',#315,#290,#317,.T.); -#315 = VERTEX_POINT('',#316); -#316 = CARTESIAN_POINT('',(-26.82464012711,3.2,-55.23443461327)); -#317 = LINE('',#318,#319); -#318 = CARTESIAN_POINT('',(-35.86541700774,3.2,-26.86994056823)); -#319 = VECTOR('',#320,1.); -#320 = DIRECTION('',(-0.30368282823,0.,0.952773183837)); -#321 = ORIENTED_EDGE('',*,*,#298,.T.); -#322 = ORIENTED_EDGE('',*,*,#105,.F.); -#323 = ORIENTED_EDGE('',*,*,#324,.F.); -#324 = EDGE_CURVE('',#315,#98,#325,.T.); -#325 = LINE('',#326,#327); -#326 = CARTESIAN_POINT('',(-26.82464012711,-5.,-55.23443461327)); -#327 = VECTOR('',#328,1.); -#328 = DIRECTION('',(0.,1.,0.)); -#329 = PLANE('',#330); -#330 = AXIS2_PLACEMENT_3D('',#331,#332,#333); -#331 = CARTESIAN_POINT('',(-41.08376419406,-5.,-10.49792081311)); -#332 = DIRECTION('',(-0.952773183837,0.,-0.30368282823)); -#333 = DIRECTION('',(0.30368282823,0.,-0.952773183837)); -#334 = ADVANCED_FACE('',(#335),#353,.T.); -#335 = FACE_BOUND('',#336,.T.); -#336 = EDGE_LOOP('',(#337,#345,#346,#347)); -#337 = ORIENTED_EDGE('',*,*,#338,.T.); -#338 = EDGE_CURVE('',#339,#315,#341,.T.); -#339 = VERTEX_POINT('',#340); -#340 = CARTESIAN_POINT('',(-28.89758284854,3.2,-57.24050703685)); -#341 = LINE('',#342,#343); -#342 = CARTESIAN_POINT('',(-14.32522020853,3.2,-43.13822889076)); -#343 = VECTOR('',#344,1.); -#344 = DIRECTION('',(0.718602345805,0.,0.695421216677)); -#345 = ORIENTED_EDGE('',*,*,#324,.T.); -#346 = ORIENTED_EDGE('',*,*,#97,.F.); -#347 = ORIENTED_EDGE('',*,*,#348,.F.); -#348 = EDGE_CURVE('',#339,#89,#349,.T.); -#349 = LINE('',#350,#351); -#350 = CARTESIAN_POINT('',(-28.89758284854,-5.,-57.24050703685)); -#351 = VECTOR('',#352,1.); -#352 = DIRECTION('',(0.,1.,0.)); -#353 = PLANE('',#354); -#354 = AXIS2_PLACEMENT_3D('',#355,#356,#357); -#355 = CARTESIAN_POINT('',(-26.82464012711,-5.,-55.23443461327)); -#356 = DIRECTION('',(-0.695421216677,0.,0.718602345805)); -#357 = DIRECTION('',(-0.718602345805,0.,-0.695421216677)); -#358 = ADVANCED_FACE('',(#359),#378,.T.); -#359 = FACE_BOUND('',#360,.F.); -#360 = EDGE_LOOP('',(#361,#370,#376,#377)); -#361 = ORIENTED_EDGE('',*,*,#362,.T.); -#362 = EDGE_CURVE('',#339,#363,#365,.T.); -#363 = VERTEX_POINT('',#364); -#364 = CARTESIAN_POINT('',(-29.01580660709,3.2,-64.4520272055)); -#365 = CIRCLE('',#366,5.1); -#366 = AXIS2_PLACEMENT_3D('',#367,#368,#369); -#367 = CARTESIAN_POINT('',(-25.35093464349,3.2,-60.90537900045)); -#368 = DIRECTION('',(0.,-1.,0.)); -#369 = DIRECTION('',(1.,0.,0.)); -#370 = ORIENTED_EDGE('',*,*,#371,.T.); -#371 = EDGE_CURVE('',#363,#81,#372,.T.); -#372 = LINE('',#373,#374); -#373 = CARTESIAN_POINT('',(-29.01580660709,-5.,-64.4520272055)); -#374 = VECTOR('',#375,1.); -#375 = DIRECTION('',(0.,1.,0.)); -#376 = ORIENTED_EDGE('',*,*,#88,.F.); -#377 = ORIENTED_EDGE('',*,*,#348,.F.); -#378 = CYLINDRICAL_SURFACE('',#379,5.1); -#379 = AXIS2_PLACEMENT_3D('',#380,#381,#382); -#380 = CARTESIAN_POINT('',(-25.35093464349,-5.,-60.90537900045)); -#381 = DIRECTION('',(0.,-1.,0.)); -#382 = DIRECTION('',(1.,0.,0.)); -#383 = ADVANCED_FACE('',(#384),#402,.T.); -#384 = FACE_BOUND('',#385,.T.); -#385 = EDGE_LOOP('',(#386,#394,#395,#396)); -#386 = ORIENTED_EDGE('',*,*,#387,.T.); -#387 = EDGE_CURVE('',#388,#363,#390,.T.); -#388 = VERTEX_POINT('',#389); -#389 = CARTESIAN_POINT('',(-27.78972445201,3.2,-65.71897950961)); -#390 = LINE('',#391,#392); -#391 = CARTESIAN_POINT('',(-29.67470230692,3.2,-63.77116791594)); -#392 = VECTOR('',#393,1.); -#393 = DIRECTION('',(-0.695421216677,0.,0.718602345805)); -#394 = ORIENTED_EDGE('',*,*,#371,.T.); -#395 = ORIENTED_EDGE('',*,*,#80,.F.); -#396 = ORIENTED_EDGE('',*,*,#397,.F.); -#397 = EDGE_CURVE('',#388,#72,#398,.T.); -#398 = LINE('',#399,#400); -#399 = CARTESIAN_POINT('',(-27.78972445201,-5.,-65.71897950961)); -#400 = VECTOR('',#401,1.); -#401 = DIRECTION('',(0.,1.,0.)); -#402 = PLANE('',#403); -#403 = AXIS2_PLACEMENT_3D('',#404,#405,#406); -#404 = CARTESIAN_POINT('',(-29.01580660709,-5.,-64.4520272055)); -#405 = DIRECTION('',(-0.718602345805,0.,-0.695421216677)); -#406 = DIRECTION('',(0.695421216677,0.,-0.718602345805)); -#407 = ADVANCED_FACE('',(#408),#427,.T.); -#408 = FACE_BOUND('',#409,.F.); -#409 = EDGE_LOOP('',(#410,#419,#425,#426)); -#410 = ORIENTED_EDGE('',*,*,#411,.T.); -#411 = EDGE_CURVE('',#388,#412,#414,.T.); -#412 = VERTEX_POINT('',#413); -#413 = CARTESIAN_POINT('',(-25.60006917852,3.2,-65.75487614033)); -#414 = CIRCLE('',#415,1.548528137424); -#415 = AXIS2_PLACEMENT_3D('',#416,#417,#418); -#416 = CARTESIAN_POINT('',(-26.67694849991,3.2,-64.64210018823)); -#417 = DIRECTION('',(0.,-1.,0.)); -#418 = DIRECTION('',(1.,0.,0.)); -#419 = ORIENTED_EDGE('',*,*,#420,.T.); -#420 = EDGE_CURVE('',#412,#64,#421,.T.); -#421 = LINE('',#422,#423); -#422 = CARTESIAN_POINT('',(-25.60006917852,-5.,-65.75487614033)); -#423 = VECTOR('',#424,1.); -#424 = DIRECTION('',(0.,1.,0.)); -#425 = ORIENTED_EDGE('',*,*,#71,.F.); -#426 = ORIENTED_EDGE('',*,*,#397,.F.); -#427 = CYLINDRICAL_SURFACE('',#428,1.548528137424); -#428 = AXIS2_PLACEMENT_3D('',#429,#430,#431); -#429 = CARTESIAN_POINT('',(-26.67694849991,-5.,-64.64210018823)); -#430 = DIRECTION('',(0.,-1.,0.)); -#431 = DIRECTION('',(1.,0.,0.)); -#432 = ADVANCED_FACE('',(#433),#451,.T.); -#433 = FACE_BOUND('',#434,.T.); -#434 = EDGE_LOOP('',(#435,#443,#444,#445)); -#435 = ORIENTED_EDGE('',*,*,#436,.T.); -#436 = EDGE_CURVE('',#437,#412,#439,.T.); -#437 = VERTEX_POINT('',#438); -#438 = CARTESIAN_POINT('',(-20.57248334837,3.2,-60.88947335481)); -#439 = LINE('',#440,#441); -#440 = CARTESIAN_POINT('',(-10.78812989347,3.2,-51.42074976419)); -#441 = VECTOR('',#442,1.); -#442 = DIRECTION('',(-0.718602345805,0.,-0.695421216677)); -#443 = ORIENTED_EDGE('',*,*,#420,.T.); -#444 = ORIENTED_EDGE('',*,*,#63,.F.); -#445 = ORIENTED_EDGE('',*,*,#446,.F.); -#446 = EDGE_CURVE('',#437,#56,#447,.T.); -#447 = LINE('',#448,#449); -#448 = CARTESIAN_POINT('',(-20.57248334837,-5.,-60.88947335481)); -#449 = VECTOR('',#450,1.); -#450 = DIRECTION('',(0.,1.,0.)); -#451 = PLANE('',#452); -#452 = AXIS2_PLACEMENT_3D('',#453,#454,#455); -#453 = CARTESIAN_POINT('',(-25.60006917852,-5.,-65.75487614033)); -#454 = DIRECTION('',(0.695421216677,0.,-0.718602345805)); -#455 = DIRECTION('',(0.718602345805,0.,0.695421216677)); -#456 = ADVANCED_FACE('',(#457),#475,.F.); -#457 = FACE_BOUND('',#458,.F.); -#458 = EDGE_LOOP('',(#459,#467,#473,#474)); -#459 = ORIENTED_EDGE('',*,*,#460,.T.); -#460 = EDGE_CURVE('',#437,#461,#463,.T.); -#461 = VERTEX_POINT('',#462); -#462 = CARTESIAN_POINT('',(20.57248334837,3.2,-60.88947335481)); -#463 = LINE('',#464,#465); -#464 = CARTESIAN_POINT('',(10.487769931912,3.2,-60.88947335481)); -#465 = VECTOR('',#466,1.); -#466 = DIRECTION('',(1.,0.,0.)); -#467 = ORIENTED_EDGE('',*,*,#468,.T.); -#468 = EDGE_CURVE('',#461,#48,#469,.T.); -#469 = LINE('',#470,#471); -#470 = CARTESIAN_POINT('',(20.57248334837,-5.,-60.88947335481)); -#471 = VECTOR('',#472,1.); -#472 = DIRECTION('',(0.,1.,0.)); -#473 = ORIENTED_EDGE('',*,*,#55,.F.); -#474 = ORIENTED_EDGE('',*,*,#446,.F.); -#475 = PLANE('',#476); -#476 = AXIS2_PLACEMENT_3D('',#477,#478,#479); -#477 = CARTESIAN_POINT('',(20.57248334837,-5.,-60.88947335481)); -#478 = DIRECTION('',(0.,0.,1.)); -#479 = DIRECTION('',(-1.,0.,0.)); -#480 = ADVANCED_FACE('',(#481),#499,.F.); -#481 = FACE_BOUND('',#482,.F.); -#482 = EDGE_LOOP('',(#483,#491,#497,#498)); -#483 = ORIENTED_EDGE('',*,*,#484,.T.); -#484 = EDGE_CURVE('',#461,#485,#487,.T.); -#485 = VERTEX_POINT('',#486); -#486 = CARTESIAN_POINT('',(26.712845130625,3.2,-66.83175546171)); -#487 = LINE('',#488,#489); -#488 = CARTESIAN_POINT('',(11.552651954056,3.2,-52.16060938843)); -#489 = VECTOR('',#490,1.); -#490 = DIRECTION('',(0.718602345805,0.,-0.695421216677)); -#491 = ORIENTED_EDGE('',*,*,#492,.T.); -#492 = EDGE_CURVE('',#485,#40,#493,.T.); -#493 = LINE('',#494,#495); -#494 = CARTESIAN_POINT('',(26.712845130625,-5.,-66.83175546171)); -#495 = VECTOR('',#496,1.); -#496 = DIRECTION('',(0.,1.,0.)); -#497 = ORIENTED_EDGE('',*,*,#47,.F.); -#498 = ORIENTED_EDGE('',*,*,#468,.F.); -#499 = PLANE('',#500); -#500 = AXIS2_PLACEMENT_3D('',#501,#502,#503); -#501 = CARTESIAN_POINT('',(26.712845130625,-5.,-66.83175546171)); -#502 = DIRECTION('',(0.695421216677,0.,0.718602345805)); -#503 = DIRECTION('',(-0.718602345805,0.,0.695421216677)); -#504 = ADVANCED_FACE('',(#505),#523,.F.); -#505 = FACE_BOUND('',#506,.F.); -#506 = EDGE_LOOP('',(#507,#515,#521,#522)); -#507 = ORIENTED_EDGE('',*,*,#508,.T.); -#508 = EDGE_CURVE('',#485,#509,#511,.T.); -#509 = VERTEX_POINT('',#510); -#510 = CARTESIAN_POINT('',(32.56245481215,3.2,-60.7871552419)); -#511 = LINE('',#512,#513); -#512 = CARTESIAN_POINT('',(31.642948840372,3.2,-61.73731197059)); -#513 = VECTOR('',#514,1.); -#514 = DIRECTION('',(0.695421216677,0.,0.718602345805)); -#515 = ORIENTED_EDGE('',*,*,#516,.T.); -#516 = EDGE_CURVE('',#509,#32,#517,.T.); -#517 = LINE('',#518,#519); -#518 = CARTESIAN_POINT('',(32.56245481215,-5.,-60.7871552419)); -#519 = VECTOR('',#520,1.); -#520 = DIRECTION('',(0.,1.,0.)); -#521 = ORIENTED_EDGE('',*,*,#39,.F.); -#522 = ORIENTED_EDGE('',*,*,#492,.F.); -#523 = PLANE('',#524); -#524 = AXIS2_PLACEMENT_3D('',#525,#526,#527); -#525 = CARTESIAN_POINT('',(32.56245481215,-5.,-60.7871552419)); -#526 = DIRECTION('',(-0.718602345805,0.,0.695421216677)); -#527 = DIRECTION('',(-0.695421216677,0.,-0.718602345805)); -#528 = ADVANCED_FACE('',(#529),#540,.F.); -#529 = FACE_BOUND('',#530,.F.); -#530 = EDGE_LOOP('',(#531,#537,#538,#539)); -#531 = ORIENTED_EDGE('',*,*,#532,.T.); -#532 = EDGE_CURVE('',#509,#162,#533,.T.); -#533 = LINE('',#534,#535); -#534 = CARTESIAN_POINT('',(14.533354293065,3.2,-43.3396488543)); -#535 = VECTOR('',#536,1.); -#536 = DIRECTION('',(-0.718602345805,0.,0.695421216677)); -#537 = ORIENTED_EDGE('',*,*,#178,.T.); -#538 = ORIENTED_EDGE('',*,*,#31,.F.); -#539 = ORIENTED_EDGE('',*,*,#516,.F.); -#540 = PLANE('',#541); -#541 = AXIS2_PLACEMENT_3D('',#542,#543,#544); -#542 = CARTESIAN_POINT('',(26.824640127118,-5.,-55.23443461327)); -#543 = DIRECTION('',(-0.695421216677,0.,-0.718602345805)); -#544 = DIRECTION('',(0.718602345805,0.,-0.695421216677)); -#545 = ADVANCED_FACE('',(#546,#564,#614,#648,#659),#670,.F.); -#546 = FACE_BOUND('',#547,.F.); -#547 = EDGE_LOOP('',(#548,#549,#550,#551,#552,#553,#554,#555,#556,#557, - #558,#559,#560,#561,#562,#563)); -#548 = ORIENTED_EDGE('',*,*,#161,.F.); -#549 = ORIENTED_EDGE('',*,*,#532,.F.); -#550 = ORIENTED_EDGE('',*,*,#508,.F.); -#551 = ORIENTED_EDGE('',*,*,#484,.F.); -#552 = ORIENTED_EDGE('',*,*,#460,.F.); -#553 = ORIENTED_EDGE('',*,*,#436,.T.); -#554 = ORIENTED_EDGE('',*,*,#411,.F.); -#555 = ORIENTED_EDGE('',*,*,#387,.T.); -#556 = ORIENTED_EDGE('',*,*,#362,.F.); -#557 = ORIENTED_EDGE('',*,*,#338,.T.); -#558 = ORIENTED_EDGE('',*,*,#314,.T.); -#559 = ORIENTED_EDGE('',*,*,#289,.F.); -#560 = ORIENTED_EDGE('',*,*,#265,.T.); -#561 = ORIENTED_EDGE('',*,*,#240,.F.); -#562 = ORIENTED_EDGE('',*,*,#216,.T.); -#563 = ORIENTED_EDGE('',*,*,#192,.F.); -#564 = FACE_BOUND('',#565,.F.); -#565 = EDGE_LOOP('',(#566,#576,#584,#592,#600,#608)); -#566 = ORIENTED_EDGE('',*,*,#567,.F.); -#567 = EDGE_CURVE('',#568,#570,#572,.T.); -#568 = VERTEX_POINT('',#569); -#569 = CARTESIAN_POINT('',(16.626582997737,3.2,-8.940188245231)); -#570 = VERTEX_POINT('',#571); -#571 = CARTESIAN_POINT('',(4.383041634064E-04,3.2,-8.903751615529)); -#572 = LINE('',#573,#574); -#573 = CARTESIAN_POINT('',(4.347099726942,3.2,-8.913277437397)); -#574 = VECTOR('',#575,1.); -#575 = DIRECTION('',(-0.999997598615,0.,2.19152081707E-03)); -#576 = ORIENTED_EDGE('',*,*,#577,.F.); -#577 = EDGE_CURVE('',#578,#568,#580,.T.); -#578 = VERTEX_POINT('',#579); -#579 = CARTESIAN_POINT('',(19.029295926024,3.2,-17.63009960955)); -#580 = LINE('',#581,#582); -#581 = CARTESIAN_POINT('',(19.849519003668,3.2,-20.59660707692)); -#582 = VECTOR('',#583,1.); -#583 = DIRECTION('',(-0.26649542889,0.,0.963836182336)); -#584 = ORIENTED_EDGE('',*,*,#585,.F.); -#585 = EDGE_CURVE('',#586,#578,#588,.T.); -#586 = VERTEX_POINT('',#587); -#587 = CARTESIAN_POINT('',(22.059435554995,3.2,-3.734519760785)); -#588 = LINE('',#589,#590); -#589 = CARTESIAN_POINT('',(17.698510515043,3.2,-23.73280021221)); -#590 = VECTOR('',#591,1.); -#591 = DIRECTION('',(-0.213058124893,0.,-0.977039526026)); -#592 = ORIENTED_EDGE('',*,*,#593,.F.); -#593 = EDGE_CURVE('',#594,#586,#596,.T.); -#594 = VERTEX_POINT('',#595); -#595 = CARTESIAN_POINT('',(-21.72552223146,3.2,-3.734519760785)); -#596 = LINE('',#597,#598); -#597 = CARTESIAN_POINT('',(0.297084840953,3.2,-3.734519760785)); -#598 = VECTOR('',#599,1.); -#599 = DIRECTION('',(1.,0.,0.)); -#600 = ORIENTED_EDGE('',*,*,#601,.F.); -#601 = EDGE_CURVE('',#602,#594,#604,.T.); -#602 = VERTEX_POINT('',#603); -#603 = CARTESIAN_POINT('',(-21.72552223146,3.2,-8.903751135252)); -#604 = LINE('',#605,#606); -#605 = CARTESIAN_POINT('',(-21.72552223146,3.2,-19.83215600037)); -#606 = VECTOR('',#607,1.); -#607 = DIRECTION('',(0.,0.,1.)); -#608 = ORIENTED_EDGE('',*,*,#609,.F.); -#609 = EDGE_CURVE('',#570,#602,#610,.T.); -#610 = LINE('',#611,#612); -#611 = CARTESIAN_POINT('',(-5.279852300138,3.2,-8.903751135252)); -#612 = VECTOR('',#613,1.); -#613 = DIRECTION('',(-1.,0.,0.)); -#614 = FACE_BOUND('',#615,.F.); -#615 = EDGE_LOOP('',(#616,#626,#634,#642)); -#616 = ORIENTED_EDGE('',*,*,#617,.F.); -#617 = EDGE_CURVE('',#618,#620,#622,.T.); -#618 = VERTEX_POINT('',#619); -#619 = CARTESIAN_POINT('',(5.809375885494,3.2,-13.06417917474)); -#620 = VERTEX_POINT('',#621); -#621 = CARTESIAN_POINT('',(2.688069798796,3.2,-49.64588621989)); -#622 = LINE('',#623,#624); -#623 = CARTESIAN_POINT('',(4.914157977861,3.2,-23.55613296875)); -#624 = VECTOR('',#625,1.); -#625 = DIRECTION('',(-8.501532861638E-02,0.,-0.996379643459)); -#626 = ORIENTED_EDGE('',*,*,#627,.F.); -#627 = EDGE_CURVE('',#628,#618,#630,.T.); -#628 = VERTEX_POINT('',#629); -#629 = CARTESIAN_POINT('',(-5.809375885494,3.2,-13.06417917474)); -#630 = LINE('',#631,#632); -#631 = CARTESIAN_POINT('',(1.615747408047,3.2,-13.06417917474)); -#632 = VECTOR('',#633,1.); -#633 = DIRECTION('',(1.,0.,-3.066574716487E-16)); -#634 = ORIENTED_EDGE('',*,*,#635,.F.); -#635 = EDGE_CURVE('',#636,#628,#638,.T.); -#636 = VERTEX_POINT('',#637); -#637 = CARTESIAN_POINT('',(-2.688069798796,3.2,-49.64588621989)); -#638 = LINE('',#639,#640); -#639 = CARTESIAN_POINT('',(-4.890802006217,3.2,-23.82986495424)); -#640 = VECTOR('',#641,1.); -#641 = DIRECTION('',(-8.501532861638E-02,0.,0.996379643459)); -#642 = ORIENTED_EDGE('',*,*,#643,.F.); -#643 = EDGE_CURVE('',#620,#636,#644,.T.); -#644 = LINE('',#645,#646); -#645 = CARTESIAN_POINT('',(1.615747408047,3.2,-49.64588621989)); -#646 = VECTOR('',#647,1.); -#647 = DIRECTION('',(-1.,0.,0.)); -#648 = FACE_BOUND('',#649,.F.); -#649 = EDGE_LOOP('',(#650)); -#650 = ORIENTED_EDGE('',*,*,#651,.F.); -#651 = EDGE_CURVE('',#652,#652,#654,.T.); -#652 = VERTEX_POINT('',#653); -#653 = CARTESIAN_POINT('',(-11.6927539352,3.2,-48.00951684793)); -#654 = CIRCLE('',#655,4.735522705283); -#655 = AXIS2_PLACEMENT_3D('',#656,#657,#658); -#656 = CARTESIAN_POINT('',(-16.42827664048,3.2,-48.00951684793)); -#657 = DIRECTION('',(-0.,1.,0.)); -#658 = DIRECTION('',(1.,0.,0.)); -#659 = FACE_BOUND('',#660,.F.); -#660 = EDGE_LOOP('',(#661)); -#661 = ORIENTED_EDGE('',*,*,#662,.F.); -#662 = EDGE_CURVE('',#663,#663,#665,.T.); -#663 = VERTEX_POINT('',#664); -#664 = CARTESIAN_POINT('',(21.163799345768,3.2,-48.00951684793)); -#665 = CIRCLE('',#666,4.735522705283); -#666 = AXIS2_PLACEMENT_3D('',#667,#668,#669); -#667 = CARTESIAN_POINT('',(16.428276640485,3.2,-48.00951684793)); -#668 = DIRECTION('',(-0.,1.,0.)); -#669 = DIRECTION('',(1.,0.,0.)); -#670 = PLANE('',#671); -#671 = AXIS2_PLACEMENT_3D('',#672,#673,#674); -#672 = CARTESIAN_POINT('',(0.403056515455,3.2,-33.34517655273)); -#673 = DIRECTION('',(0.,1.,0.)); -#674 = DIRECTION('',(1.,0.,0.)); -#675 = ADVANCED_FACE('',(#676),#701,.T.); -#676 = FACE_BOUND('',#677,.T.); -#677 = EDGE_LOOP('',(#678,#686,#694,#700)); -#678 = ORIENTED_EDGE('',*,*,#679,.F.); -#679 = EDGE_CURVE('',#680,#568,#682,.T.); -#680 = VERTEX_POINT('',#681); -#681 = CARTESIAN_POINT('',(16.626582997737,0.,-8.940188245231)); -#682 = LINE('',#683,#684); -#683 = CARTESIAN_POINT('',(16.626582997737,-22.,-8.940188245231)); -#684 = VECTOR('',#685,1.); -#685 = DIRECTION('',(0.,1.,0.)); -#686 = ORIENTED_EDGE('',*,*,#687,.T.); -#687 = EDGE_CURVE('',#680,#688,#690,.T.); -#688 = VERTEX_POINT('',#689); -#689 = CARTESIAN_POINT('',(-5.329070518201E-15,0.,-8.903751135252)); -#690 = LINE('',#691,#692); -#691 = CARTESIAN_POINT('',(-18.54556462154,1.7763568394E-15, - -8.863107566442)); -#692 = VECTOR('',#693,1.); -#693 = DIRECTION('',(-0.999997598615,0.,2.19152081707E-03)); -#694 = ORIENTED_EDGE('',*,*,#695,.T.); -#695 = EDGE_CURVE('',#688,#570,#696,.T.); -#696 = LINE('',#697,#698); -#697 = CARTESIAN_POINT('',(-5.329070518201E-15,-22.,-8.903751135252)); -#698 = VECTOR('',#699,1.); -#699 = DIRECTION('',(0.,1.,0.)); -#700 = ORIENTED_EDGE('',*,*,#567,.F.); -#701 = PLANE('',#702); -#702 = AXIS2_PLACEMENT_3D('',#703,#704,#705); -#703 = CARTESIAN_POINT('',(8.237581109188,-22.,-8.921803528809)); -#704 = DIRECTION('',(-2.19152081707E-03,0.,-0.999997598615)); -#705 = DIRECTION('',(-0.999997598615,0.,2.19152081707E-03)); -#706 = ADVANCED_FACE('',(#707),#725,.T.); -#707 = FACE_BOUND('',#708,.T.); -#708 = EDGE_LOOP('',(#709,#717,#723,#724)); -#709 = ORIENTED_EDGE('',*,*,#710,.F.); -#710 = EDGE_CURVE('',#711,#578,#713,.T.); -#711 = VERTEX_POINT('',#712); -#712 = CARTESIAN_POINT('',(19.029295926024,0.,-17.63009960955)); -#713 = LINE('',#714,#715); -#714 = CARTESIAN_POINT('',(19.029295926024,-22.,-17.63009960955)); -#715 = VECTOR('',#716,1.); -#716 = DIRECTION('',(0.,1.,0.)); -#717 = ORIENTED_EDGE('',*,*,#718,.T.); -#718 = EDGE_CURVE('',#711,#680,#719,.T.); -#719 = LINE('',#720,#721); -#720 = CARTESIAN_POINT('',(23.053273013696,0.,-32.18365022821)); -#721 = VECTOR('',#722,1.); -#722 = DIRECTION('',(-0.26649542889,0.,0.963836182336)); -#723 = ORIENTED_EDGE('',*,*,#679,.T.); -#724 = ORIENTED_EDGE('',*,*,#577,.F.); -#725 = PLANE('',#726); -#726 = AXIS2_PLACEMENT_3D('',#727,#728,#729); -#727 = CARTESIAN_POINT('',(17.956031892536,-22.,-13.7484168618)); -#728 = DIRECTION('',(-0.963836182336,0.,-0.26649542889)); -#729 = DIRECTION('',(-0.26649542889,0.,0.963836182336)); -#730 = ADVANCED_FACE('',(#731),#749,.T.); -#731 = FACE_BOUND('',#732,.T.); -#732 = EDGE_LOOP('',(#733,#741,#747,#748)); -#733 = ORIENTED_EDGE('',*,*,#734,.T.); -#734 = EDGE_CURVE('',#688,#735,#737,.T.); -#735 = VERTEX_POINT('',#736); -#736 = CARTESIAN_POINT('',(-21.72552223146,0.,-8.903751135252)); -#737 = LINE('',#738,#739); -#738 = CARTESIAN_POINT('',(-28.21385794834,0.,-8.903751135252)); -#739 = VECTOR('',#740,1.); -#740 = DIRECTION('',(-1.,0.,0.)); -#741 = ORIENTED_EDGE('',*,*,#742,.T.); -#742 = EDGE_CURVE('',#735,#602,#743,.T.); -#743 = LINE('',#744,#745); -#744 = CARTESIAN_POINT('',(-21.72552223146,-22.,-8.903751135252)); -#745 = VECTOR('',#746,1.); -#746 = DIRECTION('',(0.,1.,0.)); -#747 = ORIENTED_EDGE('',*,*,#609,.F.); -#748 = ORIENTED_EDGE('',*,*,#695,.F.); -#749 = PLANE('',#750); -#750 = AXIS2_PLACEMENT_3D('',#751,#752,#753); -#751 = CARTESIAN_POINT('',(-10.96276111573,-22.,-8.903751135252)); -#752 = DIRECTION('',(0.,0.,-1.)); -#753 = DIRECTION('',(0.,1.,0.)); -#754 = ADVANCED_FACE('',(#755),#773,.T.); -#755 = FACE_BOUND('',#756,.T.); -#756 = EDGE_LOOP('',(#757,#765,#766,#767)); -#757 = ORIENTED_EDGE('',*,*,#758,.T.); -#758 = EDGE_CURVE('',#759,#711,#761,.T.); -#759 = VERTEX_POINT('',#760); -#760 = CARTESIAN_POINT('',(22.059435554995,0.,-3.734519760785)); -#761 = LINE('',#762,#763); -#762 = CARTESIAN_POINT('',(12.741012548745,1.7763568394E-15, - -46.46683800444)); -#763 = VECTOR('',#764,1.); -#764 = DIRECTION('',(-0.213058124893,0.,-0.977039526026)); -#765 = ORIENTED_EDGE('',*,*,#710,.T.); -#766 = ORIENTED_EDGE('',*,*,#585,.F.); -#767 = ORIENTED_EDGE('',*,*,#768,.F.); -#768 = EDGE_CURVE('',#759,#586,#769,.T.); -#769 = LINE('',#770,#771); -#770 = CARTESIAN_POINT('',(22.059435554995,-22.,-3.734519760785)); -#771 = VECTOR('',#772,1.); -#772 = DIRECTION('',(0.,1.,0.)); -#773 = PLANE('',#774); -#774 = AXIS2_PLACEMENT_3D('',#775,#776,#777); -#775 = CARTESIAN_POINT('',(20.484588228021,-22.,-10.95643672209)); -#776 = DIRECTION('',(0.977039526026,0.,-0.213058124893)); -#777 = DIRECTION('',(-0.213058124893,0.,-0.977039526026)); -#778 = ADVANCED_FACE('',(#779),#797,.T.); -#779 = FACE_BOUND('',#780,.T.); -#780 = EDGE_LOOP('',(#781,#789,#795,#796)); -#781 = ORIENTED_EDGE('',*,*,#782,.T.); -#782 = EDGE_CURVE('',#735,#783,#785,.T.); -#783 = VERTEX_POINT('',#784); -#784 = CARTESIAN_POINT('',(-21.72552223146,0.,-3.734519760785)); -#785 = LINE('',#786,#787); -#786 = CARTESIAN_POINT('',(-21.72552223146,0.,-38.64614663299)); -#787 = VECTOR('',#788,1.); -#788 = DIRECTION('',(0.,0.,1.)); -#789 = ORIENTED_EDGE('',*,*,#790,.T.); -#790 = EDGE_CURVE('',#783,#594,#791,.T.); -#791 = LINE('',#792,#793); -#792 = CARTESIAN_POINT('',(-21.72552223146,-22.,-3.734519760785)); -#793 = VECTOR('',#794,1.); -#794 = DIRECTION('',(0.,1.,0.)); -#795 = ORIENTED_EDGE('',*,*,#601,.F.); -#796 = ORIENTED_EDGE('',*,*,#742,.F.); -#797 = PLANE('',#798); -#798 = AXIS2_PLACEMENT_3D('',#799,#800,#801); -#799 = CARTESIAN_POINT('',(-21.72552223146,-22.,-6.319135448019)); -#800 = DIRECTION('',(-1.,0.,0.)); -#801 = DIRECTION('',(0.,1.,0.)); -#802 = ADVANCED_FACE('',(#803),#814,.T.); -#803 = FACE_BOUND('',#804,.T.); -#804 = EDGE_LOOP('',(#805,#811,#812,#813)); -#805 = ORIENTED_EDGE('',*,*,#806,.T.); -#806 = EDGE_CURVE('',#783,#759,#807,.T.); -#807 = LINE('',#808,#809); -#808 = CARTESIAN_POINT('',(-22.63692080725,0.,-3.734519760785)); -#809 = VECTOR('',#810,1.); -#810 = DIRECTION('',(1.,0.,0.)); -#811 = ORIENTED_EDGE('',*,*,#768,.T.); -#812 = ORIENTED_EDGE('',*,*,#593,.F.); -#813 = ORIENTED_EDGE('',*,*,#790,.F.); -#814 = PLANE('',#815); -#815 = AXIS2_PLACEMENT_3D('',#816,#817,#818); -#816 = CARTESIAN_POINT('',(0.19111316645,-22.,-3.734519760785)); -#817 = DIRECTION('',(0.,0.,1.)); -#818 = DIRECTION('',(0.,-1.,0.)); -#819 = ADVANCED_FACE('',(#820),#845,.F.); -#820 = FACE_BOUND('',#821,.F.); -#821 = EDGE_LOOP('',(#822,#832,#838,#839)); -#822 = ORIENTED_EDGE('',*,*,#823,.T.); -#823 = EDGE_CURVE('',#824,#826,#828,.T.); -#824 = VERTEX_POINT('',#825); -#825 = CARTESIAN_POINT('',(1.480297366167E-15,11.242400581089, - -46.71869179633)); -#826 = VERTEX_POINT('',#827); -#827 = CARTESIAN_POINT('',(3.256654205567E-15,17.8572529153, - -18.39898295202)); -#828 = LINE('',#829,#830); -#829 = CARTESIAN_POINT('',(2.6645352591E-15,18.133069549222, - -17.21814831317)); -#830 = VECTOR('',#831,1.); -#831 = DIRECTION('',(5.275122655165E-17,0.227455280238,0.97378852709)); -#832 = ORIENTED_EDGE('',*,*,#833,.F.); -#833 = EDGE_CURVE('',#618,#826,#834,.T.); -#834 = LINE('',#835,#836); -#835 = CARTESIAN_POINT('',(5.649679875255,3.602918464526,-13.21082950266 - )); -#836 = VECTOR('',#837,1.); -#837 = DIRECTION('',(-0.349023821871,0.880598971639,-0.320511814002)); -#838 = ORIENTED_EDGE('',*,*,#617,.T.); -#839 = ORIENTED_EDGE('',*,*,#840,.T.); -#840 = EDGE_CURVE('',#620,#824,#841,.T.); -#841 = LINE('',#842,#843); -#842 = CARTESIAN_POINT('',(1.103762571829,7.940067898719,-47.92064259636 - )); -#843 = VECTOR('',#844,1.); -#844 = DIRECTION('',(-0.299648208284,0.896513522642,0.326304236859)); -#845 = PLANE('',#846); -#846 = AXIS2_PLACEMENT_3D('',#847,#848,#849); -#847 = CARTESIAN_POINT('',(5.823691883056,3.068404028665,-13.45978839622 - )); -#848 = DIRECTION('',(0.93629059846,0.342020143326,-7.988827695448E-02)); -#849 = DIRECTION('',(-0.340781908463,0.939692620786,2.907695487825E-02) - ); -#850 = ADVANCED_FACE('',(#851),#861,.F.); -#851 = FACE_BOUND('',#852,.F.); -#852 = EDGE_LOOP('',(#853,#854,#860)); -#853 = ORIENTED_EDGE('',*,*,#833,.T.); -#854 = ORIENTED_EDGE('',*,*,#855,.T.); -#855 = EDGE_CURVE('',#826,#628,#856,.T.); -#856 = LINE('',#857,#858); -#857 = CARTESIAN_POINT('',(-5.275833888477,4.546144602338, - -13.55413574101)); -#858 = VECTOR('',#859,1.); -#859 = DIRECTION('',(-0.349023821871,-0.880598971639,0.320511814002)); -#860 = ORIENTED_EDGE('',*,*,#627,.T.); -#861 = PLANE('',#862); -#862 = AXIS2_PLACEMENT_3D('',#863,#864,#865); -#863 = CARTESIAN_POINT('',(2.828438300639,3.068404028665,-13.01628215822 - )); -#864 = DIRECTION('',(2.88163763217E-16,0.342020143326,0.939692620786)); -#865 = DIRECTION('',(-1.048830324052E-16,0.939692620786,-0.342020143326) - ); -#866 = ADVANCED_FACE('',(#867),#877,.F.); -#867 = FACE_BOUND('',#868,.F.); -#868 = EDGE_LOOP('',(#869,#875,#876)); -#869 = ORIENTED_EDGE('',*,*,#870,.T.); -#870 = EDGE_CURVE('',#636,#824,#871,.T.); -#871 = LINE('',#872,#873); -#872 = CARTESIAN_POINT('',(-0.871390517001,8.635298785064, - -47.66759924779)); -#873 = VECTOR('',#874,1.); -#874 = DIRECTION('',(0.299648208284,0.896513522642,0.326304236859)); -#875 = ORIENTED_EDGE('',*,*,#840,.F.); -#876 = ORIENTED_EDGE('',*,*,#643,.T.); -#877 = PLANE('',#878); -#878 = AXIS2_PLACEMENT_3D('',#879,#880,#881); -#879 = CARTESIAN_POINT('',(2.828438300639,3.068404028665,-49.69378323641 - )); -#880 = DIRECTION('',(0.,0.342020143326,-0.939692620786)); -#881 = DIRECTION('',(0.,0.939692620786,0.342020143326)); -#882 = ADVANCED_FACE('',(#883),#889,.F.); -#883 = FACE_BOUND('',#884,.F.); -#884 = EDGE_LOOP('',(#885,#886,#887,#888)); -#885 = ORIENTED_EDGE('',*,*,#635,.T.); -#886 = ORIENTED_EDGE('',*,*,#855,.F.); -#887 = ORIENTED_EDGE('',*,*,#823,.F.); -#888 = ORIENTED_EDGE('',*,*,#870,.F.); -#889 = PLANE('',#890); -#890 = AXIS2_PLACEMENT_3D('',#891,#892,#893); -#891 = CARTESIAN_POINT('',(-5.782806207227,3.068404028665, - -13.93896851312)); -#892 = DIRECTION('',(-0.93629059846,0.342020143326,-7.988827695448E-02) - ); -#893 = DIRECTION('',(0.340781908463,0.939692620786,2.907695487825E-02)); -#894 = ADVANCED_FACE('',(#895),#914,.T.); -#895 = FACE_BOUND('',#896,.T.); -#896 = EDGE_LOOP('',(#897,#906,#912,#913)); -#897 = ORIENTED_EDGE('',*,*,#898,.T.); -#898 = EDGE_CURVE('',#899,#899,#901,.T.); -#899 = VERTEX_POINT('',#900); -#900 = CARTESIAN_POINT('',(-11.6927539352,0.,-48.00951684793)); -#901 = CIRCLE('',#902,4.735522705283); -#902 = AXIS2_PLACEMENT_3D('',#903,#904,#905); -#903 = CARTESIAN_POINT('',(-16.42827664048,0.,-48.00951684793)); -#904 = DIRECTION('',(-0.,1.,0.)); -#905 = DIRECTION('',(1.,0.,0.)); -#906 = ORIENTED_EDGE('',*,*,#907,.T.); -#907 = EDGE_CURVE('',#899,#652,#908,.T.); -#908 = LINE('',#909,#910); -#909 = CARTESIAN_POINT('',(-11.6927539352,-22.,-48.00951684793)); -#910 = VECTOR('',#911,1.); -#911 = DIRECTION('',(0.,1.,0.)); -#912 = ORIENTED_EDGE('',*,*,#651,.F.); -#913 = ORIENTED_EDGE('',*,*,#907,.F.); -#914 = CYLINDRICAL_SURFACE('',#915,4.735522705283); -#915 = AXIS2_PLACEMENT_3D('',#916,#917,#918); -#916 = CARTESIAN_POINT('',(-16.42827664048,-22.,-48.00951684793)); -#917 = DIRECTION('',(0.,1.,0.)); -#918 = DIRECTION('',(1.,0.,0.)); -#919 = ADVANCED_FACE('',(#920),#939,.T.); -#920 = FACE_BOUND('',#921,.T.); -#921 = EDGE_LOOP('',(#922,#931,#937,#938)); -#922 = ORIENTED_EDGE('',*,*,#923,.T.); -#923 = EDGE_CURVE('',#924,#924,#926,.T.); -#924 = VERTEX_POINT('',#925); -#925 = CARTESIAN_POINT('',(21.163799345768,0.,-48.00951684793)); -#926 = CIRCLE('',#927,4.735522705283); -#927 = AXIS2_PLACEMENT_3D('',#928,#929,#930); -#928 = CARTESIAN_POINT('',(16.428276640485,0.,-48.00951684793)); -#929 = DIRECTION('',(-0.,1.,0.)); -#930 = DIRECTION('',(1.,0.,0.)); -#931 = ORIENTED_EDGE('',*,*,#932,.T.); -#932 = EDGE_CURVE('',#924,#663,#933,.T.); -#933 = LINE('',#934,#935); -#934 = CARTESIAN_POINT('',(21.163799345768,-22.,-48.00951684793)); -#935 = VECTOR('',#936,1.); -#936 = DIRECTION('',(0.,1.,0.)); -#937 = ORIENTED_EDGE('',*,*,#662,.F.); -#938 = ORIENTED_EDGE('',*,*,#932,.F.); -#939 = CYLINDRICAL_SURFACE('',#940,4.735522705283); -#940 = AXIS2_PLACEMENT_3D('',#941,#942,#943); -#941 = CARTESIAN_POINT('',(16.428276640485,-22.,-48.00951684793)); -#942 = DIRECTION('',(0.,1.,0.)); -#943 = DIRECTION('',(1.,0.,0.)); -#944 = ADVANCED_FACE('',(#945),#953,.F.); -#945 = FACE_BOUND('',#946,.F.); -#946 = EDGE_LOOP('',(#947,#948,#949,#950,#951,#952)); -#947 = ORIENTED_EDGE('',*,*,#718,.T.); -#948 = ORIENTED_EDGE('',*,*,#687,.T.); -#949 = ORIENTED_EDGE('',*,*,#734,.T.); -#950 = ORIENTED_EDGE('',*,*,#782,.T.); -#951 = ORIENTED_EDGE('',*,*,#806,.T.); -#952 = ORIENTED_EDGE('',*,*,#758,.T.); -#953 = PLANE('',#954); -#954 = AXIS2_PLACEMENT_3D('',#955,#956,#957); -#955 = CARTESIAN_POINT('',(-45.46495478095,0.,-70.97315781796)); -#956 = DIRECTION('',(0.,1.,0.)); -#957 = DIRECTION('',(0.,0.,1.)); -#958 = ADVANCED_FACE('',(#959),#962,.F.); -#959 = FACE_BOUND('',#960,.F.); -#960 = EDGE_LOOP('',(#961)); -#961 = ORIENTED_EDGE('',*,*,#898,.T.); -#962 = PLANE('',#963); -#963 = AXIS2_PLACEMENT_3D('',#964,#965,#966); -#964 = CARTESIAN_POINT('',(-45.46495478095,0.,-70.97315781796)); -#965 = DIRECTION('',(0.,1.,0.)); -#966 = DIRECTION('',(0.,0.,1.)); -#967 = ADVANCED_FACE('',(#968),#971,.F.); -#968 = FACE_BOUND('',#969,.F.); -#969 = EDGE_LOOP('',(#970)); -#970 = ORIENTED_EDGE('',*,*,#923,.T.); -#971 = PLANE('',#972); -#972 = AXIS2_PLACEMENT_3D('',#973,#974,#975); -#973 = CARTESIAN_POINT('',(-45.46495478095,0.,-70.97315781796)); -#974 = DIRECTION('',(0.,1.,0.)); -#975 = DIRECTION('',(0.,0.,1.)); -#976 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#980)) GLOBAL_UNIT_ASSIGNED_CONTEXT -((#977,#978,#979)) REPRESENTATION_CONTEXT('Context #1', - '3D Context with UNIT and UNCERTAINTY') ); -#977 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#978 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#979 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#980 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(5.E-06),#977, - 'distance_accuracy_value','confusion accuracy'); -#981 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7)); -ENDSEC; -END-ISO-10303-21; diff --git a/docs/CAD/design/mate-connectors/BearConnector_Female_Trimmed.stl b/docs/CAD/design/mate-connectors/BearConnector_Female_Trimmed.stl deleted file mode 100644 index 940dab90f7..0000000000 Binary files a/docs/CAD/design/mate-connectors/BearConnector_Female_Trimmed.stl and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/BearConnector_Female_Trimmed_wall.step b/docs/CAD/design/mate-connectors/BearConnector_Female_Trimmed_wall.step deleted file mode 100644 index 14e61e2877..0000000000 --- a/docs/CAD/design/mate-connectors/BearConnector_Female_Trimmed_wall.step +++ /dev/null @@ -1,1604 +0,0 @@ -ISO-10303-21; -HEADER; -FILE_DESCRIPTION(('FreeCAD Model'),'2;1'); -FILE_NAME('Open CASCADE Shape Model','2026-08-05T11:06:23',('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 2', - 'Open CASCADE STEP translator 7.8 2','',(#8)); -#8 = PRODUCT_CONTEXT('',#2,'mechanical'); -#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#10 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#15),#1522); -#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,#291,#322,#346,#370,#395,#419,#444,#468,#492, - #517,#541,#566,#590,#614,#638,#662,#679,#710,#734,#758,#782,#806, - #830,#854,#878,#903,#928,#952,#976,#1001,#1026,#1050,#1067,#1091, - #1221,#1252,#1276,#1300,#1324,#1348,#1365,#1390,#1421,#1437,#1454, - #1465,#1490,#1504,#1513)); -#17 = ADVANCED_FACE('',(#18,#152),#286,.F.); -#18 = FACE_BOUND('',#19,.F.); -#19 = EDGE_LOOP('',(#20,#30,#38,#46,#54,#62,#70,#79,#87,#96,#104,#112, - #121,#129,#138,#146)); -#20 = ORIENTED_EDGE('',*,*,#21,.F.); -#21 = EDGE_CURVE('',#22,#24,#26,.T.); -#22 = VERTEX_POINT('',#23); -#23 = CARTESIAN_POINT('',(28.536336768195,0.,-54.80352892379)); -#24 = VERTEX_POINT('',#25); -#25 = CARTESIAN_POINT('',(43.891390829132,0.,-6.628649129335)); -#26 = LINE('',#27,#28); -#27 = CARTESIAN_POINT('',(30.462276491561,4.440892098501E-16, - -48.76109401628)); -#28 = VECTOR('',#29,1.); -#29 = DIRECTION('',(0.30368282823,0.,0.952773183837)); -#30 = ORIENTED_EDGE('',*,*,#31,.F.); -#31 = EDGE_CURVE('',#32,#22,#34,.T.); -#32 = VERTEX_POINT('',#33); -#33 = CARTESIAN_POINT('',(34.683490155872,0.,-60.75238354821)); -#34 = LINE('',#35,#36); -#35 = CARTESIAN_POINT('',(13.469833011847,0.,-40.22304997814)); -#36 = VECTOR('',#37,1.); -#37 = DIRECTION('',(-0.718602345805,0.,0.695421216677)); -#38 = ORIENTED_EDGE('',*,*,#39,.F.); -#39 = EDGE_CURVE('',#40,#32,#42,.T.); -#40 = VERTEX_POINT('',#41); -#41 = CARTESIAN_POINT('',(26.747616824317,0.,-68.95279080543)); -#42 = LINE('',#43,#44); -#43 = CARTESIAN_POINT('',(12.749348137648,0.,-83.41767694094)); -#44 = VECTOR('',#45,1.); -#45 = DIRECTION('',(0.695421216677,0.,0.718602345805)); -#46 = ORIENTED_EDGE('',*,*,#47,.F.); -#47 = EDGE_CURVE('',#48,#40,#50,.T.); -#48 = VERTEX_POINT('',#49); -#49 = CARTESIAN_POINT('',(19.965518143439,0.,-62.38947335481)); -#50 = LINE('',#51,#52); -#51 = CARTESIAN_POINT('',(8.607536374131,0.,-51.39788454757)); -#52 = VECTOR('',#53,1.); -#53 = DIRECTION('',(0.718602345805,0.,-0.695421216677)); -#54 = ORIENTED_EDGE('',*,*,#55,.F.); -#55 = EDGE_CURVE('',#56,#48,#58,.T.); -#56 = VERTEX_POINT('',#57); -#57 = CARTESIAN_POINT('',(-19.96551814343,0.,-62.38947335481)); -#58 = LINE('',#59,#60); -#59 = CARTESIAN_POINT('',(-12.74971831875,0.,-62.38947335481)); -#60 = VECTOR('',#61,1.); -#61 = DIRECTION('',(1.,0.,0.)); -#62 = ORIENTED_EDGE('',*,*,#63,.T.); -#63 = EDGE_CURVE('',#56,#64,#66,.T.); -#64 = VERTEX_POINT('',#65); -#65 = CARTESIAN_POINT('',(-24.55693735351,0.,-66.83277965903)); -#66 = LINE('',#67,#68); -#67 = CARTESIAN_POINT('',(-30.98981424001,0.,-73.05814073287)); -#68 = VECTOR('',#69,1.); -#69 = DIRECTION('',(-0.718602345805,0.,-0.695421216677)); -#70 = ORIENTED_EDGE('',*,*,#71,.F.); -#71 = EDGE_CURVE('',#72,#64,#74,.T.); -#72 = VERTEX_POINT('',#73); -#73 = CARTESIAN_POINT('',(-28.86762797071,0.,-66.76211133463)); -#74 = CIRCLE('',#75,3.048528137424); -#75 = AXIS2_PLACEMENT_3D('',#76,#77,#78); -#76 = CARTESIAN_POINT('',(-26.67694849991,0.,-64.64210018823)); -#77 = DIRECTION('',(0.,-1.,0.)); -#78 = DIRECTION('',(1.,0.,0.)); -#79 = ORIENTED_EDGE('',*,*,#80,.T.); -#80 = EDGE_CURVE('',#72,#81,#83,.T.); -#81 = VERTEX_POINT('',#82); -#82 = CARTESIAN_POINT('',(-30.0937101258,0.,-65.49515903052)); -#83 = LINE('',#84,#85); -#84 = CARTESIAN_POINT('',(-32.44179530228,0.,-63.0688029236)); -#85 = VECTOR('',#86,1.); -#86 = DIRECTION('',(-0.695421216677,0.,0.718602345805)); -#87 = ORIENTED_EDGE('',*,*,#88,.F.); -#88 = EDGE_CURVE('',#89,#81,#91,.T.); -#89 = VERTEX_POINT('',#90); -#90 = CARTESIAN_POINT('',(-29.94071467356,0.,-56.16260351814)); -#91 = CIRCLE('',#92,6.6); -#92 = AXIS2_PLACEMENT_3D('',#93,#94,#95); -#93 = CARTESIAN_POINT('',(-25.35093464349,0.,-60.90537900045)); -#94 = DIRECTION('',(0.,-1.,0.)); -#95 = DIRECTION('',(1.,0.,0.)); -#96 = ORIENTED_EDGE('',*,*,#97,.T.); -#97 = EDGE_CURVE('',#89,#98,#100,.T.); -#98 = VERTEX_POINT('',#99); -#99 = CARTESIAN_POINT('',(-28.53633676819,0.,-54.80352892379)); -#100 = LINE('',#101,#102); -#101 = CARTESIAN_POINT('',(-36.94745061313,0.,-62.94331173664)); -#102 = VECTOR('',#103,1.); -#103 = DIRECTION('',(0.718602345805,0.,0.695421216677)); -#104 = ORIENTED_EDGE('',*,*,#105,.T.); -#105 = EDGE_CURVE('',#98,#106,#108,.T.); -#106 = VERTEX_POINT('',#107); -#107 = CARTESIAN_POINT('',(-42.51292396981,0.,-10.95344505546)); -#108 = LINE('',#109,#110); -#109 = CARTESIAN_POINT('',(-33.96596941491,4.440892098501E-16, - -37.76862308461)); -#110 = VECTOR('',#111,1.); -#111 = DIRECTION('',(-0.30368282823,0.,0.952773183837)); -#112 = ORIENTED_EDGE('',*,*,#113,.F.); -#113 = EDGE_CURVE('',#114,#106,#116,.T.); -#114 = VERTEX_POINT('',#115); -#115 = CARTESIAN_POINT('',(-40.68171555857,0.,-3.418973858775)); -#116 = CIRCLE('',#117,7.454044962965); -#117 = AXIS2_PLACEMENT_3D('',#118,#119,#120); -#118 = CARTESIAN_POINT('',(-35.41090981799,0.,-8.689779599357)); -#119 = DIRECTION('',(0.,-1.,0.)); -#120 = DIRECTION('',(1.,0.,0.)); -#121 = ORIENTED_EDGE('',*,*,#122,.T.); -#122 = EDGE_CURVE('',#114,#123,#125,.T.); -#123 = VERTEX_POINT('',#124); -#124 = CARTESIAN_POINT('',(-37.84598092218,0.,-0.583239222383)); -#125 = LINE('',#126,#127); -#126 = CARTESIAN_POINT('',(-57.34820403576,0.,-20.08546233597)); -#127 = VECTOR('',#128,1.); -#128 = DIRECTION('',(0.707106781187,0.,0.707106781187)); -#129 = ORIENTED_EDGE('',*,*,#130,.F.); -#130 = EDGE_CURVE('',#131,#123,#133,.T.); -#131 = VERTEX_POINT('',#132); -#132 = CARTESIAN_POINT('',(-32.57517518159,0.,1.6)); -#133 = CIRCLE('',#134,7.454044962965); -#134 = AXIS2_PLACEMENT_3D('',#135,#136,#137); -#135 = CARTESIAN_POINT('',(-32.57517518159,0.,-5.854044962965)); -#136 = DIRECTION('',(0.,-1.,0.)); -#137 = DIRECTION('',(1.,0.,0.)); -#138 = ORIENTED_EDGE('',*,*,#139,.T.); -#139 = EDGE_CURVE('',#131,#140,#142,.T.); -#140 = VERTEX_POINT('',#141); -#141 = CARTESIAN_POINT('',(35.662741699797,0.,1.6)); -#142 = LINE('',#143,#144); -#143 = CARTESIAN_POINT('',(-4.901106540577,0.,1.6)); -#144 = VECTOR('',#145,1.); -#145 = DIRECTION('',(1.,0.,0.)); -#146 = ORIENTED_EDGE('',*,*,#147,.F.); -#147 = EDGE_CURVE('',#24,#140,#148,.T.); -#148 = LINE('',#149,#150); -#149 = CARTESIAN_POINT('',(33.5241070341,0.,3.738634665697)); -#150 = VECTOR('',#151,1.); -#151 = DIRECTION('',(-0.707106781187,0.,0.707106781187)); -#152 = FACE_BOUND('',#153,.F.); -#153 = EDGE_LOOP('',(#154,#164,#172,#180,#188,#197,#205,#214,#222,#230, - #239,#247,#256,#264,#272,#280)); -#154 = ORIENTED_EDGE('',*,*,#155,.T.); -#155 = EDGE_CURVE('',#156,#158,#160,.T.); -#156 = VERTEX_POINT('',#157); -#157 = CARTESIAN_POINT('',(32.703857168398,0.,-60.78483712899)); -#158 = VERTEX_POINT('',#159); -#159 = CARTESIAN_POINT('',(26.938753236523,0.,-55.20570756731)); -#160 = LINE('',#161,#162); -#161 = CARTESIAN_POINT('',(13.567306766147,0.,-42.26560567724)); -#162 = VECTOR('',#163,1.); -#163 = DIRECTION('',(-0.718602345805,0.,0.695421216677)); -#164 = ORIENTED_EDGE('',*,*,#165,.T.); -#165 = EDGE_CURVE('',#158,#166,#168,.T.); -#166 = VERTEX_POINT('',#167); -#167 = CARTESIAN_POINT('',(42.298608189024,0.,-7.015765476549)); -#168 = LINE('',#169,#170); -#169 = CARTESIAN_POINT('',(25.140315874087,-4.440892098501E-16, - -60.84811712245)); -#170 = VECTOR('',#171,1.); -#171 = DIRECTION('',(0.30368282823,0.,0.952773183837)); -#172 = ORIENTED_EDGE('',*,*,#173,.T.); -#173 = EDGE_CURVE('',#166,#174,#176,.T.); -#174 = VERTEX_POINT('',#175); -#175 = CARTESIAN_POINT('',(35.082842712475,0.,0.2)); -#176 = LINE('',#177,#178); -#177 = CARTESIAN_POINT('',(34.536239068456,0.,0.746603644019)); -#178 = VECTOR('',#179,1.); -#179 = DIRECTION('',(-0.707106781187,0.,0.707106781187)); -#180 = ORIENTED_EDGE('',*,*,#181,.F.); -#181 = EDGE_CURVE('',#182,#174,#184,.T.); -#182 = VERTEX_POINT('',#183); -#183 = CARTESIAN_POINT('',(-32.57517518159,0.,0.2)); -#184 = LINE('',#185,#186); -#185 = CARTESIAN_POINT('',(-30.87627118587,0.,0.2)); -#186 = VECTOR('',#187,1.); -#187 = DIRECTION('',(1.,0.,0.)); -#188 = ORIENTED_EDGE('',*,*,#189,.T.); -#189 = EDGE_CURVE('',#182,#190,#192,.T.); -#190 = VERTEX_POINT('',#191); -#191 = CARTESIAN_POINT('',(-36.85603142851,0.,-1.573188716044)); -#192 = CIRCLE('',#193,6.054044962965); -#193 = AXIS2_PLACEMENT_3D('',#194,#195,#196); -#194 = CARTESIAN_POINT('',(-32.57517518159,0.,-5.854044962965)); -#195 = DIRECTION('',(0.,-1.,0.)); -#196 = DIRECTION('',(-1.,0.,0.)); -#197 = ORIENTED_EDGE('',*,*,#198,.F.); -#198 = EDGE_CURVE('',#199,#190,#201,.T.); -#199 = VERTEX_POINT('',#200); -#200 = CARTESIAN_POINT('',(-39.69176606491,0.,-4.408923352436)); -#201 = LINE('',#202,#203); -#202 = CARTESIAN_POINT('',(-57.0671882012,0.,-21.78434548873)); -#203 = VECTOR('',#204,1.); -#204 = DIRECTION('',(0.707106781187,0.,0.707106781187)); -#205 = ORIENTED_EDGE('',*,*,#206,.T.); -#206 = EDGE_CURVE('',#199,#207,#209,.T.); -#207 = VERTEX_POINT('',#208); -#208 = CARTESIAN_POINT('',(-41.17904151244,0.,-10.52828909594)); -#209 = CIRCLE('',#210,6.054044962965); -#210 = AXIS2_PLACEMENT_3D('',#211,#212,#213); -#211 = CARTESIAN_POINT('',(-35.41090981799,0.,-8.689779599357)); -#212 = DIRECTION('',(0.,-1.,0.)); -#213 = DIRECTION('',(-1.,0.,0.)); -#214 = ORIENTED_EDGE('',*,*,#215,.F.); -#215 = EDGE_CURVE('',#216,#207,#218,.T.); -#216 = VERTEX_POINT('',#217); -#217 = CARTESIAN_POINT('',(-26.93875323652,0.,-55.20570756731)); -#218 = LINE('',#219,#220); -#219 = CARTESIAN_POINT('',(-29.06259699304,-4.440892098501E-16, - -48.54236940733)); -#220 = VECTOR('',#221,1.); -#221 = DIRECTION('',(-0.30368282823,0.,0.952773183837)); -#222 = ORIENTED_EDGE('',*,*,#223,.F.); -#223 = EDGE_CURVE('',#224,#216,#226,.T.); -#224 = VERTEX_POINT('',#225); -#225 = CARTESIAN_POINT('',(-28.96712497021,0.,-57.16864680227)); -#226 = LINE('',#227,#228); -#227 = CARTESIAN_POINT('',(-36.14667143517,0.,-64.11659091489)); -#228 = VECTOR('',#229,1.); -#229 = DIRECTION('',(0.718602345805,0.,0.695421216677)); -#230 = ORIENTED_EDGE('',*,*,#231,.T.); -#231 = EDGE_CURVE('',#224,#232,#234,.T.); -#232 = VERTEX_POINT('',#233); -#233 = CARTESIAN_POINT('',(-29.08766684168,0.,-64.52156932717)); -#234 = CIRCLE('',#235,5.2); -#235 = AXIS2_PLACEMENT_3D('',#236,#237,#238); -#236 = CARTESIAN_POINT('',(-25.35093464349,0.,-60.90537900045)); -#237 = DIRECTION('',(0.,-1.,0.)); -#238 = DIRECTION('',(-1.,0.,0.)); -#239 = ORIENTED_EDGE('',*,*,#240,.F.); -#240 = EDGE_CURVE('',#241,#232,#243,.T.); -#241 = VERTEX_POINT('',#242); -#242 = CARTESIAN_POINT('',(-27.86158468659,0.,-65.78852163128)); -#243 = LINE('',#244,#245); -#244 = CARTESIAN_POINT('',(-31.12923147938,0.,-62.41195129628)); -#245 = VECTOR('',#246,1.); -#246 = DIRECTION('',(-0.695421216677,0.,0.718602345805)); -#247 = ORIENTED_EDGE('',*,*,#248,.T.); -#248 = EDGE_CURVE('',#241,#249,#251,.T.); -#249 = VERTEX_POINT('',#250); -#250 = CARTESIAN_POINT('',(-25.53052705686,0.,-65.82673637491)); -#251 = CIRCLE('',#252,1.648528137424); -#252 = AXIS2_PLACEMENT_3D('',#253,#254,#255); -#253 = CARTESIAN_POINT('',(-26.67694849991,0.,-64.64210018823)); -#254 = DIRECTION('',(0.,-1.,0.)); -#255 = DIRECTION('',(-1.,0.,0.)); -#256 = ORIENTED_EDGE('',*,*,#257,.F.); -#257 = EDGE_CURVE('',#258,#249,#260,.T.); -#258 = VERTEX_POINT('',#259); -#259 = CARTESIAN_POINT('',(-20.53201900137,0.,-60.98947335481)); -#260 = LINE('',#261,#262); -#261 = CARTESIAN_POINT('',(-30.69923804215,0.,-70.82871181101)); -#262 = VECTOR('',#263,1.); -#263 = DIRECTION('',(-0.718602345805,0.,-0.695421216677)); -#264 = ORIENTED_EDGE('',*,*,#265,.T.); -#265 = EDGE_CURVE('',#258,#266,#268,.T.); -#266 = VERTEX_POINT('',#267); -#267 = CARTESIAN_POINT('',(20.532019001374,0.,-60.98947335481)); -#268 = LINE('',#269,#270); -#269 = CARTESIAN_POINT('',(-17.57924046663,0.,-60.98947335481)); -#270 = VECTOR('',#271,1.); -#271 = DIRECTION('',(1.,0.,0.)); -#272 = ORIENTED_EDGE('',*,*,#273,.T.); -#273 = EDGE_CURVE('',#266,#274,#276,.T.); -#274 = VERTEX_POINT('',#275); -#275 = CARTESIAN_POINT('',(26.715163243538,0.,-66.97315781796)); -#276 = LINE('',#277,#278); -#277 = CARTESIAN_POINT('',(7.481849370247,-4.440892098501E-16, - -48.36028435244)); -#278 = VECTOR('',#279,1.); -#279 = DIRECTION('',(0.718602345805,0.,-0.695421216677)); -#280 = ORIENTED_EDGE('',*,*,#281,.T.); -#281 = EDGE_CURVE('',#274,#156,#282,.T.); -#282 = LINE('',#283,#284); -#283 = CARTESIAN_POINT('',(9.75933652063,0.,-84.4941890519)); -#284 = VECTOR('',#285,1.); -#285 = DIRECTION('',(0.695421216677,0.,0.718602345805)); -#286 = PLANE('',#287); -#287 = AXIS2_PLACEMENT_3D('',#288,#289,#290); -#288 = CARTESIAN_POINT('',(-45.46495478095,0.,-70.97315781796)); -#289 = DIRECTION('',(0.,1.,0.)); -#290 = DIRECTION('',(0.,0.,1.)); -#291 = ADVANCED_FACE('',(#292),#317,.F.); -#292 = FACE_BOUND('',#293,.F.); -#293 = EDGE_LOOP('',(#294,#295,#303,#311)); -#294 = ORIENTED_EDGE('',*,*,#21,.T.); -#295 = ORIENTED_EDGE('',*,*,#296,.T.); -#296 = EDGE_CURVE('',#24,#297,#299,.T.); -#297 = VERTEX_POINT('',#298); -#298 = CARTESIAN_POINT('',(43.891390829132,20.8572529153,-6.628649129335 - )); -#299 = LINE('',#300,#301); -#300 = CARTESIAN_POINT('',(43.891390829132,-5.,-6.628649129335)); -#301 = VECTOR('',#302,1.); -#302 = DIRECTION('',(0.,1.,0.)); -#303 = ORIENTED_EDGE('',*,*,#304,.F.); -#304 = EDGE_CURVE('',#305,#297,#307,.T.); -#305 = VERTEX_POINT('',#306); -#306 = CARTESIAN_POINT('',(28.536336768195,20.8572529153,-54.80352892379 - )); -#307 = LINE('',#308,#309); -#308 = CARTESIAN_POINT('',(30.462276491561,20.8572529153,-48.76109401628 - )); -#309 = VECTOR('',#310,1.); -#310 = DIRECTION('',(0.30368282823,0.,0.952773183837)); -#311 = ORIENTED_EDGE('',*,*,#312,.F.); -#312 = EDGE_CURVE('',#22,#305,#313,.T.); -#313 = LINE('',#314,#315); -#314 = CARTESIAN_POINT('',(28.536336768195,-5.,-54.80352892379)); -#315 = VECTOR('',#316,1.); -#316 = DIRECTION('',(0.,1.,0.)); -#317 = PLANE('',#318); -#318 = AXIS2_PLACEMENT_3D('',#319,#320,#321); -#319 = CARTESIAN_POINT('',(43.891390829132,-5.,-6.628649129335)); -#320 = DIRECTION('',(-0.952773183837,0.,0.30368282823)); -#321 = DIRECTION('',(-0.30368282823,0.,-0.952773183837)); -#322 = ADVANCED_FACE('',(#323),#341,.F.); -#323 = FACE_BOUND('',#324,.F.); -#324 = EDGE_LOOP('',(#325,#326,#334,#340)); -#325 = ORIENTED_EDGE('',*,*,#147,.T.); -#326 = ORIENTED_EDGE('',*,*,#327,.T.); -#327 = EDGE_CURVE('',#140,#328,#330,.T.); -#328 = VERTEX_POINT('',#329); -#329 = CARTESIAN_POINT('',(35.662741699797,20.8572529153,1.6)); -#330 = LINE('',#331,#332); -#331 = CARTESIAN_POINT('',(35.662741699797,-5.,1.6)); -#332 = VECTOR('',#333,1.); -#333 = DIRECTION('',(0.,1.,0.)); -#334 = ORIENTED_EDGE('',*,*,#335,.F.); -#335 = EDGE_CURVE('',#297,#328,#336,.T.); -#336 = LINE('',#337,#338); -#337 = CARTESIAN_POINT('',(33.5241070341,20.8572529153,3.738634665697)); -#338 = VECTOR('',#339,1.); -#339 = DIRECTION('',(-0.707106781187,0.,0.707106781187)); -#340 = ORIENTED_EDGE('',*,*,#296,.F.); -#341 = PLANE('',#342); -#342 = AXIS2_PLACEMENT_3D('',#343,#344,#345); -#343 = CARTESIAN_POINT('',(35.662741699797,-5.,1.6)); -#344 = DIRECTION('',(-0.707106781187,0.,-0.707106781187)); -#345 = DIRECTION('',(0.707106781187,0.,-0.707106781187)); -#346 = ADVANCED_FACE('',(#347),#365,.T.); -#347 = FACE_BOUND('',#348,.T.); -#348 = EDGE_LOOP('',(#349,#350,#351,#359)); -#349 = ORIENTED_EDGE('',*,*,#139,.T.); -#350 = ORIENTED_EDGE('',*,*,#327,.T.); -#351 = ORIENTED_EDGE('',*,*,#352,.F.); -#352 = EDGE_CURVE('',#353,#328,#355,.T.); -#353 = VERTEX_POINT('',#354); -#354 = CARTESIAN_POINT('',(-32.57517518159,20.8572529153,1.6)); -#355 = LINE('',#356,#357); -#356 = CARTESIAN_POINT('',(-4.901106540577,20.8572529153,1.6)); -#357 = VECTOR('',#358,1.); -#358 = DIRECTION('',(1.,0.,0.)); -#359 = ORIENTED_EDGE('',*,*,#360,.F.); -#360 = EDGE_CURVE('',#131,#353,#361,.T.); -#361 = LINE('',#362,#363); -#362 = CARTESIAN_POINT('',(-32.57517518159,-5.,1.6)); -#363 = VECTOR('',#364,1.); -#364 = DIRECTION('',(0.,1.,0.)); -#365 = PLANE('',#366); -#366 = AXIS2_PLACEMENT_3D('',#367,#368,#369); -#367 = CARTESIAN_POINT('',(35.662741699797,-5.,1.6)); -#368 = DIRECTION('',(0.,0.,1.)); -#369 = DIRECTION('',(-1.,0.,0.)); -#370 = ADVANCED_FACE('',(#371),#390,.T.); -#371 = FACE_BOUND('',#372,.F.); -#372 = EDGE_LOOP('',(#373,#374,#382,#389)); -#373 = ORIENTED_EDGE('',*,*,#130,.T.); -#374 = ORIENTED_EDGE('',*,*,#375,.T.); -#375 = EDGE_CURVE('',#123,#376,#378,.T.); -#376 = VERTEX_POINT('',#377); -#377 = CARTESIAN_POINT('',(-37.84598092218,20.8572529153,-0.583239222383 - )); -#378 = LINE('',#379,#380); -#379 = CARTESIAN_POINT('',(-37.84598092218,-5.,-0.583239222383)); -#380 = VECTOR('',#381,1.); -#381 = DIRECTION('',(0.,1.,0.)); -#382 = ORIENTED_EDGE('',*,*,#383,.F.); -#383 = EDGE_CURVE('',#353,#376,#384,.T.); -#384 = CIRCLE('',#385,7.454044962965); -#385 = AXIS2_PLACEMENT_3D('',#386,#387,#388); -#386 = CARTESIAN_POINT('',(-32.57517518159,20.8572529153,-5.854044962965 - )); -#387 = DIRECTION('',(0.,-1.,0.)); -#388 = DIRECTION('',(1.,0.,0.)); -#389 = ORIENTED_EDGE('',*,*,#360,.F.); -#390 = CYLINDRICAL_SURFACE('',#391,7.454044962965); -#391 = AXIS2_PLACEMENT_3D('',#392,#393,#394); -#392 = CARTESIAN_POINT('',(-32.57517518159,-5.,-5.854044962965)); -#393 = DIRECTION('',(0.,-1.,0.)); -#394 = DIRECTION('',(1.,0.,0.)); -#395 = ADVANCED_FACE('',(#396),#414,.T.); -#396 = FACE_BOUND('',#397,.T.); -#397 = EDGE_LOOP('',(#398,#399,#400,#408)); -#398 = ORIENTED_EDGE('',*,*,#122,.T.); -#399 = ORIENTED_EDGE('',*,*,#375,.T.); -#400 = ORIENTED_EDGE('',*,*,#401,.F.); -#401 = EDGE_CURVE('',#402,#376,#404,.T.); -#402 = VERTEX_POINT('',#403); -#403 = CARTESIAN_POINT('',(-40.68171555857,20.8572529153,-3.418973858775 - )); -#404 = LINE('',#405,#406); -#405 = CARTESIAN_POINT('',(-57.34820403576,20.8572529153,-20.08546233597 - )); -#406 = VECTOR('',#407,1.); -#407 = DIRECTION('',(0.707106781187,0.,0.707106781187)); -#408 = ORIENTED_EDGE('',*,*,#409,.F.); -#409 = EDGE_CURVE('',#114,#402,#410,.T.); -#410 = LINE('',#411,#412); -#411 = CARTESIAN_POINT('',(-40.68171555857,-5.,-3.418973858775)); -#412 = VECTOR('',#413,1.); -#413 = DIRECTION('',(0.,1.,0.)); -#414 = PLANE('',#415); -#415 = AXIS2_PLACEMENT_3D('',#416,#417,#418); -#416 = CARTESIAN_POINT('',(-37.84598092218,-5.,-0.583239222383)); -#417 = DIRECTION('',(-0.707106781187,0.,0.707106781187)); -#418 = DIRECTION('',(-0.707106781187,0.,-0.707106781187)); -#419 = ADVANCED_FACE('',(#420),#439,.T.); -#420 = FACE_BOUND('',#421,.F.); -#421 = EDGE_LOOP('',(#422,#423,#431,#438)); -#422 = ORIENTED_EDGE('',*,*,#113,.T.); -#423 = ORIENTED_EDGE('',*,*,#424,.T.); -#424 = EDGE_CURVE('',#106,#425,#427,.T.); -#425 = VERTEX_POINT('',#426); -#426 = CARTESIAN_POINT('',(-42.51292396981,20.8572529153,-10.95344505546 - )); -#427 = LINE('',#428,#429); -#428 = CARTESIAN_POINT('',(-42.51292396981,-5.,-10.95344505546)); -#429 = VECTOR('',#430,1.); -#430 = DIRECTION('',(0.,1.,0.)); -#431 = ORIENTED_EDGE('',*,*,#432,.F.); -#432 = EDGE_CURVE('',#402,#425,#433,.T.); -#433 = CIRCLE('',#434,7.454044962965); -#434 = AXIS2_PLACEMENT_3D('',#435,#436,#437); -#435 = CARTESIAN_POINT('',(-35.41090981799,20.8572529153,-8.689779599357 - )); -#436 = DIRECTION('',(0.,-1.,0.)); -#437 = DIRECTION('',(1.,0.,0.)); -#438 = ORIENTED_EDGE('',*,*,#409,.F.); -#439 = CYLINDRICAL_SURFACE('',#440,7.454044962965); -#440 = AXIS2_PLACEMENT_3D('',#441,#442,#443); -#441 = CARTESIAN_POINT('',(-35.41090981799,-5.,-8.689779599357)); -#442 = DIRECTION('',(0.,-1.,0.)); -#443 = DIRECTION('',(1.,0.,0.)); -#444 = ADVANCED_FACE('',(#445),#463,.T.); -#445 = FACE_BOUND('',#446,.T.); -#446 = EDGE_LOOP('',(#447,#448,#449,#457)); -#447 = ORIENTED_EDGE('',*,*,#105,.T.); -#448 = ORIENTED_EDGE('',*,*,#424,.T.); -#449 = ORIENTED_EDGE('',*,*,#450,.F.); -#450 = EDGE_CURVE('',#451,#425,#453,.T.); -#451 = VERTEX_POINT('',#452); -#452 = CARTESIAN_POINT('',(-28.53633676819,20.8572529153,-54.80352892379 - )); -#453 = LINE('',#454,#455); -#454 = CARTESIAN_POINT('',(-33.96596941491,20.8572529153,-37.76862308461 - )); -#455 = VECTOR('',#456,1.); -#456 = DIRECTION('',(-0.30368282823,0.,0.952773183837)); -#457 = ORIENTED_EDGE('',*,*,#458,.F.); -#458 = EDGE_CURVE('',#98,#451,#459,.T.); -#459 = LINE('',#460,#461); -#460 = CARTESIAN_POINT('',(-28.53633676819,-5.,-54.80352892379)); -#461 = VECTOR('',#462,1.); -#462 = DIRECTION('',(0.,1.,0.)); -#463 = PLANE('',#464); -#464 = AXIS2_PLACEMENT_3D('',#465,#466,#467); -#465 = CARTESIAN_POINT('',(-42.51292396981,-5.,-10.95344505546)); -#466 = DIRECTION('',(-0.952773183837,0.,-0.30368282823)); -#467 = DIRECTION('',(0.30368282823,0.,-0.952773183837)); -#468 = ADVANCED_FACE('',(#469),#487,.T.); -#469 = FACE_BOUND('',#470,.T.); -#470 = EDGE_LOOP('',(#471,#472,#473,#481)); -#471 = ORIENTED_EDGE('',*,*,#97,.T.); -#472 = ORIENTED_EDGE('',*,*,#458,.T.); -#473 = ORIENTED_EDGE('',*,*,#474,.F.); -#474 = EDGE_CURVE('',#475,#451,#477,.T.); -#475 = VERTEX_POINT('',#476); -#476 = CARTESIAN_POINT('',(-29.94071467356,20.8572529153,-56.16260351814 - )); -#477 = LINE('',#478,#479); -#478 = CARTESIAN_POINT('',(-36.94745061313,20.8572529153,-62.94331173664 - )); -#479 = VECTOR('',#480,1.); -#480 = DIRECTION('',(0.718602345805,0.,0.695421216677)); -#481 = ORIENTED_EDGE('',*,*,#482,.F.); -#482 = EDGE_CURVE('',#89,#475,#483,.T.); -#483 = LINE('',#484,#485); -#484 = CARTESIAN_POINT('',(-29.94071467356,-5.,-56.16260351814)); -#485 = VECTOR('',#486,1.); -#486 = DIRECTION('',(0.,1.,0.)); -#487 = PLANE('',#488); -#488 = AXIS2_PLACEMENT_3D('',#489,#490,#491); -#489 = CARTESIAN_POINT('',(-28.53633676819,-5.,-54.80352892379)); -#490 = DIRECTION('',(-0.695421216677,0.,0.718602345805)); -#491 = DIRECTION('',(-0.718602345805,0.,-0.695421216677)); -#492 = ADVANCED_FACE('',(#493),#512,.T.); -#493 = FACE_BOUND('',#494,.F.); -#494 = EDGE_LOOP('',(#495,#496,#504,#511)); -#495 = ORIENTED_EDGE('',*,*,#88,.T.); -#496 = ORIENTED_EDGE('',*,*,#497,.T.); -#497 = EDGE_CURVE('',#81,#498,#500,.T.); -#498 = VERTEX_POINT('',#499); -#499 = CARTESIAN_POINT('',(-30.0937101258,20.8572529153,-65.49515903052) - ); -#500 = LINE('',#501,#502); -#501 = CARTESIAN_POINT('',(-30.0937101258,-5.,-65.49515903052)); -#502 = VECTOR('',#503,1.); -#503 = DIRECTION('',(0.,1.,0.)); -#504 = ORIENTED_EDGE('',*,*,#505,.F.); -#505 = EDGE_CURVE('',#475,#498,#506,.T.); -#506 = CIRCLE('',#507,6.6); -#507 = AXIS2_PLACEMENT_3D('',#508,#509,#510); -#508 = CARTESIAN_POINT('',(-25.35093464349,20.8572529153,-60.90537900045 - )); -#509 = DIRECTION('',(0.,-1.,0.)); -#510 = DIRECTION('',(1.,0.,0.)); -#511 = ORIENTED_EDGE('',*,*,#482,.F.); -#512 = CYLINDRICAL_SURFACE('',#513,6.6); -#513 = AXIS2_PLACEMENT_3D('',#514,#515,#516); -#514 = CARTESIAN_POINT('',(-25.35093464349,-5.,-60.90537900045)); -#515 = DIRECTION('',(0.,-1.,0.)); -#516 = DIRECTION('',(1.,0.,0.)); -#517 = ADVANCED_FACE('',(#518),#536,.T.); -#518 = FACE_BOUND('',#519,.T.); -#519 = EDGE_LOOP('',(#520,#521,#522,#530)); -#520 = ORIENTED_EDGE('',*,*,#80,.T.); -#521 = ORIENTED_EDGE('',*,*,#497,.T.); -#522 = ORIENTED_EDGE('',*,*,#523,.F.); -#523 = EDGE_CURVE('',#524,#498,#526,.T.); -#524 = VERTEX_POINT('',#525); -#525 = CARTESIAN_POINT('',(-28.86762797071,20.8572529153,-66.76211133463 - )); -#526 = LINE('',#527,#528); -#527 = CARTESIAN_POINT('',(-32.44179530228,20.8572529153,-63.0688029236) - ); -#528 = VECTOR('',#529,1.); -#529 = DIRECTION('',(-0.695421216677,0.,0.718602345805)); -#530 = ORIENTED_EDGE('',*,*,#531,.F.); -#531 = EDGE_CURVE('',#72,#524,#532,.T.); -#532 = LINE('',#533,#534); -#533 = CARTESIAN_POINT('',(-28.86762797071,-5.,-66.76211133463)); -#534 = VECTOR('',#535,1.); -#535 = DIRECTION('',(0.,1.,0.)); -#536 = PLANE('',#537); -#537 = AXIS2_PLACEMENT_3D('',#538,#539,#540); -#538 = CARTESIAN_POINT('',(-30.0937101258,-5.,-65.49515903052)); -#539 = DIRECTION('',(-0.718602345805,0.,-0.695421216677)); -#540 = DIRECTION('',(0.695421216677,0.,-0.718602345805)); -#541 = ADVANCED_FACE('',(#542),#561,.T.); -#542 = FACE_BOUND('',#543,.F.); -#543 = EDGE_LOOP('',(#544,#545,#553,#560)); -#544 = ORIENTED_EDGE('',*,*,#71,.T.); -#545 = ORIENTED_EDGE('',*,*,#546,.T.); -#546 = EDGE_CURVE('',#64,#547,#549,.T.); -#547 = VERTEX_POINT('',#548); -#548 = CARTESIAN_POINT('',(-24.55693735351,20.8572529153,-66.83277965903 - )); -#549 = LINE('',#550,#551); -#550 = CARTESIAN_POINT('',(-24.55693735351,-5.,-66.83277965903)); -#551 = VECTOR('',#552,1.); -#552 = DIRECTION('',(0.,1.,0.)); -#553 = ORIENTED_EDGE('',*,*,#554,.F.); -#554 = EDGE_CURVE('',#524,#547,#555,.T.); -#555 = CIRCLE('',#556,3.048528137424); -#556 = AXIS2_PLACEMENT_3D('',#557,#558,#559); -#557 = CARTESIAN_POINT('',(-26.67694849991,20.8572529153,-64.64210018823 - )); -#558 = DIRECTION('',(0.,-1.,0.)); -#559 = DIRECTION('',(1.,0.,0.)); -#560 = ORIENTED_EDGE('',*,*,#531,.F.); -#561 = CYLINDRICAL_SURFACE('',#562,3.048528137424); -#562 = AXIS2_PLACEMENT_3D('',#563,#564,#565); -#563 = CARTESIAN_POINT('',(-26.67694849991,-5.,-64.64210018823)); -#564 = DIRECTION('',(0.,-1.,0.)); -#565 = DIRECTION('',(1.,0.,0.)); -#566 = ADVANCED_FACE('',(#567),#585,.T.); -#567 = FACE_BOUND('',#568,.T.); -#568 = EDGE_LOOP('',(#569,#570,#571,#579)); -#569 = ORIENTED_EDGE('',*,*,#63,.T.); -#570 = ORIENTED_EDGE('',*,*,#546,.T.); -#571 = ORIENTED_EDGE('',*,*,#572,.F.); -#572 = EDGE_CURVE('',#573,#547,#575,.T.); -#573 = VERTEX_POINT('',#574); -#574 = CARTESIAN_POINT('',(-19.96551814343,20.8572529153,-62.38947335481 - )); -#575 = LINE('',#576,#577); -#576 = CARTESIAN_POINT('',(-30.98981424001,20.8572529153,-73.05814073287 - )); -#577 = VECTOR('',#578,1.); -#578 = DIRECTION('',(-0.718602345805,0.,-0.695421216677)); -#579 = ORIENTED_EDGE('',*,*,#580,.F.); -#580 = EDGE_CURVE('',#56,#573,#581,.T.); -#581 = LINE('',#582,#583); -#582 = CARTESIAN_POINT('',(-19.96551814343,-5.,-62.38947335481)); -#583 = VECTOR('',#584,1.); -#584 = DIRECTION('',(0.,1.,0.)); -#585 = PLANE('',#586); -#586 = AXIS2_PLACEMENT_3D('',#587,#588,#589); -#587 = CARTESIAN_POINT('',(-24.55693735351,-5.,-66.83277965903)); -#588 = DIRECTION('',(0.695421216677,0.,-0.718602345805)); -#589 = DIRECTION('',(0.718602345805,0.,0.695421216677)); -#590 = ADVANCED_FACE('',(#591),#609,.F.); -#591 = FACE_BOUND('',#592,.F.); -#592 = EDGE_LOOP('',(#593,#594,#602,#608)); -#593 = ORIENTED_EDGE('',*,*,#55,.T.); -#594 = ORIENTED_EDGE('',*,*,#595,.T.); -#595 = EDGE_CURVE('',#48,#596,#598,.T.); -#596 = VERTEX_POINT('',#597); -#597 = CARTESIAN_POINT('',(19.965518143439,20.8572529153,-62.38947335481 - )); -#598 = LINE('',#599,#600); -#599 = CARTESIAN_POINT('',(19.965518143439,-5.,-62.38947335481)); -#600 = VECTOR('',#601,1.); -#601 = DIRECTION('',(0.,1.,0.)); -#602 = ORIENTED_EDGE('',*,*,#603,.F.); -#603 = EDGE_CURVE('',#573,#596,#604,.T.); -#604 = LINE('',#605,#606); -#605 = CARTESIAN_POINT('',(-12.74971831875,20.8572529153,-62.38947335481 - )); -#606 = VECTOR('',#607,1.); -#607 = DIRECTION('',(1.,0.,0.)); -#608 = ORIENTED_EDGE('',*,*,#580,.F.); -#609 = PLANE('',#610); -#610 = AXIS2_PLACEMENT_3D('',#611,#612,#613); -#611 = CARTESIAN_POINT('',(19.965518143439,-5.,-62.38947335481)); -#612 = DIRECTION('',(0.,0.,1.)); -#613 = DIRECTION('',(-1.,0.,0.)); -#614 = ADVANCED_FACE('',(#615),#633,.F.); -#615 = FACE_BOUND('',#616,.F.); -#616 = EDGE_LOOP('',(#617,#618,#626,#632)); -#617 = ORIENTED_EDGE('',*,*,#47,.T.); -#618 = ORIENTED_EDGE('',*,*,#619,.T.); -#619 = EDGE_CURVE('',#40,#620,#622,.T.); -#620 = VERTEX_POINT('',#621); -#621 = CARTESIAN_POINT('',(26.747616824317,20.8572529153,-68.95279080543 - )); -#622 = LINE('',#623,#624); -#623 = CARTESIAN_POINT('',(26.747616824317,-5.,-68.95279080543)); -#624 = VECTOR('',#625,1.); -#625 = DIRECTION('',(0.,1.,0.)); -#626 = ORIENTED_EDGE('',*,*,#627,.F.); -#627 = EDGE_CURVE('',#596,#620,#628,.T.); -#628 = LINE('',#629,#630); -#629 = CARTESIAN_POINT('',(8.607536374131,20.8572529153,-51.39788454757) - ); -#630 = VECTOR('',#631,1.); -#631 = DIRECTION('',(0.718602345805,0.,-0.695421216677)); -#632 = ORIENTED_EDGE('',*,*,#595,.F.); -#633 = PLANE('',#634); -#634 = AXIS2_PLACEMENT_3D('',#635,#636,#637); -#635 = CARTESIAN_POINT('',(26.747616824317,-5.,-68.95279080543)); -#636 = DIRECTION('',(0.695421216677,0.,0.718602345805)); -#637 = DIRECTION('',(-0.718602345805,0.,0.695421216677)); -#638 = ADVANCED_FACE('',(#639),#657,.F.); -#639 = FACE_BOUND('',#640,.F.); -#640 = EDGE_LOOP('',(#641,#642,#650,#656)); -#641 = ORIENTED_EDGE('',*,*,#39,.T.); -#642 = ORIENTED_EDGE('',*,*,#643,.T.); -#643 = EDGE_CURVE('',#32,#644,#646,.T.); -#644 = VERTEX_POINT('',#645); -#645 = CARTESIAN_POINT('',(34.683490155872,20.8572529153,-60.75238354821 - )); -#646 = LINE('',#647,#648); -#647 = CARTESIAN_POINT('',(34.683490155872,-5.,-60.75238354821)); -#648 = VECTOR('',#649,1.); -#649 = DIRECTION('',(0.,1.,0.)); -#650 = ORIENTED_EDGE('',*,*,#651,.F.); -#651 = EDGE_CURVE('',#620,#644,#652,.T.); -#652 = LINE('',#653,#654); -#653 = CARTESIAN_POINT('',(12.749348137648,20.8572529153,-83.41767694094 - )); -#654 = VECTOR('',#655,1.); -#655 = DIRECTION('',(0.695421216677,0.,0.718602345805)); -#656 = ORIENTED_EDGE('',*,*,#619,.F.); -#657 = PLANE('',#658); -#658 = AXIS2_PLACEMENT_3D('',#659,#660,#661); -#659 = CARTESIAN_POINT('',(34.683490155872,-5.,-60.75238354821)); -#660 = DIRECTION('',(-0.718602345805,0.,0.695421216677)); -#661 = DIRECTION('',(-0.695421216677,0.,-0.718602345805)); -#662 = ADVANCED_FACE('',(#663),#674,.F.); -#663 = FACE_BOUND('',#664,.F.); -#664 = EDGE_LOOP('',(#665,#666,#667,#673)); -#665 = ORIENTED_EDGE('',*,*,#31,.T.); -#666 = ORIENTED_EDGE('',*,*,#312,.T.); -#667 = ORIENTED_EDGE('',*,*,#668,.F.); -#668 = EDGE_CURVE('',#644,#305,#669,.T.); -#669 = LINE('',#670,#671); -#670 = CARTESIAN_POINT('',(13.469833011847,20.8572529153,-40.22304997814 - )); -#671 = VECTOR('',#672,1.); -#672 = DIRECTION('',(-0.718602345805,0.,0.695421216677)); -#673 = ORIENTED_EDGE('',*,*,#643,.F.); -#674 = PLANE('',#675); -#675 = AXIS2_PLACEMENT_3D('',#676,#677,#678); -#676 = CARTESIAN_POINT('',(28.536336768195,-5.,-54.80352892379)); -#677 = DIRECTION('',(-0.695421216677,0.,-0.718602345805)); -#678 = DIRECTION('',(0.718602345805,0.,-0.695421216677)); -#679 = ADVANCED_FACE('',(#680),#705,.T.); -#680 = FACE_BOUND('',#681,.T.); -#681 = EDGE_LOOP('',(#682,#690,#691,#699)); -#682 = ORIENTED_EDGE('',*,*,#683,.F.); -#683 = EDGE_CURVE('',#156,#684,#686,.T.); -#684 = VERTEX_POINT('',#685); -#685 = CARTESIAN_POINT('',(32.703857168398,3.2,-60.78483712899)); -#686 = LINE('',#687,#688); -#687 = CARTESIAN_POINT('',(32.703857168398,3.,-60.78483712899)); -#688 = VECTOR('',#689,1.); -#689 = DIRECTION('',(0.,1.,0.)); -#690 = ORIENTED_EDGE('',*,*,#155,.T.); -#691 = ORIENTED_EDGE('',*,*,#692,.F.); -#692 = EDGE_CURVE('',#693,#158,#695,.T.); -#693 = VERTEX_POINT('',#694); -#694 = CARTESIAN_POINT('',(26.938753236523,3.2,-55.20570756731)); -#695 = LINE('',#696,#697); -#696 = CARTESIAN_POINT('',(26.938753236523,3.,-55.20570756731)); -#697 = VECTOR('',#698,1.); -#698 = DIRECTION('',(0.,-1.,0.)); -#699 = ORIENTED_EDGE('',*,*,#700,.F.); -#700 = EDGE_CURVE('',#684,#693,#701,.T.); -#701 = LINE('',#702,#703); -#702 = CARTESIAN_POINT('',(16.008242280412,3.2,-44.62779994931)); -#703 = VECTOR('',#704,1.); -#704 = DIRECTION('',(-0.718602345805,0.,0.695421216677)); -#705 = PLANE('',#706); -#706 = AXIS2_PLACEMENT_3D('',#707,#708,#709); -#707 = CARTESIAN_POINT('',(29.704873980143,3.,-57.88259703786)); -#708 = DIRECTION('',(-0.695421216677,0.,-0.718602345805)); -#709 = DIRECTION('',(-0.718602345805,0.,0.695421216677)); -#710 = ADVANCED_FACE('',(#711),#729,.T.); -#711 = FACE_BOUND('',#712,.T.); -#712 = EDGE_LOOP('',(#713,#721,#722,#723)); -#713 = ORIENTED_EDGE('',*,*,#714,.F.); -#714 = EDGE_CURVE('',#274,#715,#717,.T.); -#715 = VERTEX_POINT('',#716); -#716 = CARTESIAN_POINT('',(26.715163243538,3.2,-66.97315781796)); -#717 = LINE('',#718,#719); -#718 = CARTESIAN_POINT('',(26.715163243538,3.,-66.97315781796)); -#719 = VECTOR('',#720,1.); -#720 = DIRECTION('',(0.,1.,0.)); -#721 = ORIENTED_EDGE('',*,*,#281,.T.); -#722 = ORIENTED_EDGE('',*,*,#683,.T.); -#723 = ORIENTED_EDGE('',*,*,#724,.F.); -#724 = EDGE_CURVE('',#715,#684,#725,.T.); -#725 = LINE('',#726,#727); -#726 = CARTESIAN_POINT('',(30.25240665457,3.2,-63.31800414721)); -#727 = VECTOR('',#728,1.); -#728 = DIRECTION('',(0.695421216677,0.,0.718602345805)); -#729 = PLANE('',#730); -#730 = AXIS2_PLACEMENT_3D('',#731,#732,#733); -#731 = CARTESIAN_POINT('',(29.709510205968,3.,-63.87899747347)); -#732 = DIRECTION('',(-0.718602345805,0.,0.695421216677)); -#733 = DIRECTION('',(0.695421216677,0.,0.718602345805)); -#734 = ADVANCED_FACE('',(#735),#753,.T.); -#735 = FACE_BOUND('',#736,.T.); -#736 = EDGE_LOOP('',(#737,#745,#746,#747)); -#737 = ORIENTED_EDGE('',*,*,#738,.F.); -#738 = EDGE_CURVE('',#693,#739,#741,.T.); -#739 = VERTEX_POINT('',#740); -#740 = CARTESIAN_POINT('',(42.298608189024,3.2,-7.015765476549)); -#741 = LINE('',#742,#743); -#742 = CARTESIAN_POINT('',(32.699020781567,3.2,-37.13346923684)); -#743 = VECTOR('',#744,1.); -#744 = DIRECTION('',(0.30368282823,0.,0.952773183837)); -#745 = ORIENTED_EDGE('',*,*,#692,.T.); -#746 = ORIENTED_EDGE('',*,*,#165,.T.); -#747 = ORIENTED_EDGE('',*,*,#748,.F.); -#748 = EDGE_CURVE('',#739,#166,#749,.T.); -#749 = LINE('',#750,#751); -#750 = CARTESIAN_POINT('',(42.298608189024,3.,-7.015765476549)); -#751 = VECTOR('',#752,1.); -#752 = DIRECTION('',(0.,-1.,0.)); -#753 = PLANE('',#754); -#754 = AXIS2_PLACEMENT_3D('',#755,#756,#757); -#755 = CARTESIAN_POINT('',(34.581352051556,3.,-31.22785130119)); -#756 = DIRECTION('',(-0.952773183837,0.,0.30368282823)); -#757 = DIRECTION('',(0.30368282823,0.,0.952773183837)); -#758 = ADVANCED_FACE('',(#759),#777,.T.); -#759 = FACE_BOUND('',#760,.T.); -#760 = EDGE_LOOP('',(#761,#769,#775,#776)); -#761 = ORIENTED_EDGE('',*,*,#762,.F.); -#762 = EDGE_CURVE('',#763,#715,#765,.T.); -#763 = VERTEX_POINT('',#764); -#764 = CARTESIAN_POINT('',(20.532019001374,3.2,-60.98947335481)); -#765 = LINE('',#766,#767); -#766 = CARTESIAN_POINT('',(9.922784884512,3.2,-50.72247862452)); -#767 = VECTOR('',#768,1.); -#768 = DIRECTION('',(0.718602345805,0.,-0.695421216677)); -#769 = ORIENTED_EDGE('',*,*,#770,.T.); -#770 = EDGE_CURVE('',#763,#266,#771,.T.); -#771 = LINE('',#772,#773); -#772 = CARTESIAN_POINT('',(20.532019001374,3.,-60.98947335481)); -#773 = VECTOR('',#774,1.); -#774 = DIRECTION('',(0.,-1.,0.)); -#775 = ORIENTED_EDGE('',*,*,#273,.T.); -#776 = ORIENTED_EDGE('',*,*,#714,.T.); -#777 = PLANE('',#778); -#778 = AXIS2_PLACEMENT_3D('',#779,#780,#781); -#779 = CARTESIAN_POINT('',(23.522653113203,3.,-63.8836336993)); -#780 = DIRECTION('',(0.695421216677,0.,0.718602345805)); -#781 = DIRECTION('',(0.718602345805,0.,-0.695421216677)); -#782 = ADVANCED_FACE('',(#783),#801,.T.); -#783 = FACE_BOUND('',#784,.T.); -#784 = EDGE_LOOP('',(#785,#786,#787,#795)); -#785 = ORIENTED_EDGE('',*,*,#748,.T.); -#786 = ORIENTED_EDGE('',*,*,#173,.T.); -#787 = ORIENTED_EDGE('',*,*,#788,.F.); -#788 = EDGE_CURVE('',#789,#174,#791,.T.); -#789 = VERTEX_POINT('',#790); -#790 = CARTESIAN_POINT('',(35.082842712475,3.2,0.2)); -#791 = LINE('',#792,#793); -#792 = CARTESIAN_POINT('',(35.082842712475,3.,0.2)); -#793 = VECTOR('',#794,1.); -#794 = DIRECTION('',(0.,-1.,0.)); -#795 = ORIENTED_EDGE('',*,*,#796,.F.); -#796 = EDGE_CURVE('',#739,#789,#797,.T.); -#797 = LINE('',#798,#799); -#798 = CARTESIAN_POINT('',(36.596246576251,3.2,-1.313403863776)); -#799 = VECTOR('',#800,1.); -#800 = DIRECTION('',(-0.707106781187,0.,0.707106781187)); -#801 = PLANE('',#802); -#802 = AXIS2_PLACEMENT_3D('',#803,#804,#805); -#803 = CARTESIAN_POINT('',(38.67695526217,3.,-3.394112549695)); -#804 = DIRECTION('',(-0.707106781187,0.,-0.707106781187)); -#805 = DIRECTION('',(-0.707106781187,0.,0.707106781187)); -#806 = ADVANCED_FACE('',(#807),#825,.T.); -#807 = FACE_BOUND('',#808,.T.); -#808 = EDGE_LOOP('',(#809,#817,#818,#819)); -#809 = ORIENTED_EDGE('',*,*,#810,.F.); -#810 = EDGE_CURVE('',#258,#811,#813,.T.); -#811 = VERTEX_POINT('',#812); -#812 = CARTESIAN_POINT('',(-20.53201900137,3.2,-60.98947335481)); -#813 = LINE('',#814,#815); -#814 = CARTESIAN_POINT('',(-20.53201900137,3.,-60.98947335481)); -#815 = VECTOR('',#816,1.); -#816 = DIRECTION('',(0.,1.,0.)); -#817 = ORIENTED_EDGE('',*,*,#265,.T.); -#818 = ORIENTED_EDGE('',*,*,#770,.F.); -#819 = ORIENTED_EDGE('',*,*,#820,.F.); -#820 = EDGE_CURVE('',#811,#763,#821,.T.); -#821 = LINE('',#822,#823); -#822 = CARTESIAN_POINT('',(5.354765181569,3.2,-60.98947335481)); -#823 = VECTOR('',#824,1.); -#824 = DIRECTION('',(1.,0.,0.)); -#825 = PLANE('',#826); -#826 = AXIS2_PLACEMENT_3D('',#827,#828,#829); -#827 = CARTESIAN_POINT('',(10.306473847682,3.,-60.98947335481)); -#828 = DIRECTION('',(0.,0.,1.)); -#829 = DIRECTION('',(0.,-1.,0.)); -#830 = ADVANCED_FACE('',(#831),#849,.F.); -#831 = FACE_BOUND('',#832,.F.); -#832 = EDGE_LOOP('',(#833,#834,#842,#848)); -#833 = ORIENTED_EDGE('',*,*,#788,.F.); -#834 = ORIENTED_EDGE('',*,*,#835,.F.); -#835 = EDGE_CURVE('',#836,#789,#838,.T.); -#836 = VERTEX_POINT('',#837); -#837 = CARTESIAN_POINT('',(-32.57517518159,3.2,0.2)); -#838 = LINE('',#839,#840); -#839 = CARTESIAN_POINT('',(-7.942265537672,3.2,0.2)); -#840 = VECTOR('',#841,1.); -#841 = DIRECTION('',(1.,0.,0.)); -#842 = ORIENTED_EDGE('',*,*,#843,.T.); -#843 = EDGE_CURVE('',#836,#182,#844,.T.); -#844 = LINE('',#845,#846); -#845 = CARTESIAN_POINT('',(-32.57517518159,3.,0.2)); -#846 = VECTOR('',#847,1.); -#847 = DIRECTION('',(0.,-1.,0.)); -#848 = ORIENTED_EDGE('',*,*,#181,.T.); -#849 = PLANE('',#850); -#850 = AXIS2_PLACEMENT_3D('',#851,#852,#853); -#851 = CARTESIAN_POINT('',(-16.28758759079,3.,0.2)); -#852 = DIRECTION('',(0.,0.,1.)); -#853 = DIRECTION('',(0.,-1.,0.)); -#854 = ADVANCED_FACE('',(#855),#873,.F.); -#855 = FACE_BOUND('',#856,.F.); -#856 = EDGE_LOOP('',(#857,#865,#871,#872)); -#857 = ORIENTED_EDGE('',*,*,#858,.F.); -#858 = EDGE_CURVE('',#859,#249,#861,.T.); -#859 = VERTEX_POINT('',#860); -#860 = CARTESIAN_POINT('',(-25.53052705686,3.2,-65.82673637491)); -#861 = LINE('',#862,#863); -#862 = CARTESIAN_POINT('',(-25.53052705686,3.,-65.82673637491)); -#863 = VECTOR('',#864,1.); -#864 = DIRECTION('',(0.,-1.,0.)); -#865 = ORIENTED_EDGE('',*,*,#866,.F.); -#866 = EDGE_CURVE('',#811,#859,#867,.T.); -#867 = LINE('',#868,#869); -#868 = CARTESIAN_POINT('',(-9.454421870603,3.2,-50.26922436104)); -#869 = VECTOR('',#870,1.); -#870 = DIRECTION('',(-0.718602345805,0.,-0.695421216677)); -#871 = ORIENTED_EDGE('',*,*,#810,.F.); -#872 = ORIENTED_EDGE('',*,*,#257,.T.); -#873 = PLANE('',#874); -#874 = AXIS2_PLACEMENT_3D('',#875,#876,#877); -#875 = CARTESIAN_POINT('',(-23.00219525444,3.,-63.37996509944)); -#876 = DIRECTION('',(0.695421216677,0.,-0.718602345805)); -#877 = DIRECTION('',(-0.718602345805,0.,-0.695421216677)); -#878 = ADVANCED_FACE('',(#879),#898,.F.); -#879 = FACE_BOUND('',#880,.F.); -#880 = EDGE_LOOP('',(#881,#882,#891,#897)); -#881 = ORIENTED_EDGE('',*,*,#843,.F.); -#882 = ORIENTED_EDGE('',*,*,#883,.T.); -#883 = EDGE_CURVE('',#836,#884,#886,.T.); -#884 = VERTEX_POINT('',#885); -#885 = CARTESIAN_POINT('',(-36.85603142851,3.2,-1.573188716044)); -#886 = CIRCLE('',#887,6.054044962965); -#887 = AXIS2_PLACEMENT_3D('',#888,#889,#890); -#888 = CARTESIAN_POINT('',(-32.57517518159,3.2,-5.854044962965)); -#889 = DIRECTION('',(0.,-1.,0.)); -#890 = DIRECTION('',(-1.,0.,0.)); -#891 = ORIENTED_EDGE('',*,*,#892,.T.); -#892 = EDGE_CURVE('',#884,#190,#893,.T.); -#893 = LINE('',#894,#895); -#894 = CARTESIAN_POINT('',(-36.85603142851,3.,-1.573188716044)); -#895 = VECTOR('',#896,1.); -#896 = DIRECTION('',(0.,-1.,0.)); -#897 = ORIENTED_EDGE('',*,*,#189,.F.); -#898 = CYLINDRICAL_SURFACE('',#899,6.054044962965); -#899 = AXIS2_PLACEMENT_3D('',#900,#901,#902); -#900 = CARTESIAN_POINT('',(-32.57517518159,3.,-5.854044962965)); -#901 = DIRECTION('',(0.,-1.,0.)); -#902 = DIRECTION('',(-1.,0.,0.)); -#903 = ADVANCED_FACE('',(#904),#923,.F.); -#904 = FACE_BOUND('',#905,.F.); -#905 = EDGE_LOOP('',(#906,#914,#921,#922)); -#906 = ORIENTED_EDGE('',*,*,#907,.F.); -#907 = EDGE_CURVE('',#908,#241,#910,.T.); -#908 = VERTEX_POINT('',#909); -#909 = CARTESIAN_POINT('',(-27.86158468659,3.2,-65.78852163128)); -#910 = LINE('',#911,#912); -#911 = CARTESIAN_POINT('',(-27.86158468659,3.,-65.78852163128)); -#912 = VECTOR('',#913,1.); -#913 = DIRECTION('',(0.,-1.,0.)); -#914 = ORIENTED_EDGE('',*,*,#915,.T.); -#915 = EDGE_CURVE('',#908,#859,#916,.T.); -#916 = CIRCLE('',#917,1.648528137424); -#917 = AXIS2_PLACEMENT_3D('',#918,#919,#920); -#918 = CARTESIAN_POINT('',(-26.67694849991,3.2,-64.64210018823)); -#919 = DIRECTION('',(0.,-1.,0.)); -#920 = DIRECTION('',(-1.,0.,0.)); -#921 = ORIENTED_EDGE('',*,*,#858,.T.); -#922 = ORIENTED_EDGE('',*,*,#248,.F.); -#923 = CYLINDRICAL_SURFACE('',#924,1.648528137424); -#924 = AXIS2_PLACEMENT_3D('',#925,#926,#927); -#925 = CARTESIAN_POINT('',(-26.67694849991,3.,-64.64210018823)); -#926 = DIRECTION('',(0.,-1.,0.)); -#927 = DIRECTION('',(-1.,0.,0.)); -#928 = ADVANCED_FACE('',(#929),#947,.F.); -#929 = FACE_BOUND('',#930,.F.); -#930 = EDGE_LOOP('',(#931,#932,#940,#946)); -#931 = ORIENTED_EDGE('',*,*,#892,.F.); -#932 = ORIENTED_EDGE('',*,*,#933,.F.); -#933 = EDGE_CURVE('',#934,#884,#936,.T.); -#934 = VERTEX_POINT('',#935); -#935 = CARTESIAN_POINT('',(-39.69176606491,3.2,-4.408923352436)); -#936 = LINE('',#937,#938); -#937 = CARTESIAN_POINT('',(-36.19319006079,3.2,-0.910347348321)); -#938 = VECTOR('',#939,1.); -#939 = DIRECTION('',(0.707106781187,0.,0.707106781187)); -#940 = ORIENTED_EDGE('',*,*,#941,.T.); -#941 = EDGE_CURVE('',#934,#199,#942,.T.); -#942 = LINE('',#943,#944); -#943 = CARTESIAN_POINT('',(-39.69176606491,3.,-4.408923352436)); -#944 = VECTOR('',#945,1.); -#945 = DIRECTION('',(0.,-1.,0.)); -#946 = ORIENTED_EDGE('',*,*,#198,.T.); -#947 = PLANE('',#948); -#948 = AXIS2_PLACEMENT_3D('',#949,#950,#951); -#949 = CARTESIAN_POINT('',(-38.27389874671,3.,-2.99105603424)); -#950 = DIRECTION('',(-0.707106781187,0.,0.707106781187)); -#951 = DIRECTION('',(0.707106781187,0.,0.707106781187)); -#952 = ADVANCED_FACE('',(#953),#971,.F.); -#953 = FACE_BOUND('',#954,.F.); -#954 = EDGE_LOOP('',(#955,#963,#969,#970)); -#955 = ORIENTED_EDGE('',*,*,#956,.F.); -#956 = EDGE_CURVE('',#957,#232,#959,.T.); -#957 = VERTEX_POINT('',#958); -#958 = CARTESIAN_POINT('',(-29.08766684168,3.2,-64.52156932717)); -#959 = LINE('',#960,#961); -#960 = CARTESIAN_POINT('',(-29.08766684168,3.,-64.52156932717)); -#961 = VECTOR('',#962,1.); -#962 = DIRECTION('',(0.,-1.,0.)); -#963 = ORIENTED_EDGE('',*,*,#964,.F.); -#964 = EDGE_CURVE('',#908,#957,#965,.T.); -#965 = LINE('',#966,#967); -#966 = CARTESIAN_POINT('',(-29.44004200272,3.2,-64.15744811364)); -#967 = VECTOR('',#968,1.); -#968 = DIRECTION('',(-0.695421216677,0.,0.718602345805)); -#969 = ORIENTED_EDGE('',*,*,#907,.T.); -#970 = ORIENTED_EDGE('',*,*,#240,.T.); -#971 = PLANE('',#972); -#972 = AXIS2_PLACEMENT_3D('',#973,#974,#975); -#973 = CARTESIAN_POINT('',(-28.47462576413,3.,-65.15504547923)); -#974 = DIRECTION('',(-0.718602345805,0.,-0.695421216677)); -#975 = DIRECTION('',(-0.695421216677,0.,0.718602345805)); -#976 = ADVANCED_FACE('',(#977),#996,.F.); -#977 = FACE_BOUND('',#978,.F.); -#978 = EDGE_LOOP('',(#979,#980,#989,#995)); -#979 = ORIENTED_EDGE('',*,*,#941,.F.); -#980 = ORIENTED_EDGE('',*,*,#981,.T.); -#981 = EDGE_CURVE('',#934,#982,#984,.T.); -#982 = VERTEX_POINT('',#983); -#983 = CARTESIAN_POINT('',(-41.17904151244,3.2,-10.52828909594)); -#984 = CIRCLE('',#985,6.054044962965); -#985 = AXIS2_PLACEMENT_3D('',#986,#987,#988); -#986 = CARTESIAN_POINT('',(-35.41090981799,3.2,-8.689779599357)); -#987 = DIRECTION('',(0.,-1.,0.)); -#988 = DIRECTION('',(-1.,0.,0.)); -#989 = ORIENTED_EDGE('',*,*,#990,.T.); -#990 = EDGE_CURVE('',#982,#207,#991,.T.); -#991 = LINE('',#992,#993); -#992 = CARTESIAN_POINT('',(-41.17904151244,3.,-10.52828909594)); -#993 = VECTOR('',#994,1.); -#994 = DIRECTION('',(0.,-1.,0.)); -#995 = ORIENTED_EDGE('',*,*,#206,.F.); -#996 = CYLINDRICAL_SURFACE('',#997,6.054044962965); -#997 = AXIS2_PLACEMENT_3D('',#998,#999,#1000); -#998 = CARTESIAN_POINT('',(-35.41090981799,3.,-8.689779599357)); -#999 = DIRECTION('',(0.,-1.,0.)); -#1000 = DIRECTION('',(-1.,0.,0.)); -#1001 = ADVANCED_FACE('',(#1002),#1021,.F.); -#1002 = FACE_BOUND('',#1003,.F.); -#1003 = EDGE_LOOP('',(#1004,#1012,#1019,#1020)); -#1004 = ORIENTED_EDGE('',*,*,#1005,.F.); -#1005 = EDGE_CURVE('',#1006,#224,#1008,.T.); -#1006 = VERTEX_POINT('',#1007); -#1007 = CARTESIAN_POINT('',(-28.96712497021,3.2,-57.16864680227)); -#1008 = LINE('',#1009,#1010); -#1009 = CARTESIAN_POINT('',(-28.96712497021,3.,-57.16864680227)); -#1010 = VECTOR('',#1011,1.); -#1011 = DIRECTION('',(0.,-1.,0.)); -#1012 = ORIENTED_EDGE('',*,*,#1013,.T.); -#1013 = EDGE_CURVE('',#1006,#957,#1014,.T.); -#1014 = CIRCLE('',#1015,5.2); -#1015 = AXIS2_PLACEMENT_3D('',#1016,#1017,#1018); -#1016 = CARTESIAN_POINT('',(-25.35093464349,3.2,-60.90537900045)); -#1017 = DIRECTION('',(0.,-1.,0.)); -#1018 = DIRECTION('',(-1.,0.,0.)); -#1019 = ORIENTED_EDGE('',*,*,#956,.T.); -#1020 = ORIENTED_EDGE('',*,*,#231,.F.); -#1021 = CYLINDRICAL_SURFACE('',#1022,5.2); -#1022 = AXIS2_PLACEMENT_3D('',#1023,#1024,#1025); -#1023 = CARTESIAN_POINT('',(-25.35093464349,3.,-60.90537900045)); -#1024 = DIRECTION('',(0.,-1.,0.)); -#1025 = DIRECTION('',(-1.,0.,0.)); -#1026 = ADVANCED_FACE('',(#1027),#1045,.F.); -#1027 = FACE_BOUND('',#1028,.F.); -#1028 = EDGE_LOOP('',(#1029,#1030,#1038,#1044)); -#1029 = ORIENTED_EDGE('',*,*,#990,.F.); -#1030 = ORIENTED_EDGE('',*,*,#1031,.F.); -#1031 = EDGE_CURVE('',#1032,#982,#1034,.T.); -#1032 = VERTEX_POINT('',#1033); -#1033 = CARTESIAN_POINT('',(-26.93875323652,3.2,-55.20570756731)); -#1034 = LINE('',#1035,#1036); -#1035 = CARTESIAN_POINT('',(-32.39120436163,3.2,-38.09921113329)); -#1036 = VECTOR('',#1037,1.); -#1037 = DIRECTION('',(-0.30368282823,0.,0.952773183837)); -#1038 = ORIENTED_EDGE('',*,*,#1039,.T.); -#1039 = EDGE_CURVE('',#1032,#216,#1040,.T.); -#1040 = LINE('',#1041,#1042); -#1041 = CARTESIAN_POINT('',(-26.93875323652,3.,-55.20570756731)); -#1042 = VECTOR('',#1043,1.); -#1043 = DIRECTION('',(0.,-1.,0.)); -#1044 = ORIENTED_EDGE('',*,*,#215,.T.); -#1045 = PLANE('',#1046); -#1046 = AXIS2_PLACEMENT_3D('',#1047,#1048,#1049); -#1047 = CARTESIAN_POINT('',(-34.04006158346,3.,-32.92609366041)); -#1048 = DIRECTION('',(-0.952773183837,0.,-0.30368282823)); -#1049 = DIRECTION('',(-0.30368282823,0.,0.952773183837)); -#1050 = ADVANCED_FACE('',(#1051),#1062,.F.); -#1051 = FACE_BOUND('',#1052,.F.); -#1052 = EDGE_LOOP('',(#1053,#1054,#1060,#1061)); -#1053 = ORIENTED_EDGE('',*,*,#1039,.F.); -#1054 = ORIENTED_EDGE('',*,*,#1055,.F.); -#1055 = EDGE_CURVE('',#1006,#1032,#1056,.T.); -#1056 = LINE('',#1057,#1058); -#1057 = CARTESIAN_POINT('',(-14.90185526362,3.2,-43.55710346492)); -#1058 = VECTOR('',#1059,1.); -#1059 = DIRECTION('',(0.718602345805,0.,0.695421216677)); -#1060 = ORIENTED_EDGE('',*,*,#1005,.T.); -#1061 = ORIENTED_EDGE('',*,*,#223,.T.); -#1062 = PLANE('',#1063); -#1063 = AXIS2_PLACEMENT_3D('',#1064,#1065,#1066); -#1064 = CARTESIAN_POINT('',(-27.90836811563,3.,-56.14404399617)); -#1065 = DIRECTION('',(-0.695421216677,0.,0.718602345805)); -#1066 = DIRECTION('',(0.718602345805,0.,0.695421216677)); -#1067 = ADVANCED_FACE('',(#1068),#1086,.T.); -#1068 = FACE_BOUND('',#1069,.T.); -#1069 = EDGE_LOOP('',(#1070,#1071,#1072,#1073,#1074,#1075,#1076,#1077, - #1078,#1079,#1080,#1081,#1082,#1083,#1084,#1085)); -#1070 = ORIENTED_EDGE('',*,*,#304,.F.); -#1071 = ORIENTED_EDGE('',*,*,#668,.F.); -#1072 = ORIENTED_EDGE('',*,*,#651,.F.); -#1073 = ORIENTED_EDGE('',*,*,#627,.F.); -#1074 = ORIENTED_EDGE('',*,*,#603,.F.); -#1075 = ORIENTED_EDGE('',*,*,#572,.T.); -#1076 = ORIENTED_EDGE('',*,*,#554,.F.); -#1077 = ORIENTED_EDGE('',*,*,#523,.T.); -#1078 = ORIENTED_EDGE('',*,*,#505,.F.); -#1079 = ORIENTED_EDGE('',*,*,#474,.T.); -#1080 = ORIENTED_EDGE('',*,*,#450,.T.); -#1081 = ORIENTED_EDGE('',*,*,#432,.F.); -#1082 = ORIENTED_EDGE('',*,*,#401,.T.); -#1083 = ORIENTED_EDGE('',*,*,#383,.F.); -#1084 = ORIENTED_EDGE('',*,*,#352,.T.); -#1085 = ORIENTED_EDGE('',*,*,#335,.F.); -#1086 = PLANE('',#1087); -#1087 = AXIS2_PLACEMENT_3D('',#1088,#1089,#1090); -#1088 = CARTESIAN_POINT('',(-45.46495478095,20.8572529153, - -70.97315781796)); -#1089 = DIRECTION('',(0.,1.,0.)); -#1090 = DIRECTION('',(0.,0.,1.)); -#1091 = ADVANCED_FACE('',(#1092,#1142,#1153,#1171,#1205),#1216,.F.); -#1092 = FACE_BOUND('',#1093,.F.); -#1093 = EDGE_LOOP('',(#1094,#1104,#1112,#1120,#1128,#1136)); -#1094 = ORIENTED_EDGE('',*,*,#1095,.F.); -#1095 = EDGE_CURVE('',#1096,#1098,#1100,.T.); -#1096 = VERTEX_POINT('',#1097); -#1097 = CARTESIAN_POINT('',(16.626582997737,3.2,-8.940188245231)); -#1098 = VERTEX_POINT('',#1099); -#1099 = CARTESIAN_POINT('',(4.383041634064E-04,3.2,-8.903751615529)); -#1100 = LINE('',#1101,#1102); -#1101 = CARTESIAN_POINT('',(4.347099726942,3.2,-8.913277437397)); -#1102 = VECTOR('',#1103,1.); -#1103 = DIRECTION('',(-0.999997598615,0.,2.19152081707E-03)); -#1104 = ORIENTED_EDGE('',*,*,#1105,.F.); -#1105 = EDGE_CURVE('',#1106,#1096,#1108,.T.); -#1106 = VERTEX_POINT('',#1107); -#1107 = CARTESIAN_POINT('',(19.029295926024,3.2,-17.63009960955)); -#1108 = LINE('',#1109,#1110); -#1109 = CARTESIAN_POINT('',(19.849519003668,3.2,-20.59660707692)); -#1110 = VECTOR('',#1111,1.); -#1111 = DIRECTION('',(-0.26649542889,0.,0.963836182336)); -#1112 = ORIENTED_EDGE('',*,*,#1113,.F.); -#1113 = EDGE_CURVE('',#1114,#1106,#1116,.T.); -#1114 = VERTEX_POINT('',#1115); -#1115 = CARTESIAN_POINT('',(22.059435554995,3.2,-3.734519760785)); -#1116 = LINE('',#1117,#1118); -#1117 = CARTESIAN_POINT('',(17.698510515043,3.2,-23.73280021221)); -#1118 = VECTOR('',#1119,1.); -#1119 = DIRECTION('',(-0.213058124893,0.,-0.977039526026)); -#1120 = ORIENTED_EDGE('',*,*,#1121,.F.); -#1121 = EDGE_CURVE('',#1122,#1114,#1124,.T.); -#1122 = VERTEX_POINT('',#1123); -#1123 = CARTESIAN_POINT('',(-21.72552223146,3.2,-3.734519760785)); -#1124 = LINE('',#1125,#1126); -#1125 = CARTESIAN_POINT('',(0.297084840953,3.2,-3.734519760785)); -#1126 = VECTOR('',#1127,1.); -#1127 = DIRECTION('',(1.,0.,0.)); -#1128 = ORIENTED_EDGE('',*,*,#1129,.F.); -#1129 = EDGE_CURVE('',#1130,#1122,#1132,.T.); -#1130 = VERTEX_POINT('',#1131); -#1131 = CARTESIAN_POINT('',(-21.72552223146,3.2,-8.903751135252)); -#1132 = LINE('',#1133,#1134); -#1133 = CARTESIAN_POINT('',(-21.72552223146,3.2,-19.83215600037)); -#1134 = VECTOR('',#1135,1.); -#1135 = DIRECTION('',(0.,0.,1.)); -#1136 = ORIENTED_EDGE('',*,*,#1137,.F.); -#1137 = EDGE_CURVE('',#1098,#1130,#1138,.T.); -#1138 = LINE('',#1139,#1140); -#1139 = CARTESIAN_POINT('',(-5.279852300138,3.2,-8.903751135252)); -#1140 = VECTOR('',#1141,1.); -#1141 = DIRECTION('',(-1.,0.,0.)); -#1142 = FACE_BOUND('',#1143,.F.); -#1143 = EDGE_LOOP('',(#1144)); -#1144 = ORIENTED_EDGE('',*,*,#1145,.F.); -#1145 = EDGE_CURVE('',#1146,#1146,#1148,.T.); -#1146 = VERTEX_POINT('',#1147); -#1147 = CARTESIAN_POINT('',(21.163799345768,3.2,-48.00951684793)); -#1148 = CIRCLE('',#1149,4.735522705283); -#1149 = AXIS2_PLACEMENT_3D('',#1150,#1151,#1152); -#1150 = CARTESIAN_POINT('',(16.428276640485,3.2,-48.00951684793)); -#1151 = DIRECTION('',(-0.,1.,0.)); -#1152 = DIRECTION('',(1.,0.,0.)); -#1153 = FACE_BOUND('',#1154,.F.); -#1154 = EDGE_LOOP('',(#1155,#1156,#1157,#1158,#1159,#1160,#1161,#1162, - #1163,#1164,#1165,#1166,#1167,#1168,#1169,#1170)); -#1155 = ORIENTED_EDGE('',*,*,#1031,.T.); -#1156 = ORIENTED_EDGE('',*,*,#981,.F.); -#1157 = ORIENTED_EDGE('',*,*,#933,.T.); -#1158 = ORIENTED_EDGE('',*,*,#883,.F.); -#1159 = ORIENTED_EDGE('',*,*,#835,.T.); -#1160 = ORIENTED_EDGE('',*,*,#796,.F.); -#1161 = ORIENTED_EDGE('',*,*,#738,.F.); -#1162 = ORIENTED_EDGE('',*,*,#700,.F.); -#1163 = ORIENTED_EDGE('',*,*,#724,.F.); -#1164 = ORIENTED_EDGE('',*,*,#762,.F.); -#1165 = ORIENTED_EDGE('',*,*,#820,.F.); -#1166 = ORIENTED_EDGE('',*,*,#866,.T.); -#1167 = ORIENTED_EDGE('',*,*,#915,.F.); -#1168 = ORIENTED_EDGE('',*,*,#964,.T.); -#1169 = ORIENTED_EDGE('',*,*,#1013,.F.); -#1170 = ORIENTED_EDGE('',*,*,#1055,.T.); -#1171 = FACE_BOUND('',#1172,.F.); -#1172 = EDGE_LOOP('',(#1173,#1183,#1191,#1199)); -#1173 = ORIENTED_EDGE('',*,*,#1174,.F.); -#1174 = EDGE_CURVE('',#1175,#1177,#1179,.T.); -#1175 = VERTEX_POINT('',#1176); -#1176 = CARTESIAN_POINT('',(5.809375885494,3.2,-13.06417917474)); -#1177 = VERTEX_POINT('',#1178); -#1178 = CARTESIAN_POINT('',(2.688069798796,3.2,-49.64588621989)); -#1179 = LINE('',#1180,#1181); -#1180 = CARTESIAN_POINT('',(4.914157977861,3.2,-23.55613296875)); -#1181 = VECTOR('',#1182,1.); -#1182 = DIRECTION('',(-8.501532861638E-02,0.,-0.996379643459)); -#1183 = ORIENTED_EDGE('',*,*,#1184,.F.); -#1184 = EDGE_CURVE('',#1185,#1175,#1187,.T.); -#1185 = VERTEX_POINT('',#1186); -#1186 = CARTESIAN_POINT('',(-5.809375885494,3.2,-13.06417917474)); -#1187 = LINE('',#1188,#1189); -#1188 = CARTESIAN_POINT('',(1.615747408047,3.2,-13.06417917474)); -#1189 = VECTOR('',#1190,1.); -#1190 = DIRECTION('',(1.,0.,-3.066574716487E-16)); -#1191 = ORIENTED_EDGE('',*,*,#1192,.F.); -#1192 = EDGE_CURVE('',#1193,#1185,#1195,.T.); -#1193 = VERTEX_POINT('',#1194); -#1194 = CARTESIAN_POINT('',(-2.688069798796,3.2,-49.64588621989)); -#1195 = LINE('',#1196,#1197); -#1196 = CARTESIAN_POINT('',(-4.890802006217,3.2,-23.82986495424)); -#1197 = VECTOR('',#1198,1.); -#1198 = DIRECTION('',(-8.501532861638E-02,0.,0.996379643459)); -#1199 = ORIENTED_EDGE('',*,*,#1200,.F.); -#1200 = EDGE_CURVE('',#1177,#1193,#1201,.T.); -#1201 = LINE('',#1202,#1203); -#1202 = CARTESIAN_POINT('',(1.615747408047,3.2,-49.64588621989)); -#1203 = VECTOR('',#1204,1.); -#1204 = DIRECTION('',(-1.,0.,0.)); -#1205 = FACE_BOUND('',#1206,.F.); -#1206 = EDGE_LOOP('',(#1207)); -#1207 = ORIENTED_EDGE('',*,*,#1208,.F.); -#1208 = EDGE_CURVE('',#1209,#1209,#1211,.T.); -#1209 = VERTEX_POINT('',#1210); -#1210 = CARTESIAN_POINT('',(-11.6927539352,3.2,-48.00951684793)); -#1211 = CIRCLE('',#1212,4.735522705283); -#1212 = AXIS2_PLACEMENT_3D('',#1213,#1214,#1215); -#1213 = CARTESIAN_POINT('',(-16.42827664048,3.2,-48.00951684793)); -#1214 = DIRECTION('',(-0.,1.,0.)); -#1215 = DIRECTION('',(1.,0.,0.)); -#1216 = PLANE('',#1217); -#1217 = AXIS2_PLACEMENT_3D('',#1218,#1219,#1220); -#1218 = CARTESIAN_POINT('',(0.403056515455,3.2,-33.34517655273)); -#1219 = DIRECTION('',(0.,1.,0.)); -#1220 = DIRECTION('',(1.,0.,0.)); -#1221 = ADVANCED_FACE('',(#1222),#1247,.T.); -#1222 = FACE_BOUND('',#1223,.T.); -#1223 = EDGE_LOOP('',(#1224,#1232,#1240,#1246)); -#1224 = ORIENTED_EDGE('',*,*,#1225,.F.); -#1225 = EDGE_CURVE('',#1226,#1096,#1228,.T.); -#1226 = VERTEX_POINT('',#1227); -#1227 = CARTESIAN_POINT('',(16.626582997737,0.,-8.940188245231)); -#1228 = LINE('',#1229,#1230); -#1229 = CARTESIAN_POINT('',(16.626582997737,-22.,-8.940188245231)); -#1230 = VECTOR('',#1231,1.); -#1231 = DIRECTION('',(0.,1.,0.)); -#1232 = ORIENTED_EDGE('',*,*,#1233,.T.); -#1233 = EDGE_CURVE('',#1226,#1234,#1236,.T.); -#1234 = VERTEX_POINT('',#1235); -#1235 = CARTESIAN_POINT('',(-5.329070518201E-15,0.,-8.903751135252)); -#1236 = LINE('',#1237,#1238); -#1237 = CARTESIAN_POINT('',(-18.54556462154,1.7763568394E-15, - -8.863107566442)); -#1238 = VECTOR('',#1239,1.); -#1239 = DIRECTION('',(-0.999997598615,0.,2.19152081707E-03)); -#1240 = ORIENTED_EDGE('',*,*,#1241,.T.); -#1241 = EDGE_CURVE('',#1234,#1098,#1242,.T.); -#1242 = LINE('',#1243,#1244); -#1243 = CARTESIAN_POINT('',(-5.329070518201E-15,-22.,-8.903751135252)); -#1244 = VECTOR('',#1245,1.); -#1245 = DIRECTION('',(0.,1.,0.)); -#1246 = ORIENTED_EDGE('',*,*,#1095,.F.); -#1247 = PLANE('',#1248); -#1248 = AXIS2_PLACEMENT_3D('',#1249,#1250,#1251); -#1249 = CARTESIAN_POINT('',(8.237581109188,-22.,-8.921803528809)); -#1250 = DIRECTION('',(-2.19152081707E-03,0.,-0.999997598615)); -#1251 = DIRECTION('',(-0.999997598615,0.,2.19152081707E-03)); -#1252 = ADVANCED_FACE('',(#1253),#1271,.T.); -#1253 = FACE_BOUND('',#1254,.T.); -#1254 = EDGE_LOOP('',(#1255,#1263,#1269,#1270)); -#1255 = ORIENTED_EDGE('',*,*,#1256,.F.); -#1256 = EDGE_CURVE('',#1257,#1106,#1259,.T.); -#1257 = VERTEX_POINT('',#1258); -#1258 = CARTESIAN_POINT('',(19.029295926024,0.,-17.63009960955)); -#1259 = LINE('',#1260,#1261); -#1260 = CARTESIAN_POINT('',(19.029295926024,-22.,-17.63009960955)); -#1261 = VECTOR('',#1262,1.); -#1262 = DIRECTION('',(0.,1.,0.)); -#1263 = ORIENTED_EDGE('',*,*,#1264,.T.); -#1264 = EDGE_CURVE('',#1257,#1226,#1265,.T.); -#1265 = LINE('',#1266,#1267); -#1266 = CARTESIAN_POINT('',(23.053273013696,0.,-32.18365022821)); -#1267 = VECTOR('',#1268,1.); -#1268 = DIRECTION('',(-0.26649542889,0.,0.963836182336)); -#1269 = ORIENTED_EDGE('',*,*,#1225,.T.); -#1270 = ORIENTED_EDGE('',*,*,#1105,.F.); -#1271 = PLANE('',#1272); -#1272 = AXIS2_PLACEMENT_3D('',#1273,#1274,#1275); -#1273 = CARTESIAN_POINT('',(17.956031892536,-22.,-13.7484168618)); -#1274 = DIRECTION('',(-0.963836182336,0.,-0.26649542889)); -#1275 = DIRECTION('',(-0.26649542889,0.,0.963836182336)); -#1276 = ADVANCED_FACE('',(#1277),#1295,.T.); -#1277 = FACE_BOUND('',#1278,.T.); -#1278 = EDGE_LOOP('',(#1279,#1287,#1288,#1289)); -#1279 = ORIENTED_EDGE('',*,*,#1280,.T.); -#1280 = EDGE_CURVE('',#1281,#1257,#1283,.T.); -#1281 = VERTEX_POINT('',#1282); -#1282 = CARTESIAN_POINT('',(22.059435554995,0.,-3.734519760785)); -#1283 = LINE('',#1284,#1285); -#1284 = CARTESIAN_POINT('',(12.741012548745,1.7763568394E-15, - -46.46683800444)); -#1285 = VECTOR('',#1286,1.); -#1286 = DIRECTION('',(-0.213058124893,0.,-0.977039526026)); -#1287 = ORIENTED_EDGE('',*,*,#1256,.T.); -#1288 = ORIENTED_EDGE('',*,*,#1113,.F.); -#1289 = ORIENTED_EDGE('',*,*,#1290,.F.); -#1290 = EDGE_CURVE('',#1281,#1114,#1291,.T.); -#1291 = LINE('',#1292,#1293); -#1292 = CARTESIAN_POINT('',(22.059435554995,-22.,-3.734519760785)); -#1293 = VECTOR('',#1294,1.); -#1294 = DIRECTION('',(0.,1.,0.)); -#1295 = PLANE('',#1296); -#1296 = AXIS2_PLACEMENT_3D('',#1297,#1298,#1299); -#1297 = CARTESIAN_POINT('',(20.484588228021,-22.,-10.95643672209)); -#1298 = DIRECTION('',(0.977039526026,0.,-0.213058124893)); -#1299 = DIRECTION('',(-0.213058124893,0.,-0.977039526026)); -#1300 = ADVANCED_FACE('',(#1301),#1319,.T.); -#1301 = FACE_BOUND('',#1302,.T.); -#1302 = EDGE_LOOP('',(#1303,#1311,#1312,#1313)); -#1303 = ORIENTED_EDGE('',*,*,#1304,.T.); -#1304 = EDGE_CURVE('',#1305,#1281,#1307,.T.); -#1305 = VERTEX_POINT('',#1306); -#1306 = CARTESIAN_POINT('',(-21.72552223146,0.,-3.734519760785)); -#1307 = LINE('',#1308,#1309); -#1308 = CARTESIAN_POINT('',(-22.63692080725,0.,-3.734519760785)); -#1309 = VECTOR('',#1310,1.); -#1310 = DIRECTION('',(1.,0.,0.)); -#1311 = ORIENTED_EDGE('',*,*,#1290,.T.); -#1312 = ORIENTED_EDGE('',*,*,#1121,.F.); -#1313 = ORIENTED_EDGE('',*,*,#1314,.F.); -#1314 = EDGE_CURVE('',#1305,#1122,#1315,.T.); -#1315 = LINE('',#1316,#1317); -#1316 = CARTESIAN_POINT('',(-21.72552223146,-22.,-3.734519760785)); -#1317 = VECTOR('',#1318,1.); -#1318 = DIRECTION('',(0.,1.,0.)); -#1319 = PLANE('',#1320); -#1320 = AXIS2_PLACEMENT_3D('',#1321,#1322,#1323); -#1321 = CARTESIAN_POINT('',(0.19111316645,-22.,-3.734519760785)); -#1322 = DIRECTION('',(0.,0.,1.)); -#1323 = DIRECTION('',(0.,-1.,0.)); -#1324 = ADVANCED_FACE('',(#1325),#1343,.T.); -#1325 = FACE_BOUND('',#1326,.T.); -#1326 = EDGE_LOOP('',(#1327,#1335,#1336,#1337)); -#1327 = ORIENTED_EDGE('',*,*,#1328,.T.); -#1328 = EDGE_CURVE('',#1329,#1305,#1331,.T.); -#1329 = VERTEX_POINT('',#1330); -#1330 = CARTESIAN_POINT('',(-21.72552223146,0.,-8.903751135252)); -#1331 = LINE('',#1332,#1333); -#1332 = CARTESIAN_POINT('',(-21.72552223146,0.,-38.64614663299)); -#1333 = VECTOR('',#1334,1.); -#1334 = DIRECTION('',(0.,0.,1.)); -#1335 = ORIENTED_EDGE('',*,*,#1314,.T.); -#1336 = ORIENTED_EDGE('',*,*,#1129,.F.); -#1337 = ORIENTED_EDGE('',*,*,#1338,.F.); -#1338 = EDGE_CURVE('',#1329,#1130,#1339,.T.); -#1339 = LINE('',#1340,#1341); -#1340 = CARTESIAN_POINT('',(-21.72552223146,-22.,-8.903751135252)); -#1341 = VECTOR('',#1342,1.); -#1342 = DIRECTION('',(0.,1.,0.)); -#1343 = PLANE('',#1344); -#1344 = AXIS2_PLACEMENT_3D('',#1345,#1346,#1347); -#1345 = CARTESIAN_POINT('',(-21.72552223146,-22.,-6.319135448019)); -#1346 = DIRECTION('',(-1.,0.,0.)); -#1347 = DIRECTION('',(0.,1.,0.)); -#1348 = ADVANCED_FACE('',(#1349),#1360,.T.); -#1349 = FACE_BOUND('',#1350,.T.); -#1350 = EDGE_LOOP('',(#1351,#1357,#1358,#1359)); -#1351 = ORIENTED_EDGE('',*,*,#1352,.T.); -#1352 = EDGE_CURVE('',#1234,#1329,#1353,.T.); -#1353 = LINE('',#1354,#1355); -#1354 = CARTESIAN_POINT('',(-28.21385794834,0.,-8.903751135252)); -#1355 = VECTOR('',#1356,1.); -#1356 = DIRECTION('',(-1.,0.,0.)); -#1357 = ORIENTED_EDGE('',*,*,#1338,.T.); -#1358 = ORIENTED_EDGE('',*,*,#1137,.F.); -#1359 = ORIENTED_EDGE('',*,*,#1241,.F.); -#1360 = PLANE('',#1361); -#1361 = AXIS2_PLACEMENT_3D('',#1362,#1363,#1364); -#1362 = CARTESIAN_POINT('',(-10.96276111573,-22.,-8.903751135252)); -#1363 = DIRECTION('',(0.,0.,-1.)); -#1364 = DIRECTION('',(0.,1.,0.)); -#1365 = ADVANCED_FACE('',(#1366),#1385,.T.); -#1366 = FACE_BOUND('',#1367,.T.); -#1367 = EDGE_LOOP('',(#1368,#1377,#1383,#1384)); -#1368 = ORIENTED_EDGE('',*,*,#1369,.T.); -#1369 = EDGE_CURVE('',#1370,#1370,#1372,.T.); -#1370 = VERTEX_POINT('',#1371); -#1371 = CARTESIAN_POINT('',(21.163799345768,0.,-48.00951684793)); -#1372 = CIRCLE('',#1373,4.735522705283); -#1373 = AXIS2_PLACEMENT_3D('',#1374,#1375,#1376); -#1374 = CARTESIAN_POINT('',(16.428276640485,0.,-48.00951684793)); -#1375 = DIRECTION('',(-0.,1.,0.)); -#1376 = DIRECTION('',(1.,0.,0.)); -#1377 = ORIENTED_EDGE('',*,*,#1378,.T.); -#1378 = EDGE_CURVE('',#1370,#1146,#1379,.T.); -#1379 = LINE('',#1380,#1381); -#1380 = CARTESIAN_POINT('',(21.163799345768,-22.,-48.00951684793)); -#1381 = VECTOR('',#1382,1.); -#1382 = DIRECTION('',(0.,1.,0.)); -#1383 = ORIENTED_EDGE('',*,*,#1145,.F.); -#1384 = ORIENTED_EDGE('',*,*,#1378,.F.); -#1385 = CYLINDRICAL_SURFACE('',#1386,4.735522705283); -#1386 = AXIS2_PLACEMENT_3D('',#1387,#1388,#1389); -#1387 = CARTESIAN_POINT('',(16.428276640485,-22.,-48.00951684793)); -#1388 = DIRECTION('',(0.,1.,0.)); -#1389 = DIRECTION('',(1.,0.,0.)); -#1390 = ADVANCED_FACE('',(#1391),#1416,.F.); -#1391 = FACE_BOUND('',#1392,.F.); -#1392 = EDGE_LOOP('',(#1393,#1403,#1409,#1410)); -#1393 = ORIENTED_EDGE('',*,*,#1394,.T.); -#1394 = EDGE_CURVE('',#1395,#1397,#1399,.T.); -#1395 = VERTEX_POINT('',#1396); -#1396 = CARTESIAN_POINT('',(1.480297366167E-15,11.242400581089, - -46.71869179633)); -#1397 = VERTEX_POINT('',#1398); -#1398 = CARTESIAN_POINT('',(3.256654205567E-15,17.8572529153, - -18.39898295202)); -#1399 = LINE('',#1400,#1401); -#1400 = CARTESIAN_POINT('',(2.6645352591E-15,18.133069549222, - -17.21814831317)); -#1401 = VECTOR('',#1402,1.); -#1402 = DIRECTION('',(5.275122655165E-17,0.227455280238,0.97378852709)); -#1403 = ORIENTED_EDGE('',*,*,#1404,.F.); -#1404 = EDGE_CURVE('',#1175,#1397,#1405,.T.); -#1405 = LINE('',#1406,#1407); -#1406 = CARTESIAN_POINT('',(5.649679875255,3.602918464526, - -13.21082950266)); -#1407 = VECTOR('',#1408,1.); -#1408 = DIRECTION('',(-0.349023821871,0.880598971639,-0.320511814002)); -#1409 = ORIENTED_EDGE('',*,*,#1174,.T.); -#1410 = ORIENTED_EDGE('',*,*,#1411,.T.); -#1411 = EDGE_CURVE('',#1177,#1395,#1412,.T.); -#1412 = LINE('',#1413,#1414); -#1413 = CARTESIAN_POINT('',(1.103762571829,7.940067898719, - -47.92064259636)); -#1414 = VECTOR('',#1415,1.); -#1415 = DIRECTION('',(-0.299648208284,0.896513522642,0.326304236859)); -#1416 = PLANE('',#1417); -#1417 = AXIS2_PLACEMENT_3D('',#1418,#1419,#1420); -#1418 = CARTESIAN_POINT('',(5.823691883056,3.068404028665, - -13.45978839622)); -#1419 = DIRECTION('',(0.93629059846,0.342020143326,-7.988827695448E-02) - ); -#1420 = DIRECTION('',(-0.340781908463,0.939692620786,2.907695487825E-02) - ); -#1421 = ADVANCED_FACE('',(#1422),#1432,.F.); -#1422 = FACE_BOUND('',#1423,.F.); -#1423 = EDGE_LOOP('',(#1424,#1425,#1431)); -#1424 = ORIENTED_EDGE('',*,*,#1404,.T.); -#1425 = ORIENTED_EDGE('',*,*,#1426,.T.); -#1426 = EDGE_CURVE('',#1397,#1185,#1427,.T.); -#1427 = LINE('',#1428,#1429); -#1428 = CARTESIAN_POINT('',(-5.275833888477,4.546144602338, - -13.55413574101)); -#1429 = VECTOR('',#1430,1.); -#1430 = DIRECTION('',(-0.349023821871,-0.880598971639,0.320511814002)); -#1431 = ORIENTED_EDGE('',*,*,#1184,.T.); -#1432 = PLANE('',#1433); -#1433 = AXIS2_PLACEMENT_3D('',#1434,#1435,#1436); -#1434 = CARTESIAN_POINT('',(2.828438300639,3.068404028665, - -13.01628215822)); -#1435 = DIRECTION('',(2.88163763217E-16,0.342020143326,0.939692620786)); -#1436 = DIRECTION('',(-1.048830324052E-16,0.939692620786,-0.342020143326 - )); -#1437 = ADVANCED_FACE('',(#1438),#1449,.F.); -#1438 = FACE_BOUND('',#1439,.F.); -#1439 = EDGE_LOOP('',(#1440,#1441,#1442,#1443)); -#1440 = ORIENTED_EDGE('',*,*,#1192,.T.); -#1441 = ORIENTED_EDGE('',*,*,#1426,.F.); -#1442 = ORIENTED_EDGE('',*,*,#1394,.F.); -#1443 = ORIENTED_EDGE('',*,*,#1444,.F.); -#1444 = EDGE_CURVE('',#1193,#1395,#1445,.T.); -#1445 = LINE('',#1446,#1447); -#1446 = CARTESIAN_POINT('',(-0.871390517001,8.635298785064, - -47.66759924779)); -#1447 = VECTOR('',#1448,1.); -#1448 = DIRECTION('',(0.299648208284,0.896513522642,0.326304236859)); -#1449 = PLANE('',#1450); -#1450 = AXIS2_PLACEMENT_3D('',#1451,#1452,#1453); -#1451 = CARTESIAN_POINT('',(-5.782806207227,3.068404028665, - -13.93896851312)); -#1452 = DIRECTION('',(-0.93629059846,0.342020143326,-7.988827695448E-02) - ); -#1453 = DIRECTION('',(0.340781908463,0.939692620786,2.907695487825E-02) - ); -#1454 = ADVANCED_FACE('',(#1455),#1460,.F.); -#1455 = FACE_BOUND('',#1456,.F.); -#1456 = EDGE_LOOP('',(#1457,#1458,#1459)); -#1457 = ORIENTED_EDGE('',*,*,#1444,.T.); -#1458 = ORIENTED_EDGE('',*,*,#1411,.F.); -#1459 = ORIENTED_EDGE('',*,*,#1200,.T.); -#1460 = PLANE('',#1461); -#1461 = AXIS2_PLACEMENT_3D('',#1462,#1463,#1464); -#1462 = CARTESIAN_POINT('',(2.828438300639,3.068404028665, - -49.69378323641)); -#1463 = DIRECTION('',(0.,0.342020143326,-0.939692620786)); -#1464 = DIRECTION('',(0.,0.939692620786,0.342020143326)); -#1465 = ADVANCED_FACE('',(#1466),#1485,.T.); -#1466 = FACE_BOUND('',#1467,.T.); -#1467 = EDGE_LOOP('',(#1468,#1477,#1483,#1484)); -#1468 = ORIENTED_EDGE('',*,*,#1469,.T.); -#1469 = EDGE_CURVE('',#1470,#1470,#1472,.T.); -#1470 = VERTEX_POINT('',#1471); -#1471 = CARTESIAN_POINT('',(-11.6927539352,0.,-48.00951684793)); -#1472 = CIRCLE('',#1473,4.735522705283); -#1473 = AXIS2_PLACEMENT_3D('',#1474,#1475,#1476); -#1474 = CARTESIAN_POINT('',(-16.42827664048,0.,-48.00951684793)); -#1475 = DIRECTION('',(-0.,1.,0.)); -#1476 = DIRECTION('',(1.,0.,0.)); -#1477 = ORIENTED_EDGE('',*,*,#1478,.T.); -#1478 = EDGE_CURVE('',#1470,#1209,#1479,.T.); -#1479 = LINE('',#1480,#1481); -#1480 = CARTESIAN_POINT('',(-11.6927539352,-22.,-48.00951684793)); -#1481 = VECTOR('',#1482,1.); -#1482 = DIRECTION('',(0.,1.,0.)); -#1483 = ORIENTED_EDGE('',*,*,#1208,.F.); -#1484 = ORIENTED_EDGE('',*,*,#1478,.F.); -#1485 = CYLINDRICAL_SURFACE('',#1486,4.735522705283); -#1486 = AXIS2_PLACEMENT_3D('',#1487,#1488,#1489); -#1487 = CARTESIAN_POINT('',(-16.42827664048,-22.,-48.00951684793)); -#1488 = DIRECTION('',(0.,1.,0.)); -#1489 = DIRECTION('',(1.,0.,0.)); -#1490 = ADVANCED_FACE('',(#1491),#1499,.F.); -#1491 = FACE_BOUND('',#1492,.F.); -#1492 = EDGE_LOOP('',(#1493,#1494,#1495,#1496,#1497,#1498)); -#1493 = ORIENTED_EDGE('',*,*,#1264,.T.); -#1494 = ORIENTED_EDGE('',*,*,#1233,.T.); -#1495 = ORIENTED_EDGE('',*,*,#1352,.T.); -#1496 = ORIENTED_EDGE('',*,*,#1328,.T.); -#1497 = ORIENTED_EDGE('',*,*,#1304,.T.); -#1498 = ORIENTED_EDGE('',*,*,#1280,.T.); -#1499 = PLANE('',#1500); -#1500 = AXIS2_PLACEMENT_3D('',#1501,#1502,#1503); -#1501 = CARTESIAN_POINT('',(-45.46495478095,0.,-70.97315781796)); -#1502 = DIRECTION('',(0.,1.,0.)); -#1503 = DIRECTION('',(0.,0.,1.)); -#1504 = ADVANCED_FACE('',(#1505),#1508,.F.); -#1505 = FACE_BOUND('',#1506,.F.); -#1506 = EDGE_LOOP('',(#1507)); -#1507 = ORIENTED_EDGE('',*,*,#1369,.T.); -#1508 = PLANE('',#1509); -#1509 = AXIS2_PLACEMENT_3D('',#1510,#1511,#1512); -#1510 = CARTESIAN_POINT('',(-45.46495478095,0.,-70.97315781796)); -#1511 = DIRECTION('',(0.,1.,0.)); -#1512 = DIRECTION('',(0.,0.,1.)); -#1513 = ADVANCED_FACE('',(#1514),#1517,.F.); -#1514 = FACE_BOUND('',#1515,.F.); -#1515 = EDGE_LOOP('',(#1516)); -#1516 = ORIENTED_EDGE('',*,*,#1469,.T.); -#1517 = PLANE('',#1518); -#1518 = AXIS2_PLACEMENT_3D('',#1519,#1520,#1521); -#1519 = CARTESIAN_POINT('',(-45.46495478095,0.,-70.97315781796)); -#1520 = DIRECTION('',(0.,1.,0.)); -#1521 = DIRECTION('',(0.,0.,1.)); -#1522 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#1526)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#1523,#1524,#1525)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#1523 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#1524 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#1525 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#1526 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(2.E-06),#1523, - 'distance_accuracy_value','confusion accuracy'); -#1527 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7)); -ENDSEC; -END-ISO-10303-21; diff --git a/docs/CAD/design/mate-connectors/BearConnector_Female_Trimmed_wall.stl b/docs/CAD/design/mate-connectors/BearConnector_Female_Trimmed_wall.stl deleted file mode 100644 index 5c1f741349..0000000000 Binary files a/docs/CAD/design/mate-connectors/BearConnector_Female_Trimmed_wall.stl and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/DESIGN_MATE_CONNECTORS.md b/docs/CAD/design/mate-connectors/DESIGN_MATE_CONNECTORS.md deleted file mode 100644 index fe1a7a5fee..0000000000 --- a/docs/CAD/design/mate-connectors/DESIGN_MATE_CONNECTORS.md +++ /dev/null @@ -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. diff --git a/docs/CAD/design/mate-connectors/bear-01-view.png b/docs/CAD/design/mate-connectors/bear-01-view.png deleted file mode 100644 index eb57aaeefe..0000000000 Binary files a/docs/CAD/design/mate-connectors/bear-01-view.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/bear-02-view.png b/docs/CAD/design/mate-connectors/bear-02-view.png deleted file mode 100644 index 46b6d834fa..0000000000 Binary files a/docs/CAD/design/mate-connectors/bear-02-view.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/bear-02-zoom.png b/docs/CAD/design/mate-connectors/bear-02-zoom.png deleted file mode 100644 index 437adaee9d..0000000000 Binary files a/docs/CAD/design/mate-connectors/bear-02-zoom.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/bear-flat.png b/docs/CAD/design/mate-connectors/bear-flat.png deleted file mode 100644 index 16375b9d5c..0000000000 Binary files a/docs/CAD/design/mate-connectors/bear-flat.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/bear-sil-cmp.png b/docs/CAD/design/mate-connectors/bear-sil-cmp.png deleted file mode 100644 index eb2f283f95..0000000000 Binary files a/docs/CAD/design/mate-connectors/bear-sil-cmp.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/bear-sil.png b/docs/CAD/design/mate-connectors/bear-sil.png deleted file mode 100644 index 2b6e51067e..0000000000 Binary files a/docs/CAD/design/mate-connectors/bear-sil.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/bear_glyph_table.h b/docs/CAD/design/mate-connectors/bear_glyph_table.h deleted file mode 100644 index c26631e7e0..0000000000 --- a/docs/CAD/design/mate-connectors/bear_glyph_table.h +++ /dev/null @@ -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}, -}; diff --git a/docs/CAD/design/mate-connectors/bear_mesh.json b/docs/CAD/design/mate-connectors/bear_mesh.json deleted file mode 100644 index 4bfb6aa361..0000000000 --- a/docs/CAD/design/mate-connectors/bear_mesh.json +++ /dev/null @@ -1 +0,0 @@ -{"v": [[-36.7146, 0.0, -1.7146], [-36.0624, 0.0, -1.152], [-36.5065, 0.0, -1.5165], [-36.2889, 0.0, -1.3288], [-39.9409, 0.0, -4.9818], [-40.1196, 0.0, -5.2115], [-39.751, 0.0, -4.7612], [-39.5503, 0.0, -4.5503], [-35.8275, 0.0, -0.9866], [-35.3348, 0.0, -0.6912], [-35.5848, 0.0, -0.8329], [-40.4417, 0.0, -5.6962], [-40.5842, 0.0, -5.95], [-40.2867, 0.0, -5.4499], [-35.0781, 0.0, -0.5621], [-40.714, 0.0, -6.2105], [-34.5473, 0.0, -0.3422], [-34.8154, 0.0, -0.4456], [-40.8307, 0.0, -6.4772], [-40.934, 0.0, -6.7493], [-41.0236, 0.0, -7.0262], [-33.9976, 0.0, -0.1754], [-34.2745, 0.0, -0.2521], [-41.161, 0.0, -7.5917], [-41.2085, 0.0, -7.8789], [-41.0994, 0.0, -7.3072], [-33.149, 0.0, -0.0282], [-33.7172, 0.0, -0.1125], [-33.4341, 0.0, -0.0634], [-32.8624, 0.0, -0.0071], [-32.5752, 0.0, -0.0], [-41.2544, 0.0, -9.0404], [-41.2298, 0.0, -9.3304], [-41.2646, 0.0, -8.7495], [-41.2604, 0.0, -8.4585], [-41.2417, 0.0, -8.168], [-41.1908, 0.0, -9.6189], [-41.1374, 0.0, -9.905], [-41.07, 0.0, -10.1881], [-40.9885, 0.0, -10.4676], [-21.9255, 0.0, -9.1038], [-21.9255, 0.0, -3.5345], [-20.0463, 0.0, -44.6525], [-19.8744, 0.0, -44.4763], [-19.694, 0.0, -44.309], [-20.2091, 0.0, -44.837], [-19.5055, 0.0, -44.1508], [-20.3625, 0.0, -45.0294], [-19.3094, 0.0, -44.0022], [-20.5062, 0.0, -45.2292], [-19.106, 0.0, -43.8635], [-20.6397, 0.0, -45.436], [-18.896, 0.0, -43.7352], [-20.7628, 0.0, -45.6491], [-18.6799, 0.0, -43.6175], [-20.875, 0.0, -45.8681], [-18.4582, 0.0, -43.5108], [-20.9762, 0.0, -46.0924], [-18.2314, 0.0, -43.4152], [-21.0662, 0.0, -46.3215], [-18.0002, 0.0, -43.331], [-17.765, 0.0, -43.2585], [-17.5265, 0.0, -43.1977], [22.3077, 0.0, -3.5345], [-17.2853, 0.0, -43.149], [-17.042, 0.0, -43.1123], [-16.7971, 0.0, -43.0878], [-16.5513, 0.0, -43.0755], [-16.3052, 0.0, -43.0755], [-16.0594, 0.0, -43.0878], [-15.8146, 0.0, -43.1123], [-15.5712, 0.0, -43.149], [-15.33, 0.0, -43.1977], [-0.0, 0.0, -9.1038], [-15.0915, 0.0, -43.2585], [-14.8564, 0.0, -43.331], [-21.2112, 0.0, -49.2274], [-26.7105, 0.0, -55.2632], [-21.1445, 0.0, -49.4643], [-21.2659, 0.0, -48.9875], [-21.3087, 0.0, -48.7451], [-21.3393, 0.0, -48.5009], [-21.3577, 0.0, -48.2555], [-21.3638, 0.0, -48.0095], [-21.3577, 0.0, -47.7635], [-21.3393, 0.0, -47.5181], [-21.3087, 0.0, -47.2739], [-21.2659, 0.0, -47.0316], [-21.2112, 0.0, -46.7916], [-21.1445, 0.0, -46.5547], [-21.0662, 0.0, -49.6976], [-14.6251, 0.0, -43.4152], [-20.9762, 0.0, -49.9266], [-14.3984, 0.0, -43.5108], [-20.875, 0.0, -50.151], [-14.1766, 0.0, -43.6175], [-20.7628, 0.0, -50.37], [-13.9605, 0.0, -43.7352], [-20.6397, 0.0, -50.5831], [-13.7505, 0.0, -43.8635], [-20.5062, 0.0, -50.7898], [-13.5472, 0.0, -44.0022], [-20.3625, 0.0, -50.9896], [-13.351, 0.0, -44.1508], [-20.2091, 0.0, -51.182], [-13.1625, 0.0, -44.309], [-20.0463, 0.0, -51.3665], [-19.8744, 0.0, -51.5427], [-19.694, 0.0, -51.7101], [-19.5055, 0.0, -51.8683], [-19.3094, 0.0, -52.0169], [-19.106, 0.0, -52.1555], [35.0, 0.0, 0.0], [19.0522, 0.0, -18.4636], [16.4743, 0.0, -9.1399], [-27.6672, 0.0, -65.6993], [-27.6141, 0.0, -65.7466], [-27.7179, 0.0, -65.6494], [-27.5588, 0.0, -65.7913], [-27.4419, 0.0, -65.8722], [-27.3806, 0.0, -65.9082], [-27.5013, 0.0, -65.8332], [-27.3177, 0.0, -65.9412], [-27.2531, 0.0, -65.9711], [-27.1872, 0.0, -65.9978], [-27.1201, 0.0, -66.0212], [-27.0519, 0.0, -66.0413], [-26.9828, 0.0, -66.058], [-26.9129, 0.0, -66.0713], [-26.8425, 0.0, -66.0811], [-26.7007, 0.0, -66.0904], [-26.7717, 0.0, -66.0875], [-26.6296, 0.0, -66.0899], [-26.4879, 0.0, -66.0782], [-26.4177, 0.0, -66.0672], [-26.5586, 0.0, -66.0858], [-26.3481, 0.0, -66.0528], [-26.2792, 0.0, -66.035], [-26.2114, 0.0, -66.0138], [-26.1446, 0.0, -65.9893], [-26.0792, 0.0, -65.9615], [-26.0151, 0.0, -65.9306], [-25.9527, 0.0, -65.8966], [-25.892, 0.0, -65.8595], [-25.8332, 0.0, -65.8196], [-25.7765, 0.0, -65.7767], [-25.7219, 0.0, -65.7312], [-25.6696, 0.0, -65.683], [-28.9439, 0.0, -64.3825], [-29.1102, 0.0, -64.202], [-29.2675, 0.0, -64.0136], [-29.4153, 0.0, -63.8176], [-29.5533, 0.0, -63.6147], [-29.6811, 0.0, -63.4052], [-29.7986, 0.0, -63.1898], [-29.9053, 0.0, -62.9688], [-30.0011, 0.0, -62.7428], [-30.0856, 0.0, -62.5124], [-30.1588, 0.0, -62.2782], [-30.2204, 0.0, -62.0406], [-30.2702, 0.0, -61.8003], [-30.3082, 0.0, -61.5579], [-30.3342, 0.0, -61.3138], [-30.3483, 0.0, -61.0688], [-30.3503, 0.0, -60.8234], [-30.3402, 0.0, -60.5782], [-30.3182, 0.0, -60.3338], [-30.2841, 0.0, -60.0908], [-30.2382, 0.0, -59.8497], [-30.1805, 0.0, -59.6111], [-30.1112, 0.0, -59.3757], [-30.0304, 0.0, -59.144], [-29.9383, 0.0, -58.9165], [-29.8352, 0.0, -58.6938], [-29.7213, 0.0, -58.4764], [-29.5969, 0.0, -58.2649], [-13.7505, 0.0, -52.1555], [-20.6129, 0.0, -60.7895], [-13.5472, 0.0, -52.0169], [-13.9605, 0.0, -52.2838], [-14.1766, 0.0, -52.4015], [-14.3984, 0.0, -52.5083], [-14.6251, 0.0, -52.6039], [-14.8564, 0.0, -52.688], [-15.0915, 0.0, -52.7606], [-15.33, 0.0, -52.8213], [-15.5712, 0.0, -52.8701], [-15.8146, 0.0, -52.9067], [-16.0594, 0.0, -52.9312], [-16.3052, 0.0, -52.9435], [-16.5513, 0.0, -52.9435], [-16.7971, 0.0, -52.9312], [-17.042, 0.0, -52.9067], [-17.2853, 0.0, -52.8701], [-17.5265, 0.0, -52.8213], [-17.765, 0.0, -52.7606], [-18.0002, 0.0, -52.688], [-18.2314, 0.0, -52.6039], [-18.4582, 0.0, -52.5083], [-18.6799, 0.0, -52.4015], [-18.896, 0.0, -52.2838], [-29.4622, 0.0, -58.0598], [-29.3176, 0.0, -57.8615], [-29.1635, 0.0, -57.6705], [-29.0002, 0.0, -57.4873], [-28.828, 0.0, -57.3124], [42.0711, 0.0, -7.0711], [-12.9821, 0.0, -44.4763], [12.9821, 0.0, -44.4763], [-12.8103, 0.0, -44.6525], [12.8103, 0.0, -44.6525], [-12.6474, 0.0, -44.837], [13.1625, 0.0, -44.309], [12.6474, 0.0, -44.837], [13.351, 0.0, -44.1508], [-12.494, 0.0, -45.0294], [12.494, 0.0, -45.0294], [13.5472, 0.0, -44.0022], [-12.3504, 0.0, -45.2292], [12.3504, 0.0, -45.2292], [-12.2168, 0.0, -45.436], [13.7505, 0.0, -43.8635], [-12.0938, 0.0, -45.6491], [12.2168, 0.0, -45.436], [13.9605, 0.0, -43.7352], [12.0938, 0.0, -45.6491], [14.1766, 0.0, -43.6175], [-11.9815, 0.0, -45.8681], [11.9815, 0.0, -45.8681], [14.3984, 0.0, -43.5108], [-11.8803, 0.0, -46.0924], [11.8803, 0.0, -46.0924], [14.6251, 0.0, -43.4152], [11.7904, 0.0, -46.3215], [-11.7904, 0.0, -46.3215], [-11.712, 0.0, -46.5547], [14.8564, 0.0, -43.331], [11.712, 0.0, -46.5547], [15.0915, 0.0, -43.2585], [-11.6454, 0.0, -46.7916], [11.6454, 0.0, -46.7916], [15.33, 0.0, -43.1977], [-11.5906, 0.0, -47.0316], [11.5906, 0.0, -47.0316], [15.5712, 0.0, -43.149], [-11.5479, 0.0, -47.2739], [11.5479, 0.0, -47.2739], [15.8146, 0.0, -43.1123], [-11.5173, 0.0, -47.5181], [11.5173, 0.0, -47.5181], [16.0594, 0.0, -43.0878], [11.4989, 0.0, -47.7635], [-11.4989, 0.0, -47.7635], [16.3052, 0.0, -43.0755], [11.4928, 0.0, -48.0095], [-11.4928, 0.0, -48.0095], [16.5513, 0.0, -43.0755], [-11.4989, 0.0, -48.2555], [11.4989, 0.0, -48.2555], [-11.5173, 0.0, -48.5009], [16.7971, 0.0, -43.0878], [11.5173, 0.0, -48.5009], [-11.5479, 0.0, -48.7451], [17.042, 0.0, -43.1123], [11.5479, 0.0, -48.7451], [17.2853, 0.0, -43.149], [11.5906, 0.0, -48.9875], [-11.5906, 0.0, -48.9875], [17.5265, 0.0, -43.1977], [11.6454, 0.0, -49.2274], [-11.6454, 0.0, -49.2274], [17.765, 0.0, -43.2585], [-11.712, 0.0, -49.4643], [11.712, 0.0, -49.4643], [-11.7904, 0.0, -49.6976], [18.0002, 0.0, -43.331], [11.7904, 0.0, -49.6976], [18.2314, 0.0, -43.4152], [-11.8803, 0.0, -49.9266], [11.8803, 0.0, -49.9266], [18.4582, 0.0, -43.5108], [-11.9815, 0.0, -50.151], [11.9815, 0.0, -50.151], [18.6799, 0.0, -43.6175], [-12.0938, 0.0, -50.37], [12.0938, 0.0, -50.37], [18.896, 0.0, -43.7352], [-12.2168, 0.0, -50.5831], [12.2168, 0.0, -50.5831], [-12.3504, 0.0, -50.7898], [19.106, 0.0, -43.8635], [12.3504, 0.0, -50.7898], [19.3094, 0.0, -44.0022], [-12.494, 0.0, -50.9896], [12.494, 0.0, -50.9896], [19.5055, 0.0, -44.1508], [-12.6474, 0.0, -51.182], [12.6474, 0.0, -51.182], [-12.8103, 0.0, -51.3665], [19.694, 0.0, -44.309], [12.8103, 0.0, -51.3665], [-12.9821, 0.0, -51.5427], [19.8744, 0.0, -44.4763], [-13.1625, 0.0, -51.7101], [12.9821, 0.0, -51.5427], [20.0463, 0.0, -44.6525], [13.1625, 0.0, -51.7101], [20.2091, 0.0, -44.837], [13.351, 0.0, -51.8683], [-13.351, 0.0, -51.8683], [20.3625, 0.0, -45.0294], [13.5472, 0.0, -52.0169], [20.5062, 0.0, -45.2292], [20.6397, 0.0, -45.436], [20.7628, 0.0, -45.6491], [20.875, 0.0, -45.8681], [20.9762, 0.0, -46.0924], [20.6129, 0.0, -60.7895], [18.896, 0.0, -52.2838], [19.106, 0.0, -52.1555], [18.6799, 0.0, -52.4015], [18.4582, 0.0, -52.5083], [18.2314, 0.0, -52.6039], [18.0002, 0.0, -52.688], [17.765, 0.0, -52.7606], [17.5265, 0.0, -52.8213], [17.2853, 0.0, -52.8701], [17.042, 0.0, -52.9067], [16.7971, 0.0, -52.9312], [16.5513, 0.0, -52.9435], [16.3052, 0.0, -52.9435], [16.0594, 0.0, -52.9312], [15.8146, 0.0, -52.9067], [15.5712, 0.0, -52.8701], [15.33, 0.0, -52.8213], [15.0915, 0.0, -52.7606], [14.8564, 0.0, -52.688], [14.6251, 0.0, -52.6039], [14.3984, 0.0, -52.5083], [14.1766, 0.0, -52.4015], [13.9605, 0.0, -52.2838], [13.7505, 0.0, -52.1555], [26.7105, 0.0, -55.2632], [21.3577, 0.0, -48.2555], [21.3638, 0.0, -48.0095], [21.3393, 0.0, -48.5009], [21.3087, 0.0, -48.7451], [21.2659, 0.0, -48.9875], [21.2112, 0.0, -49.2274], [21.1445, 0.0, -49.4643], [21.0662, 0.0, -49.6976], [20.9762, 0.0, -49.9266], [20.875, 0.0, -50.151], [20.7628, 0.0, -50.37], [20.6397, 0.0, -50.5831], [20.5062, 0.0, -50.7898], [20.3625, 0.0, -50.9896], [20.2091, 0.0, -51.182], [20.0463, 0.0, -51.3665], [19.8744, 0.0, -51.5427], [19.694, 0.0, -51.7101], [21.0662, 0.0, -46.3215], [19.5055, 0.0, -51.8683], [21.1445, 0.0, -46.5547], [19.3094, 0.0, -52.0169], [21.2112, 0.0, -46.7916], [21.2659, 0.0, -47.0316], [21.3087, 0.0, -47.2739], [21.3393, 0.0, -47.5181], [21.3577, 0.0, -47.7635], [26.7105, 0.0, -66.6904], [32.4211, 0.0, -60.7895], [-40.1196, 3.0, -5.2115], [-39.9409, 3.0, -4.9818], [-39.751, 3.0, -4.7612], [-39.5503, 3.0, -4.5503], [-35.3348, 3.0, -0.6912], [-35.8275, 3.0, -0.9866], [-35.5848, 3.0, -0.8329], [-36.0624, 3.0, -1.152], [-40.714, 3.0, -6.2105], [-40.5842, 3.0, -5.95], [-40.4417, 3.0, -5.6962], [-40.2867, 3.0, -5.4499], [-34.5473, 3.0, -0.3422], [-35.0781, 3.0, -0.5621], [-34.8154, 3.0, -0.4456], [-41.0236, 3.0, -7.0262], [-40.934, 3.0, -6.7493], [-40.8307, 3.0, -6.4772], [-33.9976, 3.0, -0.1754], [-34.2745, 3.0, -0.2521], [-36.5065, 3.0, -1.5165], [-36.2889, 3.0, -1.3288], [-41.2085, 3.0, -7.8789], [-41.161, 3.0, -7.5917], [-41.0994, 3.0, -7.3072], [-33.149, 3.0, -0.0282], [-33.7172, 3.0, -0.1125], [-33.4341, 3.0, -0.0634], [-36.7146, 3.0, -1.7146], [-41.2417, 3.0, -8.168], [-32.8624, 3.0, -0.0071], [-32.5752, 3.0, -0.0], [-41.2298, 3.0, -9.3304], [-41.2544, 3.0, -9.0404], [-41.2646, 3.0, -8.7495], [-41.2604, 3.0, -8.4585], [-41.07, 3.0, -10.1881], [-41.1374, 3.0, -9.905], [-41.1908, 3.0, -9.6189], [-40.9885, 3.0, -10.4676], [-21.9255, 3.0, -9.1038], [-21.9255, 3.0, -3.5345], [5.6569, 3.0, -13.2042], [-5.6569, 3.0, -13.2042], [-0.0, 3.0, -9.1038], [-20.0463, 3.0, -44.6525], [-19.8744, 3.0, -44.4763], [-19.694, 3.0, -44.309], [-20.2091, 3.0, -44.837], [-19.5055, 3.0, -44.1508], [-20.3625, 3.0, -45.0294], [-19.3094, 3.0, -44.0022], [-20.5062, 3.0, -45.2292], [-19.106, 3.0, -43.8635], [-20.6397, 3.0, -45.436], [-18.896, 3.0, -43.7352], [-20.7628, 3.0, -45.6491], [-18.6799, 3.0, -43.6175], [-20.875, 3.0, -45.8681], [-18.4582, 3.0, -43.5108], [-20.9762, 3.0, -46.0924], [-18.2314, 3.0, -43.4152], [-21.0662, 3.0, -46.3215], [-18.0002, 3.0, -43.331], [-17.765, 3.0, -43.2585], [16.4743, 3.0, -9.1399], [-17.5265, 3.0, -43.1977], [22.3077, 3.0, -3.5345], [-17.2853, 3.0, -43.149], [-17.042, 3.0, -43.1123], [-16.7971, 3.0, -43.0878], [-16.5513, 3.0, -43.0755], [-16.3052, 3.0, -43.0755], [-16.0594, 3.0, -43.0878], [-15.8146, 3.0, -43.1123], [-15.5712, 3.0, -43.149], [-15.33, 3.0, -43.1977], [-15.0915, 3.0, -43.2585], [-14.8564, 3.0, -43.331], [-26.7105, 3.0, -55.2632], [-21.2112, 3.0, -49.2274], [-21.1445, 3.0, -49.4643], [-21.2659, 3.0, -48.9875], [-21.3087, 3.0, -48.7451], [-21.3393, 3.0, -48.5009], [-21.3577, 3.0, -48.2555], [-21.3638, 3.0, -48.0095], [-21.3577, 3.0, -47.7635], [-21.3393, 3.0, -47.5181], [-21.3087, 3.0, -47.2739], [-21.2659, 3.0, -47.0316], [-21.2112, 3.0, -46.7916], [-21.1445, 3.0, -46.5547], [-21.0662, 3.0, -49.6976], [-14.6251, 3.0, -43.4152], [-20.9762, 3.0, -49.9266], [-14.3984, 3.0, -43.5108], [-20.875, 3.0, -50.151], [-14.1766, 3.0, -43.6175], [-20.7628, 3.0, -50.37], [-13.9605, 3.0, -43.7352], [-20.6397, 3.0, -50.5831], [-13.7505, 3.0, -43.8635], [-20.5062, 3.0, -50.7898], [-13.5472, 3.0, -44.0022], [-20.3625, 3.0, -50.9896], [-13.351, 3.0, -44.1508], [-20.2091, 3.0, -51.182], [-13.1625, 3.0, -44.309], [-20.0463, 3.0, -51.3665], [-12.9821, 3.0, -44.4763], [-19.8744, 3.0, -51.5427], [-19.694, 3.0, -51.7101], [-19.5055, 3.0, -51.8683], [-19.3094, 3.0, -52.0169], [-19.106, 3.0, -52.1555], [35.0, 3.0, 0.0], [19.0522, 3.0, -18.4636], [-27.6141, 3.0, -65.7466], [-27.6672, 3.0, -65.6993], [-27.7179, 3.0, -65.6494], [-27.5588, 3.0, -65.7913], [-27.3806, 3.0, -65.9082], [-27.4419, 3.0, -65.8722], [-27.5013, 3.0, -65.8332], [-27.2531, 3.0, -65.9711], [-27.3177, 3.0, -65.9412], [-27.1872, 3.0, -65.9978], [-27.0519, 3.0, -66.0413], [-27.1201, 3.0, -66.0212], [-26.9129, 3.0, -66.0713], [-26.9828, 3.0, -66.058], [-26.8425, 3.0, -66.0811], [-26.7007, 3.0, -66.0904], [-26.7717, 3.0, -66.0875], [-26.5586, 3.0, -66.0858], [-26.6296, 3.0, -66.0899], [-26.2792, 3.0, -66.035], [-26.3481, 3.0, -66.0528], [-26.4177, 3.0, -66.0672], [-26.4879, 3.0, -66.0782], [-26.1446, 3.0, -65.9893], [-26.2114, 3.0, -66.0138], [-26.0792, 3.0, -65.9615], [-25.9527, 3.0, -65.8966], [-26.0151, 3.0, -65.9306], [-25.8332, 3.0, -65.8196], [-25.892, 3.0, -65.8595], [-25.7765, 3.0, -65.7767], [-25.6696, 3.0, -65.683], [-25.7219, 3.0, -65.7312], [-28.9439, 3.0, -64.3825], [-29.1102, 3.0, -64.202], [-29.2675, 3.0, -64.0136], [-29.4153, 3.0, -63.8176], [-29.5533, 3.0, -63.6147], [-29.6811, 3.0, -63.4052], [-29.7986, 3.0, -63.1898], [-29.9053, 3.0, -62.9688], [-30.0011, 3.0, -62.7428], [-30.0856, 3.0, -62.5124], [-30.1588, 3.0, -62.2782], [-30.2204, 3.0, -62.0406], [-30.2702, 3.0, -61.8003], [-30.3082, 3.0, -61.5579], [-30.3342, 3.0, -61.3138], [-30.3483, 3.0, -61.0688], [-30.3503, 3.0, -60.8234], [-30.3402, 3.0, -60.5782], [-30.3182, 3.0, -60.3338], [-30.2841, 3.0, -60.0908], [-30.2382, 3.0, -59.8497], [-30.1805, 3.0, -59.6111], [-30.1112, 3.0, -59.3757], [-30.0304, 3.0, -59.144], [-29.9383, 3.0, -58.9165], [-29.8352, 3.0, -58.6938], [-29.7213, 3.0, -58.4764], [-29.5969, 3.0, -58.2649], [-20.6129, 3.0, -60.7895], [-13.9605, 3.0, -52.2838], [-13.7505, 3.0, -52.1555], [-14.1766, 3.0, -52.4015], [-14.3984, 3.0, -52.5083], [-14.6251, 3.0, -52.6039], [-14.8564, 3.0, -52.688], [-15.0915, 3.0, -52.7606], [-15.33, 3.0, -52.8213], [-15.5712, 3.0, -52.8701], [-15.8146, 3.0, -52.9067], [-16.0594, 3.0, -52.9312], [-16.3052, 3.0, -52.9435], [-16.5513, 3.0, -52.9435], [-16.7971, 3.0, -52.9312], [-17.042, 3.0, -52.9067], [-17.2853, 3.0, -52.8701], [-17.5265, 3.0, -52.8213], [-17.765, 3.0, -52.7606], [-18.0002, 3.0, -52.688], [-18.2314, 3.0, -52.6039], [-18.4582, 3.0, -52.5083], [-18.6799, 3.0, -52.4015], [-18.896, 3.0, -52.2838], [-29.4622, 3.0, -58.0598], [-29.3176, 3.0, -57.8615], [-29.1635, 3.0, -57.6705], [-29.0002, 3.0, -57.4873], [-28.828, 3.0, -57.3124], [-2.5595, 3.0, -49.5058], [-11.4989, 3.0, -48.2555], [-11.4928, 3.0, -48.0095], [-11.5173, 3.0, -48.5009], [-11.5479, 3.0, -48.7451], [-11.5906, 3.0, -48.9875], [-11.6454, 3.0, -49.2274], [-11.712, 3.0, -49.4643], [-11.7904, 3.0, -49.6976], [-11.8803, 3.0, -49.9266], [-11.9815, 3.0, -50.151], [-12.8103, 3.0, -44.6525], [-12.0938, 3.0, -50.37], [-12.6474, 3.0, -44.837], [-12.2168, 3.0, -50.5831], [-12.494, 3.0, -45.0294], [-12.3504, 3.0, -50.7898], [-12.3504, 3.0, -45.2292], [-12.494, 3.0, -50.9896], [-12.2168, 3.0, -45.436], [-12.6474, 3.0, -51.182], [-12.0938, 3.0, -45.6491], [-12.8103, 3.0, -51.3665], [-11.9815, 3.0, -45.8681], [-12.9821, 3.0, -51.5427], [-11.8803, 3.0, -46.0924], [-13.1625, 3.0, -51.7101], [-11.7904, 3.0, -46.3215], [-13.351, 3.0, -51.8683], [-11.712, 3.0, -46.5547], [-13.5472, 3.0, -52.0169], [-11.6454, 3.0, -46.7916], [-11.5906, 3.0, -47.0316], [-11.5479, 3.0, -47.2739], [-11.5173, 3.0, -47.5181], [-11.4989, 3.0, -47.7635], [42.0711, 3.0, -7.0711], [2.5595, 3.0, -49.5058], [12.9821, 3.0, -44.4763], [12.8103, 3.0, -44.6525], [13.1625, 3.0, -44.309], [12.6474, 3.0, -44.837], [13.351, 3.0, -44.1508], [12.494, 3.0, -45.0294], [13.5472, 3.0, -44.0022], [12.3504, 3.0, -45.2292], [13.7505, 3.0, -43.8635], [12.2168, 3.0, -45.436], [13.9605, 3.0, -43.7352], [12.0938, 3.0, -45.6491], [14.1766, 3.0, -43.6175], [11.9815, 3.0, -45.8681], [14.3984, 3.0, -43.5108], [11.8803, 3.0, -46.0924], [14.6251, 3.0, -43.4152], [11.7904, 3.0, -46.3215], [14.8564, 3.0, -43.331], [11.712, 3.0, -46.5547], [15.0915, 3.0, -43.2585], [11.6454, 3.0, -46.7916], [15.33, 3.0, -43.1977], [11.5906, 3.0, -47.0316], [15.5712, 3.0, -43.149], [11.5479, 3.0, -47.2739], [15.8146, 3.0, -43.1123], [11.5173, 3.0, -47.5181], [16.0594, 3.0, -43.0878], [11.4989, 3.0, -47.7635], [16.3052, 3.0, -43.0755], [11.4928, 3.0, -48.0095], [16.5513, 3.0, -43.0755], [11.4989, 3.0, -48.2555], [16.7971, 3.0, -43.0878], [11.5173, 3.0, -48.5009], [17.042, 3.0, -43.1123], [11.5479, 3.0, -48.7451], [17.2853, 3.0, -43.149], [11.5906, 3.0, -48.9875], [17.5265, 3.0, -43.1977], [11.6454, 3.0, -49.2274], [17.765, 3.0, -43.2585], [11.712, 3.0, -49.4643], [18.0002, 3.0, -43.331], [11.7904, 3.0, -49.6976], [18.2314, 3.0, -43.4152], [11.8803, 3.0, -49.9266], [18.4582, 3.0, -43.5108], [11.9815, 3.0, -50.151], [18.6799, 3.0, -43.6175], [12.0938, 3.0, -50.37], [18.896, 3.0, -43.7352], [12.2168, 3.0, -50.5831], [19.106, 3.0, -43.8635], [12.3504, 3.0, -50.7898], [19.3094, 3.0, -44.0022], [12.494, 3.0, -50.9896], [19.5055, 3.0, -44.1508], [12.6474, 3.0, -51.182], [19.694, 3.0, -44.309], [12.8103, 3.0, -51.3665], [19.8744, 3.0, -44.4763], [12.9821, 3.0, -51.5427], [20.0463, 3.0, -44.6525], [13.1625, 3.0, -51.7101], [20.2091, 3.0, -44.837], [13.351, 3.0, -51.8683], [20.3625, 3.0, -45.0294], [13.5472, 3.0, -52.0169], [20.5062, 3.0, -45.2292], [13.7505, 3.0, -52.1555], [20.6397, 3.0, -45.436], [20.7628, 3.0, -45.6491], [20.875, 3.0, -45.8681], [20.9762, 3.0, -46.0924], [20.6129, 3.0, -60.7895], [13.9605, 3.0, -52.2838], [14.1766, 3.0, -52.4015], [14.3984, 3.0, -52.5083], [14.6251, 3.0, -52.6039], [14.8564, 3.0, -52.688], [15.0915, 3.0, -52.7606], [15.33, 3.0, -52.8213], [15.5712, 3.0, -52.8701], [15.8146, 3.0, -52.9067], [16.0594, 3.0, -52.9312], [16.3052, 3.0, -52.9435], [16.5513, 3.0, -52.9435], [16.7971, 3.0, -52.9312], [17.042, 3.0, -52.9067], [17.2853, 3.0, -52.8701], [17.5265, 3.0, -52.8213], [17.765, 3.0, -52.7606], [18.0002, 3.0, -52.688], [18.2314, 3.0, -52.6039], [18.4582, 3.0, -52.5083], [18.6799, 3.0, -52.4015], [18.896, 3.0, -52.2838], [19.106, 3.0, -52.1555], [26.7105, 3.0, -55.2632], [21.3638, 3.0, -48.0095], [21.3577, 3.0, -47.7635], [21.3393, 3.0, -47.5181], [21.3087, 3.0, -47.2739], [21.2659, 3.0, -47.0316], [21.2112, 3.0, -46.7916], [21.1445, 3.0, -46.5547], [19.3094, 3.0, -52.0169], [21.0662, 3.0, -46.3215], [19.5055, 3.0, -51.8683], [19.694, 3.0, -51.7101], [19.8744, 3.0, -51.5427], [20.0463, 3.0, -51.3665], [20.2091, 3.0, -51.182], [20.3625, 3.0, -50.9896], [20.5062, 3.0, -50.7898], [20.6397, 3.0, -50.5831], [20.7628, 3.0, -50.37], [20.875, 3.0, -50.151], [20.9762, 3.0, -49.9266], [21.0662, 3.0, -49.6976], [21.1445, 3.0, -49.4643], [21.2112, 3.0, -49.2274], [21.2659, 3.0, -48.9875], [21.3087, 3.0, -48.7451], [21.3393, 3.0, -48.5009], [21.3577, 3.0, -48.2555], [26.7105, 3.0, -66.6904], [32.4211, 3.0, -60.7895], [-0.0, 17.2725, -18.399], [-0.0, 10.6576, -46.7187]], "f": [[0, 1, 2], [2, 1, 3], [4, 5, 6], [6, 5, 7], [8, 9, 10], [11, 12, 13], [13, 12, 5], [8, 14, 9], [1, 14, 8], [12, 15, 5], [14, 16, 17], [18, 19, 15], [19, 20, 15], [5, 20, 7], [15, 20, 5], [16, 21, 22], [14, 21, 16], [1, 21, 14], [23, 24, 25], [25, 24, 20], [21, 26, 27], [27, 26, 28], [1, 26, 21], [0, 26, 1], [0, 29, 26], [0, 30, 29], [31, 32, 33], [33, 32, 34], [34, 32, 35], [36, 37, 32], [24, 38, 20], [32, 38, 35], [37, 38, 32], [35, 38, 24], [20, 39, 7], [38, 39, 20], [7, 39, 0], [0, 40, 30], [39, 40, 0], [30, 40, 41], [39, 42, 43], [43, 44, 39], [39, 45, 42], [44, 46, 39], [39, 47, 45], [46, 48, 39], [39, 49, 47], [39, 50, 40], [48, 50, 39], [39, 51, 49], [50, 52, 40], [39, 53, 51], [52, 54, 40], [39, 55, 53], [54, 56, 40], [39, 57, 55], [56, 58, 40], [39, 59, 57], [58, 60, 40], [60, 61, 40], [61, 62, 40], [41, 63, 30], [62, 64, 40], [64, 65, 40], [65, 66, 40], [66, 67, 40], [67, 68, 40], [68, 69, 40], [69, 70, 40], [70, 71, 40], [71, 72, 40], [40, 72, 73], [72, 74, 73], [74, 75, 73], [76, 77, 78], [79, 77, 76], [80, 77, 79], [81, 77, 80], [82, 77, 81], [83, 77, 82], [84, 77, 83], [85, 77, 84], [86, 77, 85], [87, 77, 86], [88, 77, 87], [89, 77, 88], [59, 77, 89], [39, 77, 59], [77, 90, 78], [75, 91, 73], [77, 92, 90], [91, 93, 73], [77, 94, 92], [93, 95, 73], [77, 96, 94], [95, 97, 73], [77, 98, 96], [97, 99, 73], [77, 100, 98], [99, 101, 73], [77, 102, 100], [101, 103, 73], [77, 104, 102], [103, 105, 73], [77, 106, 104], [77, 107, 106], [77, 108, 107], [77, 109, 108], [77, 110, 109], [77, 111, 110], [63, 112, 30], [73, 113, 114], [115, 116, 117], [116, 118, 117], [119, 120, 121], [121, 120, 118], [122, 123, 120], [118, 123, 117], [120, 123, 118], [123, 124, 117], [125, 126, 124], [124, 126, 117], [127, 128, 126], [128, 129, 126], [129, 130, 126], [131, 130, 129], [130, 132, 126], [133, 134, 135], [135, 134, 132], [132, 136, 126], [134, 136, 132], [137, 138, 136], [138, 139, 136], [139, 140, 136], [140, 141, 136], [142, 143, 141], [141, 143, 136], [143, 144, 136], [136, 145, 126], [144, 145, 136], [146, 147, 145], [145, 147, 126], [117, 147, 148], [148, 147, 149], [149, 147, 150], [150, 147, 151], [151, 147, 152], [152, 147, 153], [153, 147, 154], [154, 147, 155], [155, 147, 156], [156, 147, 157], [157, 147, 158], [158, 147, 159], [159, 147, 160], [160, 147, 161], [161, 147, 162], [162, 147, 163], [163, 147, 164], [164, 147, 165], [165, 147, 166], [166, 147, 167], [167, 147, 168], [168, 147, 169], [169, 147, 170], [170, 147, 171], [171, 147, 172], [172, 147, 173], [173, 147, 174], [174, 147, 175], [126, 147, 117], [176, 177, 178], [179, 177, 176], [180, 177, 179], [181, 177, 180], [182, 177, 181], [183, 177, 182], [184, 177, 183], [185, 177, 184], [186, 177, 185], [187, 177, 186], [188, 177, 187], [189, 177, 188], [190, 177, 189], [191, 177, 190], [192, 177, 191], [193, 177, 192], [194, 177, 193], [195, 177, 194], [77, 177, 111], [196, 177, 195], [197, 177, 196], [198, 177, 197], [199, 177, 198], [200, 177, 199], [111, 177, 200], [175, 177, 201], [201, 177, 202], [202, 177, 203], [203, 177, 204], [204, 177, 205], [205, 177, 77], [147, 177, 175], [113, 206, 63], [63, 206, 112], [207, 208, 105], [209, 210, 207], [211, 210, 209], [207, 210, 208], [105, 212, 73], [208, 212, 105], [211, 213, 210], [212, 214, 73], [215, 216, 211], [211, 216, 213], [73, 217, 113], [214, 217, 73], [218, 219, 215], [220, 219, 218], [215, 219, 216], [217, 221, 113], [222, 223, 220], [220, 223, 219], [221, 224, 113], [222, 225, 223], [224, 226, 113], [227, 228, 222], [222, 228, 225], [226, 229, 113], [230, 231, 227], [227, 231, 228], [229, 232, 113], [230, 233, 231], [234, 233, 230], [235, 233, 234], [232, 236, 113], [235, 237, 233], [236, 238, 113], [239, 240, 235], [235, 240, 237], [238, 241, 113], [242, 243, 239], [239, 243, 240], [241, 244, 113], [245, 246, 242], [242, 246, 243], [244, 247, 113], [248, 249, 245], [245, 249, 246], [247, 250, 113], [248, 251, 249], [252, 251, 248], [250, 253, 113], [252, 254, 251], [255, 254, 252], [253, 256, 113], [257, 258, 255], [259, 258, 257], [255, 258, 254], [256, 260, 113], [259, 261, 258], [262, 261, 259], [260, 263, 113], [262, 264, 261], [263, 265, 113], [262, 266, 264], [267, 266, 262], [265, 268, 113], [267, 269, 266], [270, 269, 267], [268, 271, 113], [272, 273, 270], [274, 273, 272], [270, 273, 269], [271, 275, 113], [274, 276, 273], [275, 277, 113], [278, 279, 274], [274, 279, 276], [277, 280, 113], [281, 282, 278], [278, 282, 279], [280, 283, 113], [284, 285, 281], [281, 285, 282], [283, 286, 113], [287, 288, 284], [289, 288, 287], [284, 288, 285], [286, 290, 113], [289, 291, 288], [290, 292, 113], [293, 294, 289], [289, 294, 291], [292, 295, 113], [296, 297, 293], [298, 297, 296], [293, 297, 294], [295, 299, 113], [298, 300, 297], [301, 300, 298], [299, 302, 113], [303, 304, 301], [301, 304, 300], [302, 305, 113], [303, 306, 304], [305, 307, 113], [303, 308, 306], [309, 308, 303], [178, 308, 309], [113, 310, 206], [307, 310, 113], [178, 311, 308], [310, 312, 206], [312, 313, 206], [313, 314, 206], [314, 315, 206], [315, 316, 206], [177, 317, 178], [318, 317, 319], [320, 317, 318], [321, 317, 320], [322, 317, 321], [323, 317, 322], [324, 317, 323], [325, 317, 324], [326, 317, 325], [327, 317, 326], [178, 317, 311], [328, 317, 327], [329, 317, 328], [330, 317, 329], [331, 317, 330], [332, 317, 331], [333, 317, 332], [334, 317, 333], [335, 317, 334], [336, 317, 335], [337, 317, 336], [338, 317, 337], [339, 317, 338], [340, 317, 339], [341, 317, 340], [311, 317, 341], [316, 342, 206], [343, 342, 344], [345, 342, 343], [346, 342, 345], [347, 342, 346], [348, 342, 347], [349, 342, 348], [350, 342, 349], [351, 342, 350], [352, 342, 351], [353, 342, 352], [354, 342, 353], [355, 342, 354], [356, 342, 355], [357, 342, 356], [317, 342, 319], [358, 342, 357], [359, 342, 358], [360, 342, 359], [361, 342, 316], [362, 342, 360], [363, 342, 361], [364, 342, 362], [365, 342, 363], [319, 342, 364], [366, 342, 365], [367, 342, 366], [368, 342, 367], [369, 342, 368], [344, 342, 369], [342, 370, 371], [317, 370, 342], [372, 373, 374], [372, 374, 375], [376, 377, 378], [376, 379, 377], [380, 381, 382], [380, 382, 383], [380, 383, 372], [384, 385, 386], [387, 380, 372], [387, 388, 389], [387, 389, 380], [387, 372, 375], [390, 379, 376], [390, 376, 385], [390, 384, 391], [390, 385, 384], [390, 392, 393], [390, 393, 379], [394, 395, 396], [394, 396, 387], [397, 390, 398], [397, 398, 399], [397, 392, 390], [397, 400, 392], [401, 387, 375], [401, 394, 387], [402, 400, 397], [403, 400, 402], [404, 405, 406], [404, 406, 407], [404, 407, 401], [408, 401, 375], [408, 404, 401], [408, 409, 410], [408, 410, 404], [411, 408, 375], [411, 375, 400], [412, 411, 400], [412, 400, 403], [412, 403, 413], [414, 415, 416], [417, 411, 418], [419, 418, 411], [420, 411, 417], [421, 419, 411], [422, 411, 420], [423, 421, 411], [424, 411, 422], [425, 411, 412], [425, 423, 411], [426, 411, 424], [427, 425, 412], [428, 411, 426], [429, 427, 412], [430, 411, 428], [431, 429, 412], [432, 411, 430], [433, 431, 412], [434, 411, 432], [435, 433, 412], [436, 435, 412], [437, 414, 416], [438, 436, 412], [439, 413, 403], [440, 438, 412], [441, 440, 412], [442, 441, 412], [443, 442, 412], [443, 412, 415], [444, 443, 415], [445, 444, 415], [446, 445, 415], [447, 446, 415], [448, 447, 415], [449, 448, 415], [450, 449, 415], [451, 411, 434], [451, 452, 453], [451, 454, 452], [451, 455, 454], [451, 456, 455], [451, 457, 456], [451, 458, 457], [451, 459, 458], [451, 460, 459], [451, 461, 460], [451, 462, 461], [451, 463, 462], [451, 464, 463], [451, 434, 464], [465, 451, 453], [466, 450, 415], [467, 451, 465], [468, 466, 415], [469, 451, 467], [470, 468, 415], [471, 451, 469], [472, 470, 415], [473, 451, 471], [474, 472, 415], [475, 451, 473], [476, 474, 415], [477, 451, 475], [478, 476, 415], [479, 451, 477], [480, 478, 415], [481, 451, 479], [482, 480, 415], [483, 451, 481], [484, 451, 483], [485, 451, 484], [486, 451, 485], [487, 451, 486], [488, 439, 403], [489, 414, 437], [490, 491, 492], [493, 490, 492], [494, 495, 496], [494, 496, 493], [497, 493, 492], [497, 498, 494], [497, 494, 493], [499, 497, 492], [500, 501, 499], [500, 499, 492], [502, 503, 500], [504, 502, 500], [505, 506, 504], [505, 504, 500], [507, 508, 505], [509, 510, 511], [509, 511, 512], [509, 512, 507], [513, 514, 509], [515, 507, 505], [515, 509, 507], [515, 513, 509], [516, 517, 515], [516, 515, 505], [518, 519, 516], [518, 516, 505], [520, 505, 500], [520, 518, 505], [521, 522, 520], [521, 500, 492], [521, 492, 523], [521, 520, 500], [521, 523, 524], [521, 524, 525], [521, 525, 526], [521, 526, 527], [521, 527, 528], [521, 528, 529], [521, 529, 530], [521, 530, 531], [521, 531, 532], [521, 532, 533], [521, 533, 534], [521, 534, 535], [521, 535, 536], [521, 536, 537], [521, 537, 538], [521, 538, 539], [521, 539, 540], [521, 540, 541], [521, 541, 542], [521, 542, 543], [521, 543, 544], [521, 544, 545], [521, 545, 546], [521, 546, 547], [521, 547, 548], [521, 548, 549], [521, 549, 550], [551, 552, 553], [551, 554, 552], [551, 555, 554], [551, 556, 555], [551, 557, 556], [551, 558, 557], [551, 559, 558], [551, 560, 559], [551, 561, 560], [551, 562, 561], [551, 563, 562], [551, 564, 563], [551, 451, 487], [551, 565, 564], [551, 566, 565], [551, 567, 566], [551, 568, 567], [551, 569, 568], [551, 570, 569], [551, 571, 570], [551, 572, 571], [551, 573, 572], [551, 574, 573], [551, 487, 574], [551, 550, 575], [551, 575, 576], [551, 576, 577], [551, 577, 578], [551, 578, 579], [551, 579, 451], [551, 521, 550], [580, 482, 415], [580, 581, 582], [580, 583, 581], [580, 584, 583], [580, 585, 584], [580, 586, 585], [580, 587, 586], [580, 588, 587], [580, 589, 588], [580, 590, 589], [580, 591, 482], [580, 592, 590], [580, 593, 591], [580, 594, 592], [580, 595, 593], [580, 596, 594], [580, 597, 595], [580, 598, 596], [580, 599, 597], [580, 600, 598], [580, 601, 599], [580, 602, 600], [580, 603, 601], [580, 604, 602], [580, 605, 603], [580, 606, 604], [580, 607, 605], [580, 608, 606], [580, 609, 607], [580, 610, 608], [580, 611, 609], [580, 553, 610], [580, 612, 611], [580, 613, 612], [580, 614, 613], [580, 615, 614], [580, 582, 615], [580, 551, 553], [616, 439, 488], [616, 489, 439], [617, 551, 580], [618, 617, 414], [619, 617, 618], [620, 618, 414], [621, 617, 619], [622, 620, 414], [623, 617, 621], [624, 622, 414], [625, 617, 623], [626, 624, 414], [627, 617, 625], [628, 626, 414], [629, 617, 627], [630, 414, 489], [630, 628, 414], [631, 617, 629], [632, 630, 489], [633, 617, 631], [634, 632, 489], [635, 617, 633], [636, 634, 489], [637, 617, 635], [638, 636, 489], [639, 617, 637], [640, 638, 489], [641, 617, 639], [642, 640, 489], [643, 617, 641], [644, 642, 489], [645, 617, 643], [646, 644, 489], [647, 617, 645], [648, 646, 489], [649, 617, 647], [650, 648, 489], [651, 617, 649], [652, 650, 489], [653, 617, 651], [654, 652, 489], [655, 617, 653], [656, 654, 489], [657, 617, 655], [658, 656, 489], [659, 617, 657], [660, 658, 489], [661, 617, 659], [662, 660, 489], [663, 617, 661], [664, 662, 489], [665, 617, 663], [666, 664, 489], [667, 617, 665], [668, 666, 489], [669, 617, 667], [670, 668, 489], [671, 617, 669], [672, 670, 489], [673, 617, 671], [674, 672, 489], [675, 617, 673], [676, 674, 489], [677, 617, 675], [678, 676, 489], [679, 617, 677], [680, 678, 489], [681, 617, 679], [682, 680, 489], [683, 617, 681], [684, 682, 489], [685, 617, 683], [686, 489, 616], [686, 684, 489], [687, 617, 685], [688, 686, 616], [689, 617, 687], [690, 688, 616], [691, 690, 616], [692, 691, 616], [693, 692, 616], [694, 617, 689], [694, 689, 695], [694, 695, 696], [694, 696, 697], [694, 697, 698], [694, 698, 699], [694, 699, 700], [694, 700, 701], [694, 701, 702], [694, 702, 703], [694, 703, 704], [694, 704, 705], [694, 705, 706], [694, 706, 707], [694, 707, 708], [694, 708, 709], [694, 709, 710], [694, 710, 711], [694, 711, 712], [694, 712, 713], [694, 713, 714], [694, 714, 715], [694, 715, 716], [694, 716, 717], [694, 551, 617], [718, 693, 616], [718, 694, 717], [718, 719, 720], [718, 720, 721], [718, 721, 722], [718, 722, 723], [718, 723, 724], [718, 724, 725], [718, 717, 726], [718, 725, 727], [718, 726, 728], [718, 727, 693], [718, 728, 729], [718, 729, 730], [718, 730, 731], [718, 731, 732], [718, 732, 733], [718, 733, 734], [718, 734, 735], [718, 735, 736], [718, 736, 737], [718, 737, 738], [718, 738, 739], [718, 739, 740], [718, 740, 741], [718, 741, 742], [718, 742, 743], [718, 743, 744], [718, 744, 745], [718, 745, 719], [746, 718, 747], [746, 694, 718], [412, 416, 415], [718, 616, 342], [342, 616, 206], [747, 718, 371], [371, 718, 342], [746, 747, 370], [370, 747, 371], [694, 746, 317], [317, 746, 370], [694, 317, 551], [551, 317, 177], [147, 551, 177], [521, 551, 147], [115, 117, 492], [115, 492, 491], [116, 491, 490], [116, 115, 491], [118, 490, 493], [118, 116, 490], [121, 493, 496], [121, 118, 493], [119, 496, 495], [119, 121, 496], [120, 495, 494], [120, 119, 495], [122, 494, 498], [122, 120, 494], [123, 122, 498], [123, 498, 497], [124, 123, 497], [124, 499, 501], [124, 497, 499], [125, 124, 501], [126, 125, 501], [126, 501, 500], [127, 126, 500], [127, 503, 502], [127, 500, 503], [128, 127, 502], [129, 128, 502], [129, 502, 504], [131, 129, 504], [131, 504, 506], [130, 131, 506], [130, 506, 505], [132, 130, 505], [132, 505, 508], [135, 132, 508], [135, 507, 512], [135, 508, 507], [133, 512, 511], [133, 135, 512], [134, 133, 511], [136, 510, 509], [136, 511, 510], [136, 134, 511], [137, 509, 514], [137, 136, 509], [138, 137, 514], [139, 514, 513], [139, 138, 514], [140, 513, 515], [140, 139, 513], [141, 515, 517], [141, 140, 515], [142, 516, 519], [142, 517, 516], [142, 141, 517], [143, 142, 519], [144, 519, 518], [144, 143, 519], [145, 518, 520], [145, 144, 518], [146, 520, 522], [146, 145, 520], [147, 522, 521], [147, 146, 522], [492, 117, 148], [523, 492, 148], [204, 205, 579], [204, 579, 578], [203, 578, 577], [203, 204, 578], [202, 577, 576], [202, 203, 577], [201, 576, 575], [201, 202, 576], [175, 575, 550], [175, 201, 575], [174, 550, 549], [174, 175, 550], [173, 549, 548], [173, 174, 549], [172, 173, 548], [172, 548, 547], [171, 172, 547], [171, 547, 546], [170, 171, 546], [170, 546, 545], [169, 170, 545], [169, 545, 544], [168, 169, 544], [168, 544, 543], [167, 168, 543], [167, 542, 541], [167, 543, 542], [166, 167, 541], [165, 166, 541], [165, 541, 540], [164, 165, 540], [164, 540, 539], [163, 164, 539], [163, 539, 538], [162, 163, 538], [162, 538, 537], [161, 537, 536], [161, 162, 537], [160, 536, 535], [160, 161, 536], [159, 160, 535], [159, 535, 534], [158, 533, 532], [158, 534, 533], [158, 159, 534], [157, 158, 532], [156, 532, 531], [156, 157, 532], [155, 531, 530], [155, 156, 531], [154, 529, 528], [154, 530, 529], [154, 155, 530], [153, 154, 528], [152, 528, 527], [152, 153, 528], [151, 527, 526], [151, 152, 527], [150, 526, 525], [150, 151, 526], [149, 525, 524], [149, 150, 525], [148, 524, 523], [148, 149, 524], [451, 205, 77], [451, 579, 205], [411, 77, 39], [411, 451, 77], [6, 7, 375], [6, 375, 374], [4, 374, 373], [4, 6, 374], [5, 372, 383], [5, 373, 372], [5, 4, 373], [13, 5, 383], [11, 383, 382], [11, 13, 383], [12, 382, 381], [12, 11, 382], [15, 381, 380], [15, 12, 381], [18, 389, 388], [18, 380, 389], [18, 15, 380], [19, 18, 388], [20, 388, 387], [20, 19, 388], [25, 387, 396], [25, 20, 387], [23, 396, 395], [23, 25, 396], [24, 23, 395], [24, 394, 401], [24, 395, 394], [35, 24, 401], [34, 35, 401], [34, 401, 407], [33, 34, 407], [33, 407, 406], [31, 33, 406], [31, 405, 404], [31, 406, 405], [32, 31, 404], [36, 404, 410], [36, 32, 404], [37, 36, 410], [37, 410, 409], [38, 409, 408], [38, 37, 409], [39, 408, 411], [39, 38, 408], [400, 7, 0], [400, 375, 7], [29, 30, 403], [29, 403, 402], [26, 402, 397], [26, 29, 402], [28, 397, 399], [28, 26, 397], [27, 398, 390], [27, 399, 398], [27, 28, 399], [21, 27, 390], [22, 390, 391], [22, 21, 390], [16, 391, 384], [16, 22, 391], [17, 384, 386], [17, 16, 384], [14, 386, 385], [14, 17, 386], [9, 385, 376], [9, 14, 385], [10, 376, 378], [10, 9, 376], [8, 377, 379], [8, 378, 377], [8, 10, 378], [1, 8, 379], [3, 379, 393], [3, 1, 379], [2, 393, 392], [2, 3, 393], [0, 392, 400], [0, 2, 392], [112, 488, 403], [112, 403, 30], [616, 112, 206], [616, 488, 112], [748, 415, 414], [414, 617, 749], [748, 414, 749], [749, 617, 580], [749, 415, 748], [749, 580, 415], [582, 581, 255], [255, 581, 257], [257, 583, 259], [259, 583, 262], [581, 583, 257], [583, 584, 262], [262, 585, 267], [267, 585, 270], [584, 585, 262], [270, 586, 272], [585, 586, 270], [586, 587, 272], [272, 588, 274], [587, 588, 272], [274, 589, 278], [278, 589, 281], [588, 589, 274], [589, 590, 281], [281, 592, 284], [590, 592, 281], [592, 594, 284], [284, 594, 287], [594, 596, 287], [287, 596, 289], [596, 598, 289], [289, 598, 293], [598, 600, 293], [293, 600, 296], [600, 602, 296], [296, 602, 298], [602, 604, 298], [298, 604, 301], [604, 606, 301], [301, 606, 303], [606, 608, 303], [303, 608, 309], [608, 610, 309], [309, 610, 178], [610, 553, 178], [178, 553, 176], [176, 553, 179], [553, 552, 179], [552, 554, 179], [179, 554, 180], [180, 554, 181], [554, 555, 181], [181, 556, 182], [182, 556, 183], [555, 556, 181], [556, 557, 183], [183, 558, 184], [557, 558, 183], [184, 559, 185], [185, 559, 186], [558, 559, 184], [186, 560, 187], [559, 560, 186], [560, 561, 187], [187, 562, 188], [188, 562, 189], [561, 562, 187], [562, 563, 189], [189, 564, 190], [190, 564, 191], [563, 564, 189], [564, 565, 191], [191, 566, 192], [192, 566, 193], [565, 566, 191], [566, 567, 193], [567, 568, 193], [193, 568, 194], [568, 569, 194], [194, 569, 195], [569, 570, 195], [195, 570, 196], [570, 571, 196], [196, 571, 197], [571, 572, 197], [197, 572, 198], [198, 572, 199], [572, 573, 199], [573, 574, 199], [199, 574, 200], [574, 487, 200], [200, 487, 111], [487, 486, 111], [111, 486, 110], [486, 485, 110], [110, 485, 109], [485, 484, 109], [109, 484, 108], [484, 483, 108], [108, 483, 107], [483, 481, 107], [107, 481, 106], [481, 479, 106], [106, 479, 104], [479, 477, 104], [104, 477, 102], [477, 475, 102], [102, 475, 100], [475, 473, 100], [100, 473, 98], [473, 471, 98], [98, 471, 96], [471, 469, 96], [96, 469, 94], [469, 467, 94], [94, 467, 92], [467, 465, 92], [92, 465, 90], [465, 453, 90], [90, 453, 78], [78, 452, 76], [453, 452, 78], [76, 454, 79], [452, 454, 76], [79, 455, 80], [454, 455, 79], [80, 456, 81], [455, 456, 80], [456, 457, 81], [81, 457, 82], [457, 458, 82], [82, 458, 83], [458, 459, 83], [83, 459, 84], [459, 460, 84], [84, 460, 85], [460, 461, 85], [85, 461, 86], [461, 462, 86], [86, 462, 87], [462, 463, 87], [87, 463, 88], [463, 464, 88], [88, 464, 89], [464, 434, 89], [89, 434, 59], [434, 432, 59], [59, 432, 57], [432, 430, 57], [57, 430, 55], [430, 428, 55], [55, 428, 53], [428, 426, 53], [53, 426, 51], [51, 424, 49], [426, 424, 51], [49, 422, 47], [424, 422, 49], [47, 420, 45], [422, 420, 47], [45, 417, 42], [420, 417, 45], [42, 418, 43], [417, 418, 42], [43, 419, 44], [418, 419, 43], [44, 421, 46], [419, 421, 44], [46, 423, 48], [421, 423, 46], [48, 425, 50], [423, 425, 48], [50, 427, 52], [425, 427, 50], [52, 429, 54], [427, 429, 52], [54, 431, 56], [429, 431, 54], [56, 433, 58], [431, 433, 56], [433, 435, 58], [58, 435, 60], [435, 436, 60], [60, 436, 61], [436, 438, 61], [61, 438, 62], [438, 440, 62], [62, 440, 64], [64, 441, 65], [440, 441, 64], [65, 442, 66], [66, 442, 67], [441, 442, 65], [442, 443, 67], [67, 444, 68], [443, 444, 67], [68, 445, 69], [444, 445, 68], [69, 446, 70], [445, 446, 69], [70, 447, 71], [446, 447, 70], [71, 448, 72], [72, 448, 74], [447, 448, 71], [448, 449, 74], [74, 450, 75], [449, 450, 74], [75, 466, 91], [450, 466, 75], [91, 468, 93], [93, 468, 95], [466, 468, 91], [468, 470, 95], [95, 472, 97], [470, 472, 95], [97, 474, 99], [472, 474, 97], [99, 476, 101], [101, 476, 103], [474, 476, 99], [476, 478, 103], [103, 480, 105], [105, 480, 207], [478, 480, 103], [480, 482, 207], [207, 591, 209], [482, 591, 207], [591, 593, 209], [209, 593, 211], [593, 595, 211], [211, 595, 215], [595, 597, 215], [215, 597, 218], [597, 599, 218], [218, 599, 220], [599, 601, 220], [220, 601, 222], [601, 603, 222], [222, 603, 227], [603, 605, 227], [227, 605, 230], [605, 607, 230], [230, 607, 234], [607, 609, 234], [234, 609, 235], [609, 611, 235], [235, 611, 239], [611, 612, 239], [239, 612, 242], [612, 613, 242], [242, 613, 245], [245, 614, 248], [613, 614, 245], [614, 615, 248], [248, 615, 252], [252, 582, 255], [615, 582, 252], [719, 745, 344], [344, 745, 343], [343, 744, 345], [745, 744, 343], [345, 743, 346], [744, 743, 345], [346, 742, 347], [743, 742, 346], [347, 741, 348], [742, 741, 347], [348, 740, 349], [741, 740, 348], [349, 739, 350], [740, 739, 349], [350, 738, 351], [739, 738, 350], [351, 737, 352], [738, 737, 351], [352, 736, 353], [737, 736, 352], [736, 735, 353], [353, 735, 354], [735, 734, 354], [354, 734, 355], [734, 733, 355], [355, 733, 356], [733, 732, 356], [356, 732, 357], [732, 731, 357], [357, 731, 358], [731, 730, 358], [358, 730, 359], [730, 729, 359], [359, 729, 360], [729, 728, 360], [360, 728, 362], [728, 726, 362], [362, 726, 364], [726, 717, 364], [364, 717, 319], [717, 716, 319], [319, 716, 318], [716, 715, 318], [318, 715, 320], [320, 714, 321], [715, 714, 320], [321, 713, 322], [714, 713, 321], [322, 712, 323], [713, 712, 322], [323, 711, 324], [712, 711, 323], [324, 710, 325], [711, 710, 324], [325, 709, 326], [710, 709, 325], [326, 708, 327], [709, 708, 326], [327, 707, 328], [708, 707, 327], [328, 706, 329], [707, 706, 328], [329, 705, 330], [706, 705, 329], [330, 704, 331], [705, 704, 330], [331, 703, 332], [704, 703, 331], [332, 702, 333], [703, 702, 332], [333, 701, 334], [702, 701, 333], [701, 700, 334], [334, 700, 335], [700, 699, 335], [335, 699, 336], [699, 698, 336], [336, 698, 337], [698, 697, 337], [337, 697, 338], [697, 696, 338], [338, 696, 339], [696, 695, 339], [339, 695, 340], [695, 689, 340], [340, 689, 341], [689, 687, 341], [341, 687, 311], [687, 685, 311], [311, 685, 308], [685, 683, 308], [308, 683, 306], [683, 681, 306], [306, 681, 304], [681, 679, 304], [304, 679, 300], [679, 677, 300], [300, 677, 297], [677, 675, 297], [297, 675, 294], [675, 673, 294], [294, 673, 291], [673, 671, 291], [291, 671, 288], [671, 669, 288], [288, 669, 285], [669, 667, 285], [285, 667, 282], [667, 665, 282], [282, 665, 279], [665, 663, 279], [279, 663, 276], [663, 661, 276], [276, 661, 273], [273, 659, 269], [661, 659, 273], [269, 657, 266], [659, 657, 269], [266, 655, 264], [657, 655, 266], [264, 653, 261], [655, 653, 264], [653, 651, 261], [261, 651, 258], [651, 649, 258], [258, 649, 254], [649, 647, 254], [254, 647, 251], [647, 645, 251], [251, 645, 249], [645, 643, 249], [249, 643, 246], [643, 641, 246], [246, 641, 243], [641, 639, 243], [243, 639, 240], [639, 637, 240], [240, 637, 237], [637, 635, 237], [237, 635, 233], [635, 633, 233], [233, 633, 231], [633, 631, 231], [231, 631, 228], [631, 629, 228], [228, 629, 225], [223, 627, 219], [629, 627, 225], [225, 627, 223], [627, 625, 219], [219, 623, 216], [625, 623, 219], [216, 621, 213], [623, 621, 216], [213, 619, 210], [210, 619, 208], [621, 619, 213], [619, 618, 208], [208, 620, 212], [618, 620, 208], [212, 622, 214], [214, 622, 217], [620, 622, 212], [622, 624, 217], [217, 626, 221], [624, 626, 217], [221, 628, 224], [626, 628, 221], [224, 630, 226], [628, 630, 224], [226, 632, 229], [229, 632, 232], [630, 632, 226], [632, 634, 232], [634, 636, 232], [232, 636, 236], [236, 636, 238], [636, 638, 238], [238, 638, 241], [638, 640, 241], [241, 642, 244], [640, 642, 241], [244, 644, 247], [642, 644, 244], [247, 646, 250], [644, 646, 247], [250, 648, 253], [646, 648, 250], [253, 650, 256], [648, 650, 253], [256, 652, 260], [260, 652, 263], [650, 652, 256], [652, 654, 263], [263, 656, 265], [654, 656, 263], [265, 658, 268], [268, 658, 271], [656, 658, 265], [658, 660, 271], [271, 662, 275], [660, 662, 271], [275, 664, 277], [662, 664, 275], [277, 666, 280], [664, 666, 277], [280, 668, 283], [666, 668, 280], [283, 670, 286], [668, 670, 283], [286, 672, 290], [670, 672, 286], [290, 674, 292], [672, 674, 290], [292, 676, 295], [674, 676, 292], [295, 678, 299], [299, 678, 302], [676, 678, 295], [678, 680, 302], [302, 682, 305], [305, 682, 307], [680, 682, 302], [682, 684, 307], [684, 686, 307], [307, 686, 310], [686, 688, 310], [310, 688, 312], [688, 690, 312], [312, 690, 313], [690, 691, 313], [313, 691, 314], [691, 692, 314], [314, 692, 315], [315, 692, 316], [692, 693, 316], [693, 727, 316], [316, 727, 361], [727, 725, 361], [361, 725, 363], [725, 724, 363], [363, 724, 365], [724, 723, 365], [365, 723, 366], [723, 722, 366], [366, 722, 367], [722, 721, 367], [367, 721, 368], [368, 720, 369], [721, 720, 368], [369, 719, 344], [720, 719, 369], [437, 73, 114], [437, 416, 73], [40, 73, 412], [73, 416, 412], [40, 412, 41], [41, 412, 413], [439, 63, 413], [413, 63, 41], [63, 489, 113], [439, 489, 63], [113, 437, 114], [489, 437, 113]]} \ No newline at end of file diff --git a/docs/CAD/design/mate-connectors/circularmateconnectors.png b/docs/CAD/design/mate-connectors/circularmateconnectors.png deleted file mode 100644 index 3150a5667b..0000000000 Binary files a/docs/CAD/design/mate-connectors/circularmateconnectors.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/cmp-sheet.png b/docs/CAD/design/mate-connectors/cmp-sheet.png deleted file mode 100644 index 1d67ae31ed..0000000000 Binary files a/docs/CAD/design/mate-connectors/cmp-sheet.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/connector-glyph-proposal.html b/docs/CAD/design/mate-connectors/connector-glyph-proposal.html deleted file mode 100644 index 2a8475ee24..0000000000 --- a/docs/CAD/design/mate-connectors/connector-glyph-proposal.html +++ /dev/null @@ -1,299 +0,0 @@ - - - - - -Mate connector glyph — polarity and verse - - - -
- -
-
Orca Design · assembly
-

Mate connector glyph — polarity and verse

-

- Onshape's core (disc + roll quadrant + Z arrow) is adopted unchanged because it is proven and - aligned. The addition is polarity — which connector is anchored and - which one travels — which no surveyed CAD system encodes in its glyph. -

-
- -

The three jobs of the glyph

-
-
-
- - - - -
-
Disc — the XY plane
-

Says “I am a frame, and this is the plane I sit in.” The dot is the exact origin.

-
- -
-
- - - - - -
-
Quadrant — the roll
-

- The filled sector is the +X/+Y quadrant. It steps 90° with the reorient control, so the - clocking that Fastened and Slider lock is visible before you commit. -

-
- -
-
- - - - - - - -
-
Arrow — the verse
-

- Drawn on +Z only. Nothing below the disc. A double-headed axis is what - makes people ask which way it points; a one-sided arrow cannot be misread. -

-
-
- -

Polarity — the part nobody else draws

-
-
-
- - - - - - - - -
-
Fixed — the socket
-

- Hollow head, muted colour. This body does not move. It receives. -

-
- -
-
- - - - - - - -
-
Driven — the plug
-

- Solid head, active colour. This body is the one that jumps. It inserts. -

-
- -
-
- - - - - - - - - - - - -
-
Roll undefined
-

- Hatched quadrant, dashed disc: a circular face or a seam gave no usable direction. Says - “pick a direction” without a dialog. -

-
-
- -

The pair reads as a magnet

-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - fixed · receives - driven · inserts - -
-

- Two arrows nose to nose. Because a magnet's north seeks a south, “facing” is the - self-evident default — 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. -

-
- -

States

-
- - - - - - - - - - - - - - - - - - - - - -
StateDrawingWhy
Candidate (hover)small dot onlyOnshape draws plain white dots at every corner and midpoint. Dots propose; the full glyph commits.
Pickeddisc + quadrant + coneThe committed frame, with roll and verse both readable.
Roll undefinedhatched quadrant, dashed discTurns requirement R8 from a message nobody reads into a mark you cannot miss.
-
- -

Constraints on the drawing

-
-

- Do not make it a fourth RGB triad. The bed-centre world triad - (DesignCanvas.cpp:65) 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. -

-
- roll quadrant - Z / driven - fixed - roll undefined -
-
- -
- - diff --git a/docs/CAD/design/mate-connectors/coplanar_test.py b/docs/CAD/design/mate-connectors/coplanar_test.py deleted file mode 100644 index ea02ed3c80..0000000000 --- a/docs/CAD/design/mate-connectors/coplanar_test.py +++ /dev/null @@ -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") diff --git a/docs/CAD/design/mate-connectors/cylindricalmateconnectors.png b/docs/CAD/design/mate-connectors/cylindricalmateconnectors.png deleted file mode 100644 index e4c1cebcf9..0000000000 Binary files a/docs/CAD/design/mate-connectors/cylindricalmateconnectors.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/faceted_ridge_key.scad b/docs/CAD/design/mate-connectors/faceted_ridge_key.scad deleted file mode 100644 index 3eb60064fb..0000000000 --- a/docs/CAD/design/mate-connectors/faceted_ridge_key.scad +++ /dev/null @@ -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. diff --git a/docs/CAD/design/mate-connectors/fem-0.png b/docs/CAD/design/mate-connectors/fem-0.png deleted file mode 100644 index 391881e72a..0000000000 Binary files a/docs/CAD/design/mate-connectors/fem-0.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/fem-1.png b/docs/CAD/design/mate-connectors/fem-1.png deleted file mode 100644 index c329d20bea..0000000000 Binary files a/docs/CAD/design/mate-connectors/fem-1.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/fem-2.png b/docs/CAD/design/mate-connectors/fem-2.png deleted file mode 100644 index a32f2ffbf1..0000000000 Binary files a/docs/CAD/design/mate-connectors/fem-2.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/fem-3.png b/docs/CAD/design/mate-connectors/fem-3.png deleted file mode 100644 index f21ae3f98b..0000000000 Binary files a/docs/CAD/design/mate-connectors/fem-3.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/fem-sheet.png b/docs/CAD/design/mate-connectors/fem-sheet.png deleted file mode 100644 index add56590a1..0000000000 Binary files a/docs/CAD/design/mate-connectors/fem-sheet.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/fem-sheet2.png b/docs/CAD/design/mate-connectors/fem-sheet2.png deleted file mode 100644 index 879b4295fb..0000000000 Binary files a/docs/CAD/design/mate-connectors/fem-sheet2.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/female.stl b/docs/CAD/design/mate-connectors/female.stl deleted file mode 100644 index 57b0ef6d20..0000000000 --- a/docs/CAD/design/mate-connectors/female.stl +++ /dev/null @@ -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 diff --git a/docs/CAD/design/mate-connectors/female_only.scad b/docs/CAD/design/mate-connectors/female_only.scad deleted file mode 100644 index 89f83b4655..0000000000 --- a/docs/CAD/design/mate-connectors/female_only.scad +++ /dev/null @@ -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 - -// 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(); -} diff --git a/docs/CAD/design/mate-connectors/fit_check.py b/docs/CAD/design/mate-connectors/fit_check.py deleted file mode 100644 index 5a9ee01063..0000000000 --- a/docs/CAD/design/mate-connectors/fit_check.py +++ /dev/null @@ -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") diff --git a/docs/CAD/design/mate-connectors/glyph-preview.png b/docs/CAD/design/mate-connectors/glyph-preview.png deleted file mode 100644 index 5e62c9a17f..0000000000 Binary files a/docs/CAD/design/mate-connectors/glyph-preview.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/glyph_preview.py b/docs/CAD/design/mate-connectors/glyph_preview.py deleted file mode 100644 index e339e6bdab..0000000000 --- a/docs/CAD/design/mate-connectors/glyph_preview.py +++ /dev/null @@ -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") diff --git a/docs/CAD/design/mate-connectors/glyph_probe.py b/docs/CAD/design/mate-connectors/glyph_probe.py deleted file mode 100644 index f4d48fdd6c..0000000000 --- a/docs/CAD/design/mate-connectors/glyph_probe.py +++ /dev/null @@ -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)) diff --git a/docs/CAD/design/mate-connectors/handedness-sheet.png b/docs/CAD/design/mate-connectors/handedness-sheet.png deleted file mode 100644 index b44c370328..0000000000 Binary files a/docs/CAD/design/mate-connectors/handedness-sheet.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/handedness.py b/docs/CAD/design/mate-connectors/handedness.py deleted file mode 100644 index 68fa07bb55..0000000000 --- a/docs/CAD/design/mate-connectors/handedness.py +++ /dev/null @@ -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") diff --git a/docs/CAD/design/mate-connectors/linearedgemateconnectors.png b/docs/CAD/design/mate-connectors/linearedgemateconnectors.png deleted file mode 100644 index fd5e64190d..0000000000 Binary files a/docs/CAD/design/mate-connectors/linearedgemateconnectors.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/make_female.py b/docs/CAD/design/mate-connectors/make_female.py deleted file mode 100644 index ccb38426bf..0000000000 --- a/docs/CAD/design/mate-connectors/make_female.py +++ /dev/null @@ -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") diff --git a/docs/CAD/design/mate-connectors/mate-connector-virtual-sharp.png b/docs/CAD/design/mate-connectors/mate-connector-virtual-sharp.png deleted file mode 100644 index f9a80c1e10..0000000000 Binary files a/docs/CAD/design/mate-connectors/mate-connector-virtual-sharp.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/mateconnector-planarcentroid.png b/docs/CAD/design/mate-connectors/mateconnector-planarcentroid.png deleted file mode 100644 index 16ca1c01e6..0000000000 Binary files a/docs/CAD/design/mate-connectors/mateconnector-planarcentroid.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/mateconnector-planarpoints.png b/docs/CAD/design/mate-connectors/mateconnector-planarpoints.png deleted file mode 100644 index d952ddc1f0..0000000000 Binary files a/docs/CAD/design/mate-connectors/mateconnector-planarpoints.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/matepointiconLG.png b/docs/CAD/design/mate-connectors/matepointiconLG.png deleted file mode 100644 index 2f3022d116..0000000000 Binary files a/docs/CAD/design/mate-connectors/matepointiconLG.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/matepointreorientsecondaryaxis.png b/docs/CAD/design/mate-connectors/matepointreorientsecondaryaxis.png deleted file mode 100644 index bd65a9f40e..0000000000 Binary files a/docs/CAD/design/mate-connectors/matepointreorientsecondaryaxis.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/muzzle_variants.py b/docs/CAD/design/mate-connectors/muzzle_variants.py deleted file mode 100644 index f4c9ab33b5..0000000000 --- a/docs/CAD/design/mate-connectors/muzzle_variants.py +++ /dev/null @@ -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") diff --git a/docs/CAD/design/mate-connectors/planarfacemateconnectors.png b/docs/CAD/design/mate-connectors/planarfacemateconnectors.png deleted file mode 100644 index 9fea27ab2b..0000000000 Binary files a/docs/CAD/design/mate-connectors/planarfacemateconnectors.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/relief-sheet.png b/docs/CAD/design/mate-connectors/relief-sheet.png deleted file mode 100644 index 0c6d0d11f0..0000000000 Binary files a/docs/CAD/design/mate-connectors/relief-sheet.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/relief_sheet.py b/docs/CAD/design/mate-connectors/relief_sheet.py deleted file mode 100644 index 4b8bc6aa74..0000000000 --- a/docs/CAD/design/mate-connectors/relief_sheet.py +++ /dev/null @@ -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") diff --git a/docs/CAD/design/mate-connectors/relief_test.py b/docs/CAD/design/mate-connectors/relief_test.py deleted file mode 100644 index 3fb83b2a8e..0000000000 --- a/docs/CAD/design/mate-connectors/relief_test.py +++ /dev/null @@ -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)}") diff --git a/docs/CAD/design/mate-connectors/render_key.py b/docs/CAD/design/mate-connectors/render_key.py deleted file mode 100644 index eb4bcdbd99..0000000000 --- a/docs/CAD/design/mate-connectors/render_key.py +++ /dev/null @@ -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)) diff --git a/docs/CAD/design/mate-connectors/render_stl.py b/docs/CAD/design/mate-connectors/render_stl.py deleted file mode 100644 index 16ad0e92a9..0000000000 --- a/docs/CAD/design/mate-connectors/render_stl.py +++ /dev/null @@ -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(" 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") diff --git a/docs/CAD/design/mate-connectors/rk-0.png b/docs/CAD/design/mate-connectors/rk-0.png deleted file mode 100644 index 1d7208238a..0000000000 Binary files a/docs/CAD/design/mate-connectors/rk-0.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/rk-1.png b/docs/CAD/design/mate-connectors/rk-1.png deleted file mode 100644 index 282bd0a2fe..0000000000 Binary files a/docs/CAD/design/mate-connectors/rk-1.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/rk-2.png b/docs/CAD/design/mate-connectors/rk-2.png deleted file mode 100644 index 75d83e1402..0000000000 Binary files a/docs/CAD/design/mate-connectors/rk-2.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/rk-3.png b/docs/CAD/design/mate-connectors/rk-3.png deleted file mode 100644 index eccdf94807..0000000000 Binary files a/docs/CAD/design/mate-connectors/rk-3.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/rk-4.png b/docs/CAD/design/mate-connectors/rk-4.png deleted file mode 100644 index 794fd4bd97..0000000000 Binary files a/docs/CAD/design/mate-connectors/rk-4.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/rk-back.png b/docs/CAD/design/mate-connectors/rk-back.png deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs/CAD/design/mate-connectors/rk-front.png b/docs/CAD/design/mate-connectors/rk-front.png deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs/CAD/design/mate-connectors/rk-iso.png b/docs/CAD/design/mate-connectors/rk-iso.png deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs/CAD/design/mate-connectors/rk-sheet.png b/docs/CAD/design/mate-connectors/rk-sheet.png deleted file mode 100644 index e785a02839..0000000000 Binary files a/docs/CAD/design/mate-connectors/rk-sheet.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/simplify-sheet.png b/docs/CAD/design/mate-connectors/simplify-sheet.png deleted file mode 100644 index 9c05180c59..0000000000 Binary files a/docs/CAD/design/mate-connectors/simplify-sheet.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/simplify_study.py b/docs/CAD/design/mate-connectors/simplify_study.py deleted file mode 100644 index c0bbcf63ab..0000000000 --- a/docs/CAD/design/mate-connectors/simplify_study.py +++ /dev/null @@ -1,142 +0,0 @@ -"""Reduce the bear face to the fewest marks that still read at glyph size — wi3z. - -Geometry comes from bear_outline.json, which extract_outline.py pulled off the supplied male -B-rep's back plate: the outer wire IS the silhouette, the inner wires are the two eyes and the -muzzle opening. Nothing here is traced by eye. - -The glyph is drawn IN the connector's plane, so a grazing view foreshortens it along one axis by -sin(elevation) — exactly what collapsed the disc's roll quadrant to 3 pixels at 10 deg. Every -candidate is therefore rendered at three elevations as well as three pixel sizes. -""" -import json, math, os -from PIL import Image, ImageDraw - -HERE = os.path.dirname(os.path.abspath(__file__)) -D = json.load(open(os.path.join(HERE, "bear_outline.json"))) - -def norm(pts): - """Part frame (X right, Z down-negative) -> glyph frame (x right, y up), centred, unit height.""" - p = [(x, -z) for x, z in pts] - return p - -outer = norm(D["outer"]) -holes = [norm(h["pts"]) for h in D["holes"]] -# the two Ø9.8 wires are the eyes; the wide one is the muzzle -eyes = [h for h, meta in zip(holes, D["holes"]) if meta["d"] < 20] -muzzle = [h for h, meta in zip(holes, D["holes"]) if meta["d"] >= 20] - -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)) -def to_unit(pts): return [((x-CX)/SPAN, (y-CY)/SPAN) for x, y in pts] - -def rdp(pts, eps): - """Douglas-Peucker. Vertex count is the honest measure of 'how simplified'.""" - 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_closed(pts, eps): - r = rdp(pts + [pts[0]], eps) - return r[:-1] - -def centroid(pts): - return (sum(p[0] for p in pts)/len(pts), sum(p[1] for p in pts)/len(pts)) - -U_OUT = to_unit(outer) -U_EYE = [to_unit(e) for e in eyes] -U_MUZ = [to_unit(m) for m in muzzle] - -def eye_dots(scale=1.0): - out = [] - for e in U_EYE: - cx, cy = centroid(e) - r = max(max(p[0] for p in e)-min(p[0] for p in e), - max(p[1] for p in e)-min(p[1] for p in e))/2*scale - out.append((cx, cy, r)) - return out - -def muzzle_tri(): - """The muzzle reduced to one filled triangle: its two lower corners and its apex.""" - m = U_MUZ[0] - lo = min(p[1] for p in m); hi = max(p[1] for p in m) - bottom = [p for p in m if p[1] < lo + 0.06*(hi-lo)] - apex = max(m, key=lambda p: p[1]) - return [min(bottom), max(bottom), apex] - -CANDIDATES = { - "C0 full": dict(out=U_OUT, eyes=eye_dots(), muz=U_MUZ[0]), - "C1 eps .004": dict(out=simp_closed(U_OUT, .004), eyes=eye_dots(), muz=simp_closed(U_MUZ[0], .004)), - "C2 eps .012": dict(out=simp_closed(U_OUT, .012), eyes=eye_dots(), muz=muzzle_tri()), - "C3 eps .030": dict(out=simp_closed(U_OUT, .030), eyes=eye_dots(1.15), muz=muzzle_tri()), - "C4 no eyes": dict(out=simp_closed(U_OUT, .012), eyes=[], muz=muzzle_tri()), -} - -def sym_report(pts, tol=0.02): - """Trivial symmetry group is the property doing the work. If a simplification restores a - mirror or a 180 deg rotation, that simplification is wrong.""" - def match(tf): - t = [tf(p) for p in pts] - hit = 0 - for q in t: - if min(math.hypot(q[0]-p[0], q[1]-p[1]) for p in pts) <= tol: hit += 1 - return hit, len(pts) - return { - "mirror-x": match(lambda p: (-p[0], p[1])), - "mirror-y": match(lambda p: ( p[0], -p[1])), - "rot-180": match(lambda p: (-p[0], -p[1])), - } - -def render(c, 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)) - def m(p): - return (S/2 + p[0]*S*0.92, S/2 - p[1]*S*0.92*k) - d.polygon([m(p) for p in c["out"]], fill=255) - if c["muz"]: d.polygon([m(p) for p in c["muz"]], fill=0) - for cx, cy, r in c["eyes"]: - a = m((cx-r, cy+r)); b = m((cx+r, cy-r)) - d.ellipse([a[0], a[1], b[0], b[1]], fill=0) - return img.resize((px, px), Image.LANCZOS) - -print(f"{'candidate':14} {'verts':>6} {'marks':>6} symmetry (matched/total, lower is better)") -print("-"*78) -for name, c in CANDIDATES.items(): - s = sym_report(c["out"]) - marks = 1 + (1 if c["muz"] else 0) + len(c["eyes"]) - sym = " ".join(f"{k} {v[0]}/{v[1]}" for k, v in s.items()) - print(f"{name:14} {len(c['out']):6} {marks:6} {sym}") - -SIZES = [22, 32, 48] -ELEVS = [(90, "flat on"), (47, "47 deg"), (16, "16 deg"), (6, "6 deg")] -pad, cell = 8, 56 -W = pad + len(SIZES)*len(ELEVS)*cell + pad -H = pad + len(CANDIDATES)*cell + pad -sheet = Image.new("RGB", (W, H), (24, 27, 32)) -for r, (name, c) in enumerate(CANDIDATES.items()): - for ci, (elev, _) in enumerate(ELEVS): - for si, px in enumerate(SIZES): - g = render(c, px, elev) - tile = Image.new("RGB", (px, px), (24, 27, 32)) - gold = Image.new("RGB", (px, px), (237, 168, 23)) - tile.paste(gold, (0, 0), g) - x = pad + (ci*len(SIZES)+si)*cell + (cell-px)//2 - y = pad + r*cell + (cell-px)//2 - sheet.paste(tile, (x, y)) -sheet = sheet.resize((W*2, H*2), Image.NEAREST) -sheet.save(os.path.join(HERE, "simplify-sheet.png")) -print("\ncolumns: " + " | ".join(f"{e[1]} @ 22/32/48px" for e in ELEVS)) -print("rows: " + ", ".join(CANDIDATES)) -print("WROTE simplify-sheet.png") diff --git a/docs/CAD/design/mate-connectors/size-test.png b/docs/CAD/design/mate-connectors/size-test.png deleted file mode 100644 index 941f76ffb8..0000000000 Binary files a/docs/CAD/design/mate-connectors/size-test.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/sz-22-down+Z.png b/docs/CAD/design/mate-connectors/sz-22-down+Z.png deleted file mode 100644 index 85cbf9ba4e..0000000000 Binary files a/docs/CAD/design/mate-connectors/sz-22-down+Z.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/sz-22-grazing.png b/docs/CAD/design/mate-connectors/sz-22-grazing.png deleted file mode 100644 index 047f2c6e06..0000000000 Binary files a/docs/CAD/design/mate-connectors/sz-22-grazing.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/sz-22-iso.png b/docs/CAD/design/mate-connectors/sz-22-iso.png deleted file mode 100644 index 55b8a13eed..0000000000 Binary files a/docs/CAD/design/mate-connectors/sz-22-iso.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/sz-32-down+Z.png b/docs/CAD/design/mate-connectors/sz-32-down+Z.png deleted file mode 100644 index 683fc3d0aa..0000000000 Binary files a/docs/CAD/design/mate-connectors/sz-32-down+Z.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/sz-32-grazing.png b/docs/CAD/design/mate-connectors/sz-32-grazing.png deleted file mode 100644 index 92e17ca09a..0000000000 Binary files a/docs/CAD/design/mate-connectors/sz-32-grazing.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/sz-32-iso.png b/docs/CAD/design/mate-connectors/sz-32-iso.png deleted file mode 100644 index 1fe0ccb091..0000000000 Binary files a/docs/CAD/design/mate-connectors/sz-32-iso.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/sz-48-down+Z.png b/docs/CAD/design/mate-connectors/sz-48-down+Z.png deleted file mode 100644 index ec451714ff..0000000000 Binary files a/docs/CAD/design/mate-connectors/sz-48-down+Z.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/sz-48-grazing.png b/docs/CAD/design/mate-connectors/sz-48-grazing.png deleted file mode 100644 index 524c8e5343..0000000000 Binary files a/docs/CAD/design/mate-connectors/sz-48-grazing.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/sz-48-iso.png b/docs/CAD/design/mate-connectors/sz-48-iso.png deleted file mode 100644 index e0ed208b64..0000000000 Binary files a/docs/CAD/design/mate-connectors/sz-48-iso.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/t-+0.1-0.png b/docs/CAD/design/mate-connectors/t-+0.1-0.png deleted file mode 100644 index b0c09ef253..0000000000 Binary files a/docs/CAD/design/mate-connectors/t-+0.1-0.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/t-+0.1-1.png b/docs/CAD/design/mate-connectors/t-+0.1-1.png deleted file mode 100644 index 782cd16c0d..0000000000 Binary files a/docs/CAD/design/mate-connectors/t-+0.1-1.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/t-+1.6-0.png b/docs/CAD/design/mate-connectors/t-+1.6-0.png deleted file mode 100644 index f3b37f6697..0000000000 Binary files a/docs/CAD/design/mate-connectors/t-+1.6-0.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/t-+1.6-1.png b/docs/CAD/design/mate-connectors/t-+1.6-1.png deleted file mode 100644 index 731e092769..0000000000 Binary files a/docs/CAD/design/mate-connectors/t-+1.6-1.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/trim-cmp.png b/docs/CAD/design/mate-connectors/trim-cmp.png deleted file mode 100644 index 8dcc0c973f..0000000000 Binary files a/docs/CAD/design/mate-connectors/trim-cmp.png and /dev/null differ diff --git a/docs/CAD/design/mate-connectors/trim_female.py b/docs/CAD/design/mate-connectors/trim_female.py deleted file mode 100644 index 2753e4a3a8..0000000000 --- a/docs/CAD/design/mate-connectors/trim_female.py +++ /dev/null @@ -1,74 +0,0 @@ -# Trim the boxy frame off the female so its outer shape is the bear face itself. -# -# Method: take the male's flat back face (the plane Y=0 -- that face IS the bear silhouette), -# offset its OUTER wire outward in 2D, extrude the result along the insertion axis, and keep only -# the part of the female inside it. Everything outside is the block frame and goes away. -# -# NOTE ON THE NUMBER. The pocket's side walls stand at +0.20 mm from the male outline, because that -# is the clearance. A trim boundary at +0.10 mm therefore falls INSIDE them by 0.10 mm and removes -# the side wall entirely rather than leaving a thin one. The script runs the requested value and -# then measures what is actually left, so the outcome is a number rather than an opinion; it also -# emits a second variant at an offset that leaves a printable wall, for comparison. -# -# Run: /snap/bin/freecad.cmd trim_female.py - -import os, sys -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") -FEMALE = os.path.join(HERE, "BearConnector_Female.step") - -REQUESTED = 0.10 # as asked -CLEARANCE = 0.20 # what the pocket was built with -SAFE_WALL = 1.60 # a wall that survives an FDM nozzle: clearance + ~1.4 mm - -male = Part.Shape(); male.read(MALE); male = male.Solids[0] -fem = Part.Shape(); fem.read(FEMALE); fem = fem.Solids[0] -print(f"female in : {fem.Volume/1000:.2f} cm3, {len(fem.Faces)} faces") - -# --- the bear silhouette: the male's flat back face at Y = 0 -back = None -for f in male.Faces: - n = f.normalAt(0, 0) - if abs(f.CenterOfMass.y) < 1e-6 and abs(abs(n.y) - 1.0) < 1e-6: - if back is None or f.Area > back.Area: - back = f -if back is None: - print("FAIL: could not find the flat back face at Y=0"); sys.exit(1) -print(f"silhouette: back face area {back.Area:.1f} mm2, {len(back.Wires)} wires " - f"(outer + {len(back.Wires)-1} holes: eyes and mouth)") - -fb = fem.BoundBox -y0, y1 = fb.YMin - 5.0, fb.YMax + 5.0 - -def trimmed(offset): - """keep only the part of the female inside the silhouette grown by `offset`""" - wire = back.OuterWire - grown = wire.makeOffset2D(offset, join=2, fill=False, openResult=False, intersection=True) - face = Part.Face(Part.Wire(grown.Edges)) - prism = face.extrude(Vector(0, y1 - y0, 0)) - prism.translate(Vector(0, y0 - face.CenterOfMass.y, 0)) - return fem.common(prism) - -for tag, off, out in (("requested", REQUESTED, "BearConnector_Female_Trimmed.step"), - ("safe wall", SAFE_WALL, "BearConnector_Female_Trimmed_wall.step")): - r = trimmed(off) - if not r.Solids: - print(f"\n{tag} (+{off:.2f} mm): NOTHING LEFT"); continue - wall = off - CLEARANCE - # is there any material left at the level of the pocket's side wall? - sec = r.section(Part.makePlane(400, 400, Vector(-200, 1.5, -200), Vector(0, 1, 0))) - perim = sum(e.Length for e in sec.Edges) - print(f"\n{tag} (+{off:.2f} mm) wall = {wall:+.2f} mm") - print(f" volume {r.Volume/1000:.2f} cm3, {len(r.Solids)} solid(s), {len(r.Faces)} faces") - print(f" section through the pocket wall at Y=1.5: {perim:.1f} mm of edge") - if wall <= 0: - print(f" -> the trim cuts {abs(wall):.2f} mm INSIDE the pocket wall: no side wall remains") - doc = App.newDocument(tag.replace(" ", "_")) - o = doc.addObject("Part::Feature", "Female") - o.Shape = r; doc.recompute() - Part.export([o], os.path.join(HERE, out)) - print(f" wrote {out}") diff --git a/docs/CAD/design/mate-connectors/verify_trimmed.py b/docs/CAD/design/mate-connectors/verify_trimmed.py deleted file mode 100644 index 81f6f3fea1..0000000000 --- a/docs/CAD/design/mate-connectors/verify_trimmed.py +++ /dev/null @@ -1,25 +0,0 @@ -# Check both trimmed females still fit the male, and export STLs for a visual comparison. -# Run: /snap/bin/freecad.cmd verify_trimmed.py -import os -import Mesh, Part - -HERE = os.path.dirname(os.path.abspath(__file__)) -male = Part.Shape(); male.read(os.path.join(HERE, "bear.step")); male = male.Solids[0] - -for name in ("BearConnector_Female_Trimmed", "BearConnector_Female_Trimmed_wall"): - p = os.path.join(HERE, name + ".step") - s = Part.Shape(); s.read(p); s = s.Solids[0] - d = male.distToShape(s)[0] - c = male.common(s) - cv = c.Volume if c.Solids else 0.0 - bb = s.BoundBox - print(f"{name}") - print(f" {bb.XLength:.2f} x {bb.YLength:.2f} x {bb.ZLength:.2f} mm, {s.Volume/1000:.2f} cm3, " - f"{len(s.Faces)} faces, valid={s.isValid()}") - print(f" gap to male {d:.4f} mm, interference {cv:.6f} mm3") - m = Mesh.Mesh(); m.addFacets([tuple(t) for t in s.tessellate(0.12)[1]] and - [(s.tessellate(0.12)[0][a], s.tessellate(0.12)[0][b], - s.tessellate(0.12)[0][c2]) - for a, b, c2 in s.tessellate(0.12)[1]]) - m.write(os.path.join(HERE, name + ".stl")) - print(f" wrote {name}.stl ({m.CountFacets} facets)") diff --git a/docs/CAD/design_tab.md b/docs/CAD/design_tab.md deleted file mode 100644 index 1be0a2ea33..0000000000 --- a/docs/CAD/design_tab.md +++ /dev/null @@ -1,333 +0,0 @@ -# The Design tab - -Object-driven parametric CAD inside the slicer. Point at geometry; the geometry offers the -verbs that apply to it. Selection comes first and the tool consumes it. Draw a sketch, -constrain it, turn it into a solid, refine it, and send it straight to Prepare — without -leaving for another application and coming back through an STL. - -The model is a **recipe**, not a mesh. Every action becomes a feature in a tree that is -replayed from the start whenever anything changes, so editing a dimension you set twenty -steps ago rebuilds everything downstream. The geometry kernel is OCCT, which the slicer -already ships for STEP import. - ---- - -## Getting started - -1. Open the **Design** tab. -2. Click a face or a reference plane in the viewport, then press `Shift+S` (Sketch). The offer - opens with the sketch tools on it. -3. Draw a closed profile, then press **✓ Confirm** in the floating action bar. -4. With the sketch selected, press `Shift+E` (Extrude). -5. Press **Commit to Plate** to hand the solid to Prepare. - -The status line under the toolbar is the thing to watch: it says what the current tool is -waiting for. When no plane is picked it reads *"Click a face or a reference plane in the -viewport, then a sketch tool"*; once one is picked it reads *"Sketching on — pick a -tool"*. It is also where a refusal explains itself. - ---- - -## Selecting - -- One left-click selects what is under the cursor. There is no click-cycling through - face → edge → body. -- A click near a corner takes the corner, not the face behind it. -- Left-drag sweeps a rubber band, and a rubber band takes the whole body. -- An open sketch line can be clicked, even where it bounds a region. -- Double-click a sketch stroke to edit it — the gesture belongs on the geometry. -- Editing a dimension's value **updates** that dimension instead of adding a second one next - to it. -- The floating chrome that belongs to a sketch leaves with the sketch when it ends. -- Sketching happens on the face you clicked, first click. -- A sketch whose entities form no wire **fails** instead of extruding a default box. A - subtraction that removes nothing is reported as an error instead of a silent success. - ---- - -## The offer - -Right-click on the geometry, released without moving the mouse (an 8 px budget — a -right-drag that orbits the camera does not open it). Left-click still only selects, so -pointing at things stays quiet. - -The offer also opens by itself the moment you press Sketch on a face or plane, showing the -sketch tools — the app hands you the tools directly. - -**Eight families, always in this fixed order:** Create, Add material, Remove, Dress-up, -Repeat, Transform, Reference, Modify. - -- A family with at least one applicable verb shows it. Several applicable verbs collapse - into a submenu under the family name. -- A family with nothing applicable is **shown greyed in place, with the reason** — e.g. - *"Create — Click a face or a reference plane in the viewport, then a sketch tool"*. It is - not hidden. A control that cannot be used still says what it is and what you would have to - do first. -- Inside a sketch the offer shows the sketch verbs; outside it shows the feature verbs. - -**Document-level actions never enter the offer**, because they act on the document and not -on a selection: Import STEP, Import mesh, Text, SVG, Export STEP, Commit to Plate, Undo, -Redo, Variables, Section view, Origin planes, World axes. They live in the toolbar. - ---- - -## Keyboard - -Single letters drive sketch tools **while a sketch is open**; Shift+letter drives feature -tools and single letters drive view toggles **when no sketch is open**. The two maps are -selected by the mode, not by whether a sketch session is running. - -### Sketch (while a sketch is open) - -| Key | Tool | -|---|---| -| `L` | Line — click start, then end | -| `R` | Rectangle — click two opposite corners | -| `C` | Circle — click centre, then radius | -| `A` | Arc — click start, end, then a point | -| `S` | Slot — two centreline ends, then width | -| `E` | Ellipse — centre, major end, minor point | -| `B` | Spline — click control points | -| `P` | Point — click to place | -| `G` | Polygon — click centre, then a vertex | -| `D` | Dimension — click 2 points or an entity | -| `T` | Trim — click a segment to trim it | -| `X` | Extend — click a line/arc to extend it | -| `O` | Offset — pick an entity, drag the distance | -| `M` | Mirror — pick axis, then entities | -| `F` | Fillet — pick two lines, set the radius | -| `H` | Chamfer — pick two lines, set the distance | -| `K` | Constrain — finish the live sketch and enter constrain | -| `Q` | Construction toggle — draw the next entity as construction geometry | -| `Del` | Delete the selected sketch entity | -| `Esc` | Cancel the live tool | - -### Feature (when no sketch is open) - -| Key | Tool | -|---|---| -| `Shift+S` | Sketch | -| `Shift+E` | Extrude — extrude a profile, or push/pull a picked face | -| `Shift+R` | Revolve | -| `Shift+W` | Sweep | -| `Shift+L` | Loft | -| `Shift+N` | Pattern | -| `Shift+G` | Surface Extrude | -| `Shift+J` | Surface Revolve | -| `Shift+O` | Surface Loft | -| `Shift+Q` | Surface Fill | -| `Shift+U` | Surface Offset | -| `Shift+V` | Thicken Surface | -| `Shift+P` | Plane | -| `Shift+A` | Axis | -| `Shift+C` | Coord Sys | -| `Shift+Y` | Transform | -| `Shift+Z` | Mirror | -| `Shift+B` | Boolean | -| `Shift+X` | Cut | -| `Shift+F` | Fillet / Chamfer | -| `Shift+D` | Draft | -| `Shift+K` | Shell | -| `Shift+H` | Hole | -| `Shift+T` | Thread | -| `Shift+I` | Import STEP | -| `Shift+M` | Import mesh | - -### View toggles (single letters, when no sketch is open) - -| Key | Action | -|---|---| -| `Home` | Axonometric view, fitted to the model | -| `P` | Origin planes on/off | -| `A` | World axes on/off | -| `X` | Section view on/off | - -While the section is on: `PageUp` / `PageDown` move the cut plane, `F` flips which half is -kept. With no section on, `F` is Place on Face — lay the picked face flat on the bed. - ---- - -## Sketching - -A sketch is a closed (or open) 2D profile on a plane or on a flat face of an existing body. -Press `Shift+S`, click the face or plane you want to sketch on, and draw. The toolbar and -the offer both carry the sketch tools. - -**Entities:** line, polyline, rectangle (corner / centre / oblique / rounded), circle -(centre-radius / 2-point / 3-point), arc (centre-point / 3-point / tangent), ellipse and -elliptical arc, polygon (inscribed / circumscribed), slot (straight / arc), spline, point, -and text. - -**Editing:** move, rotate, scale, trim, extend, offset, mirror, and linear or polar arrays. - -**Constraints:** coincident, horizontal, vertical, parallel, perpendicular, tangent, equal, -concentric, midpoint, symmetric, fix, plus dimensional radius, diameter, distance and angle. -The solver reports the remaining degrees of freedom and tells you when a sketch is fully -constrained — or when a constraint conflicts with one already there. - -Sketches stay editable. Selecting one in the feature tree reopens it with its dimensions -live. - ---- - -## Building solids - -Grouped in the toolbar by what they do, one concept per drawer. - -### Add material -| Tool | Shortcut | What it does | -|---|---|---| -| Extrude | `Shift+E` | Extrude a profile, or push/pull a face already on a body | -| Revolve | `Shift+R` | Revolve a profile about an axis | -| Sweep | `Shift+W` | Sweep a profile along a path — including a helix, for springs and augers | -| Loft | `Shift+L` | Skin between two or more profiles | -| Thicken | — | Offset a solid face into a thin plate as a new body | -| Rib | — | Grow a stiffening wall from an open sketch line, fused to a body | - -Extrude offers blind, symmetric, two-sided, through-all and up-to-face end conditions, plus -a draft angle on the side wall, and can add, subtract, intersect or start a new body. - -### Surface -Sheet bodies — surfaces with no thickness — for shapes that are easier to build as skins and -solidify afterwards. - -| Tool | Shortcut | -|---|---| -| Surface Extrude | `Shift+G` | -| Surface Revolve | `Shift+J` | -| Surface Loft | `Shift+O` | -| Surface Fill | `Shift+Q` | -| Surface Offset | `Shift+U` | -| Thicken Surface | `Shift+V` | - -Thicken Surface is how a sheet becomes a printable solid. - -### Dress-up -| Tool | Shortcut | -|---|---| -| Fillet / Chamfer | `Shift+F` | -| Draft (taper a face) | `Shift+D` | -| Shell | `Shift+K` | -| Delete Face | — | - -Delete Face removes faces and heals the solid — useful for stripping a feature off an -imported part. - -### Holes -**Hole** (`Shift+H`) drills simple, counterbored or countersunk holes, with an ISO/ANSI -standards table so you can ask for an M6 clearance hole instead of computing a diameter. -**Thread** (`Shift+T`) cuts a real helical thread into a bore or onto a shaft. - -### Placement -Operations that move a body without changing its shape: **Transform** (`Shift+Y`), -**Mirror** (`Shift+Z`), and **Mate** for assemblies. - -### Combining -**Boolean** (`Shift+B`) unions, subtracts or intersects two bodies. **Cut** (`Shift+X`) -splits a body with a plane. **Pattern** (`Shift+N`) repeats a body linearly, in a circle, or -along a curve. - ---- - -## Reference geometry - -Datum features carry no material; they exist to give later features something to attach to. - -- **Plane** (`Shift+P`) — offset, tilted, midplane, tangent, through two edges, or coincident -- **Axis** (`Shift+A`) — two points, a face normal, a cylinder centreline, the intersection of - two planes, or along an edge -- **Coord Sys** (`Shift+C`) — a full frame, from a world point or from a face plus a - direction edge -- **Helix** — a helical curve to sweep along -- **Project** — project a body's edges onto a plane as sketch geometry - -On the Coord Sys tool, picking a direction **edge** is worth the extra click: without one the -frame takes its X from the face's first edge, which is deterministic but not necessarily the -direction you meant. - ---- - -## Assemblies - -**Mate** aligns two coordinate systems and moves one body onto the other. Five kinds: - -| Kind | Leaves free | -|---|---| -| Fastened | nothing — 6 DOF locked | -| Planar | sliding in the plane | -| Revolute | rotation about the axis | -| Slider | sliding along the axis | -| Cylindrical | rotation *and* sliding | - -**Check interference** reports every overlapping pair of solids with the overlapping volume, -so a clash is a number rather than an impression. Bodies that merely touch enclose no volume -and are not reported. - ---- - -## Variables and expressions - -Define named variables and drive dimensions from them. Any numeric field accepts an -expression — `width/2`, `wall*3` — and everything re-evaluates on recompute. Change one -variable and the whole model follows. - ---- - -## Import and export - -**Import STEP** brings in a real B-rep solid, not a mesh: its faces and edges can be filleted, -shelled and cut like anything modelled here. - -**Import mesh** (STL/OBJ) converts triangles to a B-rep body and tells you honestly what it -got — whether the result is a closed solid or an open shell, with the boundary and -non-manifold edge counts. A large mesh becomes a large number of faces, which is slow to -edit; the importer warns before you commit to it. - -**Export STEP** writes the model out for another CAD tool. - -**Commit to Plate** sends the solid to Prepare for slicing. The whole feature recipe is saved -inside the 3MF, so reopening the project restores the editable model rather than a frozen -mesh. - ---- - -## View controls - -**Section view** (`X`) hides half the model so you can see inside — `PageUp`/`PageDown` move -the plane, `F` flips which half is kept. **Place on Face** (`F`, when section is off) lays a -picked face flat on the bed. Origin planes (`P`) and world axes (`A`) can be toggled on while -you orient yourself. - ---- - -## Known limitations - -Being straight about the edges, so nobody discovers them the hard way: - -- **Rib** needs a sketch containing an explicit open line. A parametric rectangle sketch - carries no individual entities, so Rib cannot use one. -- **Surface Loft** and **Surface Fill** have kernel tests but have not been exercised by hand. -- Card wiring for 9 of the 16 late-wired tools has never been click-tested. -- Mate resolves by composing transforms directly. There is no 3D assembly solver, so mates - are applied in order rather than solved simultaneously, and mate limits are not implemented. -- Move-face and replace-face are not implemented — OCCT offers no clean primitive for them. -- There is no automated GUI test in CI. Every behaviour above is traced to code and to a - hand pass, not to a synthetic click. - ---- - -## Where the code lives - -| Path | Role | -|---|---| -| `src/libslic3r/CAD/CadDocument.*` | the feature recipe and its replay | -| `src/libslic3r/CAD/GeometryEngine.*` | OCCT wrapper — faces, edges, booleans, healing | -| `src/libslic3r/CAD/SketchEngine.*` | profile → wire → solid | -| `src/libslic3r/CAD/SketchSolver.*` | constraint solving, over the vendored solver | -| `src/libslic3r/slvs/` | vendored 2D constraint solver (GPLv3) | -| `src/slic3r/GUI/CAD/DesignPanel.*` | the tab: toolbar, cards, feature tree | -| `src/slic3r/GUI/CAD/DesignCanvas.*` | viewport integration | -| `src/slic3r/GUI/CAD/DesignSketchTool.*` | in-canvas sketching | - -Build with `-DSLIC3R_CAD=ON` (the default). With it OFF the tab is not compiled and the deps -prefix matches upstream exactly — see [cad_dependency_weight.md](cad_dependency_weight.md). diff --git a/docs/CAD/design_tab_pr_description.md b/docs/CAD/design_tab_pr_description.md deleted file mode 100644 index a6d67f0e7c..0000000000 --- a/docs/CAD/design_tab_pr_description.md +++ /dev/null @@ -1,73 +0,0 @@ -# Design (CAD) tab — upstream pull request - -## What this adds - -A sketch-first parametric CAD tab inside the slicer. The workflow is direct: -sketch → constrain → solid features → commit to plate. The whole feature recipe is -persisted inside the 3MF, so reopening restores an editable model rather than a frozen mesh. - -- Kernel: OCCT, which upstream already links for STEP import — see - [cad_dependency_weight.md](docs/cad_dependency_weight.md) -- Constraint solver: vendored SolveSpace `libslvs` subset -- Interaction model: object-driven — point at geometry, the geometry offers the verbs that - apply to it; see [cad_ux_guidelines.md](docs/cad_ux_guidelines.md) -- Full user-facing documentation: [design_tab.md](docs/design_tab.md) - -## Why it belongs in the slicer - -Every round trip through an external CAD tool costs a file export, a re-import, and the -design intent that both steps discard. A part modified after slicing should return to its -feature history, not to a mesh. Keeping the CAD model inside the slicer preserves that -loop — the nozzle diameter, the build volume and the material are known at design time. - -For the integration case in full: [design_tab_upstream_portability.md](docs/design_tab_upstream_portability.md). - -## How it is built - -The `SLIC3R_CAD` CMake flag (default ON) gates the entire tab. With it OFF the tab is not -compiled and the deps prefix matches upstream exactly — the dependency diff is one line in -OCCT's CMake: `BUILD_MODULE_ModelingAlgorithms=OFF → ON`. - -Measured cost table: [cad_dependency_weight.md](docs/cad_dependency_weight.md). - -## Diff shape - - - -Against merge-base `d6cb667b894f`: - - 306 files changed, 83032 insertions(+), 777 deletions(-) - -350 commits, of which 284 are new files and 37 modify upstream files. 99.3 % of the diff -is new code. The negotiable surface is the 37 modified files. - -## Tests - -205 `TEST_CASE` blocks across 6 new test source files. This counts assertions written, not -assertions passed — a run needs a build. - -`scripts/CAD/run-kernel-tests.sh` is the headless verification contract: it builds only -`libslic3r_tests` (not the GUI app), needs no display, and exit 0 means the CAD suite -passed. It now runs with **no exclusions** — both cases that used to be quarantined (the -circle-line tangency solver abort and the internal-thread reference) are fixed. - -## Licensing - -The vendored solver in `src/libslic3r/slvs/` is **GPL-3.0** (see `src/libslic3r/slvs/LICENSE`), -not LGPL. The combined work is distributable under AGPL-3.0. See the Licensing section of -[design_tab_upstream_portability.md](docs/design_tab_upstream_portability.md) for the -AGPLv3/GPLv3 compatibility argument; this point should be confirmed with upstream explicitly. - -## Not verified - -- Card wiring for 9 of the 16 late-wired tools was never click-tested. -- There is no automated GUI test in CI. A green kernel run says nothing about the GUI — - synthetic clicks never drift, so the test suite and the viewport are two separate realities. -- The click-test defect rate has **not converged**: a second pass found no new defects, but - four further days of work found five more. The earlier pass is not evidence of stability. - -## Reviewer's map - -See the [Where the code lives](docs/design_tab.md#where-the-code-lives) table in the user -doc for the file-to-role mapping, and [docs/ux/tool_atlas.json](docs/ux/tool_atlas.json) as -the generated-from source of `src/slic3r/GUI/CAD/DesignOffer.hpp`. diff --git a/docs/CAD/design_tab_upstream_portability.md b/docs/CAD/design_tab_upstream_portability.md deleted file mode 100644 index a7e9cd84ed..0000000000 --- a/docs/CAD/design_tab_upstream_portability.md +++ /dev/null @@ -1,162 +0,0 @@ -# Design (CAD) tab — upstream integration brief - -**Question:** can the Design tab (sketch-first parametric CAD: sketch → constrain → -extrude/revolve/fillet/hole/thread/shell, multi-body, undo, 3MF persistence) land in -mainline OrcaSlicer? - -**Answer: yes, and the ask is far smaller than previously believed.** OCCT is *already* -an OrcaSlicer dependency. We are not asking upstream to adopt a new library; we are -asking it to widen one it already builds, at a measured cost of **3.77 MiB on Windows**. - -> ### Corrections to the 2026-06-21 assessment -> That revision was written before the persistence work landed and got two load-bearing -> facts wrong. Both are corrected here from direct measurement of the branch: -> -> 1. **"The real blocker: OCCT … a dependency mainline OrcaSlicer has never carried."** -> **False.** `deps/OCCT/` exists at the merge-base and upstream links it from -> `Format/STEP.cpp`, `Format/svg.cpp`, and `Shape/TextShape.cpp`. Our entire -> dependency diff is **one line**: `BUILD_MODULE_ModelingAlgorithms=OFF → ON`. -> 2. **"vendored SolveSpace solver … LGPL."** **False.** `src/libslic3r/slvs/LICENSE` is -> **GPL-3.0**, not LGPL. This is fine (see Licensing) but must not be misstated. -> -> It also claimed "no changes to Model" — no longer true; 3MF recipe persistence adds one -> `std::string` to `Model`. - -## Measured shape of the change - -Against merge-base `449a4cf9fc` (34 commits ahead): - -| | files | lines | -|---|---:|---:| -| **New files** | 138 | +61,720 | -| **Modified upstream files** | 23 | +457 / −75 | -| **Deleted upstream files** | 0 | — | - -The 62 kLOC headline is inflated by localization. The feature itself: - -| area | LOC | files | -|---|---:|---:| -| kernel (`src/libslic3r/`) | 15,828 | 37 | -| GUI (`src/slic3r/`) | 19,544 | 14 | -| tests (Catch2) | 2,567 | 6 | -| i18n (unrelated; strip from the CAD PR) | 23,438 | 77 | - -**99.3 % of the diff is new files.** The negotiable surface is 457 added lines across 23 -files, and nothing upstream is deleted. The largest single hook is `GLCanvas3D.cpp` -(+110/−2): an `m_design_sketch_tool` member plus render/mouse/key hooks, **every one -already null-guarded** — which is why the compile-time gate below is cheap. - -No changes to the slicing pipeline (Print/PrintObject/Layer/GCode), Tab, or the -printer-profile/config system. - -## The dependency ask, precisely - -Not "adopt OCCT" — **widen the existing OCCT build**: - -```diff -- -DBUILD_MODULE_ModelingAlgorithms=OFF -+ -DBUILD_MODULE_ModelingAlgorithms=ON -``` - -Cost, measured from the shipped Windows artifact (42 OCCT DLLs, 45.43 MiB total): - -| toolkit | size | note | -|---|---:|---| -| `TKFillet.dll` | 2.02 MiB | only exists with the flag ON | -| `TKOffset.dll` | 1.75 MiB | only exists with the flag ON | -| **delta** | **3.77 MiB** | Windows only (OCCT is Shared on Win, Static elsewhere) | - -`TKBool` is *not* part of the delta — upstream's `DataExchange` already pulls it in -transitively. On macOS/Linux OCCT links statically, so the cost is only the code actually -referenced, not a 3.77 MiB floor. - -**Unmeasured, and we should measure before the call:** clean-deps build-time delta with -the flag ON vs OFF, and the resulting CI runner-minute cost. Do not guess these at him. - -## Licensing - -- Vendored solver `src/libslic3r/slvs/` — **GPL-3.0**, 9,339 LOC, © Jonathan Westhues, - a self-contained subset of SolveSpace (`libslvs`). No external dependencies. -- OrcaSlicer — **AGPL-3.0** (`LICENSE.txt`). - -GPLv3 §13 expressly permits combining a GPLv3 work with an AGPLv3 work; AGPLv3 §13 grants -the converse. The combined work is distributable under AGPL-3.0 with the solver's GPLv3 -terms preserved. This is a favourable direction (GPLv3 → into an AGPLv3 project), but it -is a point to **confirm explicitly with upstream**, not to assert unilaterally. - -Open question for SoftFever: keep the solver **vendored** (current: pinned, no submodule, -no external build) or move it to `deps/` as a fetched external? Vendoring costs us -upstream-sync burden; `deps/` costs build complexity. - -## The one irreversible decision: the 3MF format - -Persistence adds an **optional** archive entry and one field: - -```cpp -// Model.hpp -std::string cad_recipe; // empty for non-CAD projects -``` - -``` -Metadata/orca_cad.bin // written only when cad_recipe is non-empty -``` - -Readers that do not know the entry ignore it; writers skip it entirely when empty. So -existing projects are bit-identical and old readers are unaffected. Good. - -**But the moment upstream ships this, it owns forward-compatibility forever.** Three -things should be settled *before* the first release, because none can be changed after: - -1. **Name.** Renamed to `Metadata/orca_cad.bin`. -2. **Encoding.** The recipe is an opaque **cereal `PortableBinaryArchive`** blob whose - layout is the field order of `CadFeature::serialize`. Portable across endianness and - word size — *not* across a field reorder. Append-only is currently a convention held by - discipline, not by any check. -3. **Embedded BRep.** `Import` features embed OCCT's ASCII BRep for the imported solid, - which couples saved project files to an OCCT BRep revision. Alternative: re-import from - the source STEP and store only a reference. Worth deciding deliberately. - -**Concrete gap we should close before the call.** `test_caddocument.cpp` covers the -in-memory round-trip and correctly refuses a version-999 blob — but there is **no -checked-in v1 fixture on disk**. A reordered field in `CadFeature::serialize` would pass -the entire suite while silently breaking every previously-saved project. Ship a golden -`.bin` fixture generated today plus a test that loads it; that is the only thing that will -hold the format still once real users have files. - -## Proposed PR decomposition - -35 kLOC in one PR is not reviewable. Behind the flag, slices 1–4 are behaviour-neutral for -existing users: - -1. **Build gate + OCCT flag + Windows packaging guard.** `-DSLIC3R_CAD=ON/OFF`, default - **OFF**. Flips `ModelingAlgorithms=ON`. Includes the guard that asserts every linked - OCCT toolkit has a shipped DLL (already on both forks: `546cef5f42`). ← *this is what - makes SoftFever's "parallel build" a one-line CI matrix entry.* -2. **Vendored `slvs` solver** + its Catch2 tests. No GUI, no OCCT. -3. **CAD kernel** (`CadDocument`, `SketchEngine`, `GeometryEngine`, `Sketch*`) + kernel - tests. Headless, no GUI. -4. **3MF recipe persistence** + golden-fixture regression test. -5. **GUI Design tab** (`DesignPanel`, `DesignCanvas`, `DesignSketchTool`, `GLGizmoSketch`) - + the 23 upstream hooks. - -## Agenda for the call - -Questions only SoftFever can answer: - -- Does OrcaSlicer *want* to be a CAD-integrated slicer? (Strategic; everything else is mechanical.) -- Default of `SLIC3R_CAD` at merge time, and when it flips ON. -- Vendored solver vs `deps/` external; and confirmation of the GPLv3/AGPLv3 combination. -- Project-file format: neutral name, encoding, embedded-BRep policy, and who owns v1 forward-compat. -- Undo/redo: the Design tab has its own stack; integrate with Orca's snapshot system or keep separate? -- Does he want the i18n work (Romanian, +23 kLOC) as a wholly separate PR? (Yes, almost certainly.) - -## Verdict - -Portability **high**. The prior "does upstream want OCCT" framing was wrong — OCCT is -already there. What remains is a 3.77 MiB dependency widening, a compile-time gate that -the existing null-guards make cheap, and one file-format decision that must be made before -the first release rather than after. - ---- -*Revised 2026-07-10 from direct measurement of `cad-mainline` @ `546cef5f42` vs upstream -merge-base `449a4cf9fc`. Supersedes the 2026-06-21 read-only assessment.* diff --git a/docs/CAD/ux/interaction-model.md b/docs/CAD/ux/interaction-model.md deleted file mode 100644 index 2041978ea0..0000000000 --- a/docs/CAD/ux/interaction-model.md +++ /dev/null @@ -1,144 +0,0 @@ -# Design tab — interaction model - -The contract for Esc, the right mouse button, and the states between them. Code that changes any -of the three changes this file in the same commit. - -## 1. The state machine - -`src/slic3r/GUI/CAD/DesignInteraction.hpp` — a four-level LIFO stack. The enum value *is* the -depth, so "which level does this press belong to" is a comparison rather than a chain of -special cases spread over three files. - -```cpp -enum class CadLevel : int { - Idle = 0, // nothing transient is up: Esc clears the selection - Tool = 1, // a feature card / armed sketch tool / constrain session: Esc exits it - Gesture = 2, // an uncommitted delta (entity being drawn, body being dragged): Esc reverts it - Transient = 3, // a value field or a popup menu: Esc closes just that -}; - -struct CadInteractionState { // the four bits routing actually needs - bool value_field_open{false}; - bool gesture_active{false}; - bool tool_armed{false}; - bool has_selection{false}; -}; - -constexpr CadLevel cad_escape_level(const CadInteractionState& s) -{ - if (s.value_field_open) return CadLevel::Transient; - if (s.gesture_active) return CadLevel::Gesture; - if (s.tool_armed) return CadLevel::Tool; - return CadLevel::Idle; -} -``` - -The rule is a `constexpr` free function over a POD, not a method on the panel, so the ordering -that is the entire contract is checkable without a window, a GL context or an event loop. Five -`static_assert`s in the header do exactly that, at compile time. - -**Strict invariant.** No level of Esc deletes a feature, discards a sketch that holds geometry, -or rolls history back. Destroying work needs a gesture that says so: - -| To destroy | Gesture | -|---|---| -| a feature | Delete / Backspace on an explicit selection | -| a drawn sketch | the ribbon's ✗ Cancel, which asks first | -| the last committed change | Ctrl+Z | - -## 2. Event routing - -**`OnKeyDown(WXK_ESCAPE)`** — `DesignPanel`'s `wxEVT_CHAR_HOOK`, one line: - -```cpp -if (key == WXK_ESCAPE) { escape(); return; } -``` - -Every Esc in the tab goes through it, whatever holds focus. `DesignPanel::escape_level()` answers -the four questions of `CadInteractionState` about this panel; `DesignPanel::escape()` acts on the -one level that answer names, and on no other: - -| Level | What one press does | What it must not touch | -|---|---|---| -| `Transient` | close the value field (`cancel_value` / `inline_cancel`) | the tool, which stays armed | -| `Gesture` | drop the clicks of the entity being drawn, or put a moved body back at the pose it had when the gizmo appeared | everything already committed | -| `Tool` | discard a feature card's *candidate*; drop an armed sketch tool to Select; end Constrain | committed features; entities already drawn | -| `Idle` | clear the selection (model and sketch); leave a sketch session **only if it is empty** | a sketch holding geometry — it is left through Finish or Cancel | - -A sketch *session* is deliberately not a `Tool`. It is the environment the Idle level lives in, -which is what makes the destructive path unrepresentable rather than merely unlikely. - -**`OnRightDown` / `OnRightUp`** — `DesignCanvas::set_on_context_menu`, bound after `GLCanvas3D`'s -own handlers so it can consume the event before them: - -```cpp -RIGHT_DOWN: remember the press position and the clock, then Skip() // the canvas still seeds the orbit - -RIGHT_UP: terminated = sketch_tool.take_right_consumed(); // read-and-clear, always - is_click = drift <= 3 px && dt <= 200 ms; // both budgets, or it was navigation - if (callback && !terminated && !inline_busy && is_click) { - select_at_screen(press.x, press.y); // raycast at the PRESS, not the release - on_context_menu(ClientToScreen(press)); - return; // consumed - } - Skip(); // orbit / pan / the handlers underneath -``` - -Two independent budgets because the two failure modes are independent: drift alone still popped a -menu at the end of a slow, careful orbit. `take_right_consumed()` is how a right-click that -already meant something to the armed sketch tool (terminate a chain, drop an edit-op) declines to -also mean "open a menu". - -## 3. Transition table - -`sel` = something is picked. Blank = the input does nothing at that state. - -| State | Left-click | Right-click | Esc | Enter | -|---|---|---|---|---| -| **Idle — model view** | pick / escalate the pick | offer menu for what is under the cursor | clear the selection | — | -| **Idle — sketch, empty** | pick | sketch offer menu | leave the session (nothing to lose) | Finish sketch | -| **Idle — sketch, drawn** | pick | sketch offer menu | clear the selection; status says the sketch is kept | Finish sketch | -| **Tool — feature card** | pick the card's next reference | offer menu | discard the candidate, close the card | commit the feature | -| **Tool — sketch tool armed** | place the first point | drop the tool to Select | drop the tool to Select | — | -| **Tool — constrain** | pick an entity | offer menu | end the session | apply | -| **Gesture — drawing** | place the next point | terminate the chain (keep what is drawn) | drop the in-progress entity, tool stays armed | commit the entity as drawn | -| **Gesture — moving a body** | drop the body here | end the move | revert to the pose at move-start | keep the placement | -| **Transient — value field** | — | — | close the field, tool stays armed | commit the value, advance the chain | -| **Transient — popup menu** | run the entry | — | close the menu | run the highlighted entry | -| **any** | — | — | *never* deletes, discards or rolls back | — | - -Right-hold-and-drag is not in the table on purpose: past 3 px or 200 ms it is navigation, and -navigation does not transition the state machine. - -## 4. Visual scaffolding - -Entering a sketch changes three things at once, so the state is legible from across the room: - -- **Banner.** A teal strip across the top of the viewport: `Editing: Sketch N · N = look normal to - the plane · Finish or Cancel in the toolbar`. Indicator only — Confirm and Cancel stay on the one - ribbon action bar, per the Design UX contract. It is a sibling above the canvas, not a floating - child over it: a child window over a `wxGLCanvas` is a native window on GTK and does not reliably - stack over GL, and this banner's job is to be unmissable rather than clever. -- **The printer bed is muted.** A plate grid and a sketch grid are the same visual language, and - reading one as the other is how a sketch gets drawn against the wrong reference. The view - checkbox remains the stored preference and is restored on the way out; ticking it mid-sketch - still shows the bed, because that is a deliberate act and this is only a default. -- **`N` looks normal to the plane**, keeping the current zoom, with the plane's own y axis as up. - Sketch key map only — in Feature mode the navigator orb owns orientation. - -## 5. Context menu content - -The offer is generated from `docs/CAD/ux/tool_atlas.json`; its 8-row shape and permanent row -indices are ratified and are not changed here. Checked against the per-context vocabularies asked -for in the 2026-09-05 interaction brief, the atlas already carries all of them except two, both on -a planar face: - -| Asked for | Status | -|---|---| -| Revolve on a planar face | **not offered, and should not be**: `revolve` accepts `sk_loop` only, because the kernel takes a sketch profile — a face is not one | -| Offset Face | offered as **Thicken** (`thicken`, accepts `face_planar`); `surf_offset` is the sheet-body verb and accepts `body_sheet` | - -View and document actions — Zoom to Fit, View Isometric, Clear Selection, Finish Sketch, Normal to -Sketch — stay in chrome by the atlas's own rule: the offer describes verbs that consume a -*selection*, and these act on the document or the camera. Esc covers Clear Selection, `N` covers -Normal to Sketch, and the ribbon covers Finish. diff --git a/docs/CAD/ux/mockups/cmp_12__edge_str.svg b/docs/CAD/ux/mockups/cmp_12__edge_str.svg deleted file mode 100644 index 3e641e90ec..0000000000 --- a/docs/CAD/ux/mockups/cmp_12__edge_str.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Straight edge selected — pick what to do with itWorking document · 12 slotsDress-up2Pattern on CurveReference3Straight edge \ No newline at end of file diff --git a/docs/CAD/ux/mockups/cmp_12__face_planar.svg b/docs/CAD/ux/mockups/cmp_12__face_planar.svg deleted file mode 100644 index 85809d5bf6..0000000000 --- a/docs/CAD/ux/mockups/cmp_12__face_planar.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Planar face selected — pick what to do with itWorking document · 12 slotsSketchShift+SAdd material2Remove2Dress-up3PatternShift+NTransform2Reference5Modify2Planar face \ No newline at end of file diff --git a/docs/CAD/ux/mockups/cmp_12__sk_line.svg b/docs/CAD/ux/mockups/cmp_12__sk_line.svg deleted file mode 100644 index 6535c9fe77..0000000000 --- a/docs/CAD/ux/mockups/cmp_12__sk_line.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Sketch line selected — pick what to do with itWorking document · 12 slotsCreate9OffsetOTrimTDress-up2MirrorMMoveReference3Modify2Sketch line \ No newline at end of file diff --git a/docs/CAD/ux/mockups/cmp_8__edge_str.svg b/docs/CAD/ux/mockups/cmp_8__edge_str.svg deleted file mode 100644 index 2c41ce74fc..0000000000 --- a/docs/CAD/ux/mockups/cmp_8__edge_str.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Straight edge selected — pick what to do with itWorking document · 8 slotsDress-up2Pattern on CurveReference3Straight edge \ No newline at end of file diff --git a/docs/CAD/ux/mockups/cmp_8__face_planar.svg b/docs/CAD/ux/mockups/cmp_8__face_planar.svg deleted file mode 100644 index 5a58e2fb72..0000000000 --- a/docs/CAD/ux/mockups/cmp_8__face_planar.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Planar face selected — pick what to do with itWorking document · 8 slotsSketchShift+SAdd material2Remove2Dress-up3PatternShift+NTransform2Reference5Modify2Planar face \ No newline at end of file diff --git a/docs/CAD/ux/mockups/cmp_8__sk_line.svg b/docs/CAD/ux/mockups/cmp_8__sk_line.svg deleted file mode 100644 index 4ca4000d59..0000000000 --- a/docs/CAD/ux/mockups/cmp_8__sk_line.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Sketch line selected — pick what to do with itWorking document · 8 slotsCreate9OffsetOTrimTDress-up2MirrorMMoveReference3Modify2Sketch line \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__art.svg b/docs/CAD/ux/mockups/fresh__art.svg deleted file mode 100644 index a54dd49b43..0000000000 --- a/docs/CAD/ux/mockups/fresh__art.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetText / imported art selected — pick what to do with itFresh document · 8 slotsABCModify2Text / imported art \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__art__modify.svg b/docs/CAD/ux/mockups/fresh__art__modify.svg deleted file mode 100644 index 184611c682..0000000000 --- a/docs/CAD/ux/mockups/fresh__art__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetModify — pick oneFresh document · 8 slotsABCDeleteDelEditText / imported art · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__bodies_2.svg b/docs/CAD/ux/mockups/fresh__bodies_2.svg deleted file mode 100644 index f282bef03a..0000000000 --- a/docs/CAD/ux/mockups/fresh__bodies_2.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetTwo bodies selected — pick what to do with itFresh document · 8 slotsMeasureDeleteDelTwo bodies \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__body_sheet.svg b/docs/CAD/ux/mockups/fresh__body_sheet.svg deleted file mode 100644 index 635b2f6aa6..0000000000 --- a/docs/CAD/ux/mockups/fresh__body_sheet.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetSheet body selected — pick what to do with itFresh document · 8 slotsMeasureModify2Sheet body \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__body_sheet__modify.svg b/docs/CAD/ux/mockups/fresh__body_sheet__modify.svg deleted file mode 100644 index 0cd330046d..0000000000 --- a/docs/CAD/ux/mockups/fresh__body_sheet__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetModify — pick oneFresh document · 8 slotsDeleteDelEditSheet body · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__body_solid.svg b/docs/CAD/ux/mockups/fresh__body_solid.svg deleted file mode 100644 index ef21e2b8e2..0000000000 --- a/docs/CAD/ux/mockups/fresh__body_solid.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetSolid body selected — pick what to do with itFresh document · 8 slotsMeasureModify2Solid body \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__body_solid__modify.svg b/docs/CAD/ux/mockups/fresh__body_solid__modify.svg deleted file mode 100644 index 8b5d1485a0..0000000000 --- a/docs/CAD/ux/mockups/fresh__body_solid__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetModify — pick oneFresh document · 8 slotsDeleteDelEditSolid body · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__coordsys.svg b/docs/CAD/ux/mockups/fresh__coordsys.svg deleted file mode 100644 index e20d08d000..0000000000 --- a/docs/CAD/ux/mockups/fresh__coordsys.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetCoordinate system selected — pick what to do with itFresh document · 8 slotsModify2Coordinate system \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__coordsys__modify.svg b/docs/CAD/ux/mockups/fresh__coordsys__modify.svg deleted file mode 100644 index b6b4871d16..0000000000 --- a/docs/CAD/ux/mockups/fresh__coordsys__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetModify — pick oneFresh document · 8 slotsDeleteDelEditCoordinate system · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__datum_axis.svg b/docs/CAD/ux/mockups/fresh__datum_axis.svg deleted file mode 100644 index 8174807ceb..0000000000 --- a/docs/CAD/ux/mockups/fresh__datum_axis.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetDatum axis selected — pick what to do with itFresh document · 8 slotsModify2Datum axis \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__datum_axis__modify.svg b/docs/CAD/ux/mockups/fresh__datum_axis__modify.svg deleted file mode 100644 index cba7005358..0000000000 --- a/docs/CAD/ux/mockups/fresh__datum_axis__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetModify — pick oneFresh document · 8 slotsDeleteDelEditDatum axis · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__datum_plane.svg b/docs/CAD/ux/mockups/fresh__datum_plane.svg deleted file mode 100644 index ffabd59969..0000000000 --- a/docs/CAD/ux/mockups/fresh__datum_plane.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetDatum plane selected — pick what to do with itFresh document · 8 slotsSketchShift+SReference2Modify2Datum plane \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__datum_plane__modify.svg b/docs/CAD/ux/mockups/fresh__datum_plane__modify.svg deleted file mode 100644 index 9ac26e4ce1..0000000000 --- a/docs/CAD/ux/mockups/fresh__datum_plane__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetModify — pick oneFresh document · 8 slotsDeleteDelEditDatum plane · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__datum_plane__reference.svg b/docs/CAD/ux/mockups/fresh__datum_plane__reference.svg deleted file mode 100644 index b42479441a..0000000000 --- a/docs/CAD/ux/mockups/fresh__datum_plane__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetReference — pick oneFresh document · 8 slotsPlaneShift+PHelixDatum plane · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__edge_circ.svg b/docs/CAD/ux/mockups/fresh__edge_circ.svg deleted file mode 100644 index 3e72544c04..0000000000 --- a/docs/CAD/ux/mockups/fresh__edge_circ.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetCircular edge selected — pick what to do with itFresh document · 8 slotsMeasureCircular edge \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__edge_str.svg b/docs/CAD/ux/mockups/fresh__edge_str.svg deleted file mode 100644 index c4e54d5335..0000000000 --- a/docs/CAD/ux/mockups/fresh__edge_str.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetStraight edge selected — pick what to do with itFresh document · 8 slotsReference3Straight edge \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__edge_str__reference.svg b/docs/CAD/ux/mockups/fresh__edge_str__reference.svg deleted file mode 100644 index 80dd7a877a..0000000000 --- a/docs/CAD/ux/mockups/fresh__edge_str__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetReference — pick oneFresh document · 8 slotsMeasurePlaneShift+PAxisShift+AStraight edge · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__face_cyl.svg b/docs/CAD/ux/mockups/fresh__face_cyl.svg deleted file mode 100644 index d2335e4177..0000000000 --- a/docs/CAD/ux/mockups/fresh__face_cyl.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetCylindrical face selected — pick what to do with itFresh document · 8 slotsReference3EditCylindrical face \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__face_cyl__reference.svg b/docs/CAD/ux/mockups/fresh__face_cyl__reference.svg deleted file mode 100644 index 7cb83afb56..0000000000 --- a/docs/CAD/ux/mockups/fresh__face_cyl__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetReference — pick oneFresh document · 8 slotsMeasureAxisShift+AHelixCylindrical face · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__face_other.svg b/docs/CAD/ux/mockups/fresh__face_other.svg deleted file mode 100644 index d8dcfb96ab..0000000000 --- a/docs/CAD/ux/mockups/fresh__face_other.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetCurved face selected — pick what to do with itFresh document · 8 slotsMeasureEditCurved face \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__face_planar.svg b/docs/CAD/ux/mockups/fresh__face_planar.svg deleted file mode 100644 index a66268fc9a..0000000000 --- a/docs/CAD/ux/mockups/fresh__face_planar.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetPlanar face selected — pick what to do with itFresh document · 8 slotsSketchShift+SExtrudeShift+EReference4EditPlanar face \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__face_planar__reference.svg b/docs/CAD/ux/mockups/fresh__face_planar__reference.svg deleted file mode 100644 index 9aa608a5e7..0000000000 --- a/docs/CAD/ux/mockups/fresh__face_planar__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetReference — pick oneFresh document · 8 slotsCoord SysShift+CMeasurePlaneShift+PAxisShift+APlanar face · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__none.svg b/docs/CAD/ux/mockups/fresh__none.svg deleted file mode 100644 index 56ac028b74..0000000000 --- a/docs/CAD/ux/mockups/fresh__none.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetClick a face or a reference plane, then a toolFresh document · 8 slotsSketchShift+SReference4Nothing selected \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__none__reference.svg b/docs/CAD/ux/mockups/fresh__none__reference.svg deleted file mode 100644 index 0492aa6de0..0000000000 --- a/docs/CAD/ux/mockups/fresh__none__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetReference — pick oneFresh document · 8 slotsCoord SysShift+CHelixPlaneShift+PAxisShift+ANothing selected · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_2ent.svg b/docs/CAD/ux/mockups/fresh__sk_2ent.svg deleted file mode 100644 index dd02122149..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_2ent.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetTwo sketch entities selected — pick what to do with itFresh document · 8 slotsCreate9OffsetOTrimTDress-up2MirrorMMoveReference3Modify2Two sketch entities \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_2ent__create.svg b/docs/CAD/ux/mockups/fresh__sk_2ent__create.svg deleted file mode 100644 index 98e984bff2..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_2ent__create.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetCreate — pick oneFresh document · 8 slotsLineLRectangleRCircleCArcASlotSEllipseESplineBMore2Two sketch entities · Create \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_2ent__dressup.svg b/docs/CAD/ux/mockups/fresh__sk_2ent__dressup.svg deleted file mode 100644 index 926594bb86..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_2ent__dressup.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetDress-up — pick oneFresh document · 8 slotsFilletFChamferHTwo sketch entities · Dress-up \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_2ent__modify.svg b/docs/CAD/ux/mockups/fresh__sk_2ent__modify.svg deleted file mode 100644 index 242332967e..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_2ent__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetModify — pick oneFresh document · 8 slotsDeleteDelExtendXTwo sketch entities · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_2ent__reference.svg b/docs/CAD/ux/mockups/fresh__sk_2ent__reference.svg deleted file mode 100644 index aa105c3a9e..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_2ent__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetReference — pick oneFresh document · 8 slotsConstructionQDimensionDConstrainKTwo sketch entities · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_arc.svg b/docs/CAD/ux/mockups/fresh__sk_arc.svg deleted file mode 100644 index 0712508411..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_arc.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetSketch arc or circle selected — pick what to do with itFresh document · 8 slotsCreate9OffsetOTrimTMirrorMMoveReference3Modify2Sketch arc or circle \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_arc__create.svg b/docs/CAD/ux/mockups/fresh__sk_arc__create.svg deleted file mode 100644 index d22d916e60..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_arc__create.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetCreate — pick oneFresh document · 8 slotsLineLRectangleRCircleCArcASlotSEllipseESplineBMore2Sketch arc or circle · Create \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_arc__modify.svg b/docs/CAD/ux/mockups/fresh__sk_arc__modify.svg deleted file mode 100644 index cf3fc2450f..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_arc__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetModify — pick oneFresh document · 8 slotsDeleteDelExtendXSketch arc or circle · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_arc__reference.svg b/docs/CAD/ux/mockups/fresh__sk_arc__reference.svg deleted file mode 100644 index cfbe0a1042..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_arc__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetReference — pick oneFresh document · 8 slotsConstructionQDimensionDConstrainKSketch arc or circle · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_line.svg b/docs/CAD/ux/mockups/fresh__sk_line.svg deleted file mode 100644 index e40d823e13..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_line.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetSketch line selected — pick what to do with itFresh document · 8 slotsCreate9OffsetOTrimTDress-up2MirrorMMoveReference3Modify2Sketch line \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_line__create.svg b/docs/CAD/ux/mockups/fresh__sk_line__create.svg deleted file mode 100644 index 1a6f7b561c..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_line__create.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetCreate — pick oneFresh document · 8 slotsLineLRectangleRCircleCArcASlotSEllipseESplineBMore2Sketch line · Create \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_line__dressup.svg b/docs/CAD/ux/mockups/fresh__sk_line__dressup.svg deleted file mode 100644 index 3fce662ba1..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_line__dressup.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetDress-up — pick oneFresh document · 8 slotsFilletFChamferHSketch line · Dress-up \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_line__modify.svg b/docs/CAD/ux/mockups/fresh__sk_line__modify.svg deleted file mode 100644 index 26318535ef..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_line__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetModify — pick oneFresh document · 8 slotsDeleteDelExtendXSketch line · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_line__reference.svg b/docs/CAD/ux/mockups/fresh__sk_line__reference.svg deleted file mode 100644 index 2abc91fc99..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_line__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetReference — pick oneFresh document · 8 slotsConstructionQDimensionDConstrainKSketch line · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_loop.svg b/docs/CAD/ux/mockups/fresh__sk_loop.svg deleted file mode 100644 index 58eaa9e4fb..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_loop.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetClosed sketch loop selected — pick what to do with itFresh document · 8 slotsAdd material5Modify2Closed sketch loop \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_loop__add.svg b/docs/CAD/ux/mockups/fresh__sk_loop__add.svg deleted file mode 100644 index c6e6c90701..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_loop__add.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetAdd material — pick oneFresh document · 8 slotsExtrudeShift+ERevolveShift+RSurface ExtrudeShift+GSurface RevolveSurface FillClosed sketch loop · Add material \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_loop__modify.svg b/docs/CAD/ux/mockups/fresh__sk_loop__modify.svg deleted file mode 100644 index 416e62d7cd..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_loop__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetModify — pick oneFresh document · 8 slotsDeleteDelEditClosed sketch loop · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_none.svg b/docs/CAD/ux/mockups/fresh__sk_none.svg deleted file mode 100644 index ce7604c47d..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_none.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetSketch, nothing picked selected — pick what to do with itFresh document · 8 slotsCreate9Reference2Sketch, nothing picked \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_none__create.svg b/docs/CAD/ux/mockups/fresh__sk_none__create.svg deleted file mode 100644 index 06994f9517..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_none__create.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetCreate — pick oneFresh document · 8 slotsLineLRectangleRCircleCArcASlotSEllipseESplineBMore2Sketch, nothing picked · Create \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_none__reference.svg b/docs/CAD/ux/mockups/fresh__sk_none__reference.svg deleted file mode 100644 index e4e558bbd9..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_none__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetReference — pick oneFresh document · 8 slotsDimensionDConstructionQSketch, nothing picked · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_point.svg b/docs/CAD/ux/mockups/fresh__sk_point.svg deleted file mode 100644 index 65acf94ac7..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_point.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetSketch point selected — pick what to do with itFresh document · 8 slotsCreate9MoveReference2DeleteDelSketch point \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_point__create.svg b/docs/CAD/ux/mockups/fresh__sk_point__create.svg deleted file mode 100644 index fbe094d147..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_point__create.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetCreate — pick oneFresh document · 8 slotsLineLRectangleRCircleCArcASlotSEllipseESplineBMore2Sketch point · Create \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__sk_point__reference.svg b/docs/CAD/ux/mockups/fresh__sk_point__reference.svg deleted file mode 100644 index 8b02eba29f..0000000000 --- a/docs/CAD/ux/mockups/fresh__sk_point__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetReference — pick oneFresh document · 8 slotsDimensionDConstrainKSketch point · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__vertex.svg b/docs/CAD/ux/mockups/fresh__vertex.svg deleted file mode 100644 index 1e1661d6aa..0000000000 --- a/docs/CAD/ux/mockups/fresh__vertex.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetVertex selected — pick what to do with itFresh document · 8 slotsReference4Vertex \ No newline at end of file diff --git a/docs/CAD/ux/mockups/fresh__vertex__reference.svg b/docs/CAD/ux/mockups/fresh__vertex__reference.svg deleted file mode 100644 index aad3d46b6e..0000000000 --- a/docs/CAD/ux/mockups/fresh__vertex__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetReference — pick oneFresh document · 8 slotsCoord SysShift+CMeasurePlaneShift+PAxisShift+AVertex · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/gen_offer_mockups.py b/docs/CAD/ux/mockups/gen_offer_mockups.py deleted file mode 100644 index 6ddd4caae4..0000000000 --- a/docs/CAD/ux/mockups/gen_offer_mockups.py +++ /dev/null @@ -1,884 +0,0 @@ -#!/usr/bin/env python3 -"""Render the object-driven tool offer from docs/ux/tool_atlas.json. - -Fork-neutral on purpose: this file and everything it emits name no product, so the -two forks carry byte-identical copies (the charter itself is the only doc that -substitutes the product name). - -Every mockup in docs/ux/mockups/ and the review page docs/ux/offer_atlas.html are -generated by this script. Nothing is hand-drawn: the point of the offer is that a -tool's address never changes, and a human drawing 40-odd states by hand is exactly -how an address quietly changes. - - python3 docs/ux/mockups/gen_offer_mockups.py - -It fails loudly rather than rendering a broken map: an unknown slot, an unknown -selection id in `accepts`, or one address holding two different verbs for the same -selection is an error, not a warning. - -SVG on purpose. There is no rsvg/inkscape/cairosvg on the build box and -ImageMagick would rasterise through its own weak internal renderer; SVG renders -exactly in a browser at any zoom and stays diffable in git. Rasterise later if -slides need it. -""" - -import json -import re -import math -import os -import sys -from collections import defaultdict - -HERE = os.path.dirname(os.path.abspath(__file__)) -UX = os.path.dirname(HERE) -ATLAS = os.path.join(UX, "tool_atlas.json") - -# The reach target from the charter (L11 / 6.1): if the ring plus a real part do not -# fit here, the design has failed before it is built. -W, H = 1366, 768 - -C = { - "app": "#1a1d21", "chrome": "#23272d", "panel": "#20242a", "line": "#31363d", - "vp0": "#2f353d", "vp1": "#242930", "grid": "#333a43", - "text": "#e7ecf1", "muted": "#8e9aa7", "dim": "#5f6a76", - "top": "#93a1b1", "left": "#6f7b89", "right": "#5a6472", "edge": "#39414b", - "hi": "#f5a623", "hi_soft": "#f5a62333", "sheet": "#7fa8c9", - "ring": "#171a1e", "chip": "#2e343c", "chip_line": "#3c444e", - "key": "#454f5b", "accent": "#4f9bd9", "empty": "#2a2f36", -} - - -# ---------------------------------------------------------------- glyphs -# Each glyph draws inside a 24x24 box centred on (0,0) — i.e. -12..12. -def _g(body): - return body - - -GLYPHS = { - # model - "sketch": '', - "extrude": '', - "revolve": '', - "sweep": '', - "loft": '', - "thicken": '', - "rib": '', - "boolean": '', - "hole": '', - "thread": '', - "shell": '', - "cut": '', - "split": '', - "fillet": '', - "chamfer": '', - "draft": '', - "surf_off": '', - "pattern": '', - "mirror": '', - "pat_curve": '', - "move": '', - "mate": '', - "align": '', - "plane": '', - "axis": '', - "csys": '', - "helix": '', - "project": '', - "measure": '', - "mass": '', - "interfere": '', - "edit": '', - "del_face": '', - "colour": '', - "delete": '', - # sketch primitives - "sk_line": '', - "sk_rect": '', - "sk_circle": '', - "sk_arc": '', - "sk_slot": '', - "sk_ell": '', - "sk_spline": '', - "sk_poly": '', - "sk_point": '', - "sk_offset": '', - "sk_trim": '', - "sk_ext": '', - "sk_mir": '', - "sk_move": '', - "sk_dim": '', - "sk_lock": '', - "sk_constr": '', - # families (used when a slot holds more than one verb) - "fam_create": '', - "fam_add": '', - "fam_remove": '', - "fam_dressup": '', - "fam_repeat": '', - "fam_transform": '', - "fam_reference": '', - # Deliberately NOT the bare pencil: create/sketch already owns that shape and the two slots - # are adjacent (N and NW). A pencil over a solid reads as "change the thing that exists". - "fam_modify": '' - '', -} - -VERB_GLYPH = { - "sketch": "sketch", "extrude": "extrude", "revolve": "revolve", "sweep": "sweep", - "loft": "loft", "thicken": "thicken", "rib": "rib", "boolean": "boolean", - "surf_extrude": "extrude", "surf_revolve": "revolve", "surf_loft": "loft", - "surf_fill": "surf_off", "thicken_surf": "thicken", "hole": "hole", "thread": "thread", - "shell": "shell", "cut": "cut", "split": "split", "fillet": "fillet", - "chamfer": "chamfer", "draft": "draft", "surf_offset": "surf_off", - "pattern": "pattern", "mirror": "mirror", "pat_curve": "pat_curve", - "transform": "move", "mate": "mate", "align": "align", "plane": "plane", - "axis": "axis", "coordsys_v": "csys", "helix": "helix", "project": "project", - "measure": "measure", "mass_props": "mass", "interference": "interfere", - "edit_feature": "edit", "delete_face": "del_face", "colour": "colour", "delete": "delete", - "sk_line_t": "sk_line", "sk_rect": "sk_rect", "sk_circle": "sk_circle", - "sk_arc_t": "sk_arc", "sk_slot": "sk_slot", "sk_ellipse": "sk_ell", - "sk_spline": "sk_spline", "sk_polygon": "sk_poly", "sk_point_t": "sk_point", - "sk_offset": "sk_offset", "sk_trim": "sk_trim", "sk_fillet": "fillet", - "sk_chamfer": "chamfer", "sk_mirror": "sk_mir", "sk_move": "sk_move", - "sk_dimension": "sk_dim", "sk_constrain": "sk_lock", "sk_construct": "sk_constr", - "sk_extend": "sk_ext", "sk_delete": "delete", -} - - -def glyph(name, cx, cy, colour, scale=1.0, sw=1.6): - body = GLYPHS.get(name, '') - return (f'{body}') - - -# ---------------------------------------------------------------- iso scene -COS30, SIN30 = math.cos(math.radians(30)), math.sin(math.radians(30)) - - -def iso(x, y, z, ox, oy, s=1.0): - return (ox + (x - y) * COS30 * s, oy + (x + y) * SIN30 * s - z * s) - - -def poly(pts, fill, stroke=None, extra=""): - d = " ".join(f"{x:.1f},{y:.1f}" for x, y in pts) - # Only emit our own stroke-width when the caller has not supplied one: a duplicate - # attribute is last-one-wins in a browser but invalid markup, and it is exactly the - # kind of thing a stricter renderer refuses outright. - if stroke: - st = f' stroke="{stroke}"' + ("" if "stroke-width" in extra else ' stroke-width="1"') - else: - st = ' stroke="none"' - return f'' - - -def box(ox, oy, w=150, d=105, h=52, s=1.0, hi=None): - """Three visible faces of a box. `hi` names the face/edge to highlight.""" - P = lambda x, y, z: iso(x, y, z, ox, oy, s) - top = [P(0, 0, h), P(w, 0, h), P(w, d, h), P(0, d, h)] - left = [P(0, d, h), P(w, d, h), P(w, d, 0), P(0, d, 0)] - right = [P(w, 0, h), P(w, d, h), P(w, d, 0), P(w, 0, 0)] - out = [ - poly(left, C["left"], C["edge"]), - poly(right, C["right"], C["edge"]), - poly(top, C["hi"] if hi == "top" else C["top"], C["edge"]), - ] - if hi == "top": - out.append(poly(top, "none", "#ffd07a", ' stroke-width="2.5"')) - if hi == "front_edge": - a, b = P(0, d, h), P(w, d, h) - out.append(f'') - if hi == "vertex": - v = P(w, d, h) - out.append(f'') - if hi == "whole": - out.append(poly(top, "none", "#ffd07a", ' stroke-width="2.5"')) - out.append(poly(left, "none", "#ffd07a", ' stroke-width="2.5"')) - out.append(poly(right, "none", "#ffd07a", ' stroke-width="2.5"')) - return "".join(out), P - - -def bore(P, cx, cy, h, r=22, s=1.0, hi=False): - """A cylindrical bore through the top face, drawn as ellipse + wall.""" - c = P(cx, cy, h) - rx, ry = r * COS30 * 2 * s, r * SIN30 * 2 * s - col = C["hi"] if hi else "#20252b" - wall = C["hi"] if hi == "wall" else "#3b434d" - return (f'' - f'' - + (f'' if hi else "")), c - - -def scene(kind, ox, oy): - """Return (svg, anchor) for a selection kind. Anchor = where the ring centres.""" - s = 1.0 - if kind == "origin": - # A document with nothing in it: the three origin planes are all there is to click. - P = lambda x, y, z: iso(x, y, z, ox, oy, s) - g = poly([P(-70, -70, 0), P(70, -70, 0), P(70, 70, 0), P(-70, 70, 0)], - "#4f9bd91f", "#4f9bd977", ' stroke-width="1.5" stroke-dasharray="6 4"') - g += poly([P(-70, 0, -70), P(70, 0, -70), P(70, 0, 70), P(-70, 0, 70)], - "#e05c5c14", "#e05c5c66", ' stroke-width="1.5" stroke-dasharray="6 4"') - g += poly([P(0, -70, -70), P(0, 70, -70), P(0, 70, 70), P(0, -70, 70)], - "#5ce07a14", "#5ce07a66", ' stroke-width="1.5" stroke-dasharray="6 4"') - return g, P(0, 0, 0) - if kind == "empty": - g, P = box(ox, oy, s=s) - return g, P(75, 52, 26) - if kind == "box_top": - g, P = box(ox, oy, s=s, hi="top") - return g, P(75, 52, 52) - if kind == "box_whole": - g, P = box(ox, oy, s=s, hi="whole") - return g, P(75, 52, 26) - if kind == "box_edge": - g, P = box(ox, oy, s=s, hi="front_edge") - return g, P(75, 105, 52) - if kind == "box_vertex": - g, P = box(ox, oy, s=s, hi="vertex") - return g, P(150, 105, 52) - if kind in ("box_bore", "box_bore_rim"): - g, P = box(ox, oy, s=s) - b, c = bore(P, 75, 52, 52, hi=("wall" if kind == "box_bore" else True)) - return g + b, c - if kind == "box_fillet_face": - g, P = box(ox, oy, s=s) - a, b = P(0, 0, 52), P(0, 105, 52) - g += (f'') - return g, P(0, 52, 52) - if kind == "sheet": - P = lambda x, y, z: iso(x, y, z, ox, oy, s) - pts = [P(0, 0, 40), P(150, 0, 55), P(150, 105, 30), P(0, 105, 18)] - return (poly(pts, C["sheet"], "#ffd07a", ' stroke-width="2.5" opacity="0.85"'), - P(75, 52, 36)) - if kind == "two_boxes": - g1, P1 = box(ox - 60, oy - 10, w=110, d=80, h=44, hi="whole") - g2, P2 = box(ox + 70, oy + 26, w=95, d=70, h=60, hi="whole") - return g1 + g2, (ox + 42, oy + 6) - if kind == "datum": - P = lambda x, y, z: iso(x, y, z, ox, oy, s) - g, _ = box(ox, oy, s=s) - pts = [P(-25, -25, 70), P(175, -25, 70), P(175, 130, 70), P(-25, 130, 70)] - g += poly(pts, "#4f9bd933", C["hi"], ' stroke-width="2.5" stroke-dasharray="6 4"') - return g, P(75, 52, 70) - if kind == "axis": - g, P = box(ox, oy, s=s) - a, b = P(-30, 52, 52), P(180, 52, 52) - g += (f'') - return g, P(75, 52, 52) - if kind == "csys": - P = lambda x, y, z: iso(x, y, z, ox, oy, s) - g, _ = box(ox, oy, s=s) - o = P(0, 0, 52) - for tgt, col in ((P(60, 0, 52), "#e05c5c"), (P(0, 60, 52), "#5ce07a"), (P(0, 0, 112), "#5c9ce0")): - g += (f'') - g += f'' - return g, o - if kind == "art": - P = lambda x, y, z: iso(x, y, z, ox, oy, s) - g, _ = box(ox, oy, s=s) - c = P(75, 52, 52) - g += (f'' - f'ABC') - return g, c - if kind == "loop": - P = lambda x, y, z: iso(x, y, z, ox, oy, s) - g, _ = box(ox, oy, s=s) - pts = [P(28, 22, 52), P(122, 22, 52), P(122, 83, 52), P(28, 83, 52)] - g += poly(pts, C["hi_soft"], C["hi"], ' stroke-width="3"') - return g, P(75, 52, 52) - # --- sketch mode: flat, camera normal to the plane - gx, gy = ox - 30, oy - 10 - grid = "".join( - f'' - for i in range(11)) + "".join( - f'' - for i in range(16)) - if kind == "sk_empty": - return grid, (gx, gy) - if kind == "sk_line": - return (grid + f'' - f'' - f'', (gx, gy + 10)) - if kind == "sk_arc": - return (grid + f'', (gx, gy)) - if kind == "sk_point": - return (grid + f'', (gx, gy)) - if kind == "sk_two": - return (grid + f'' - f'', (gx, gy + 5)) - return grid, (gx, gy) - - -# ---------------------------------------------------------------- app chrome -def chrome(title, status, mode="model", empty_doc=False): - tabs = ["Prepare", "Preview", "Design"] - g = [f'', - f'' - f'' - f'', - f'', - f''] - x = 24 - for t in tabs: - on = (t == "Design") - g.append(f'{t}') - if on: - g.append(f'') - x += len(t) * 9 + 34 - # toolbar - g.append(f'' - f'') - fams = (["sketch", "extrude", "hole", "fillet", "pattern", "boolean", "plane", "move", "measure"] - if mode == "model" else - ["sk_line", "sk_rect", "sk_circle", "sk_arc", "sk_slot", "sk_spline", "sk_dim", "sk_trim", "sk_lock"]) - for i, gl in enumerate(fams): - cx = 40 + i * 46 - g.append(f'') - g.append(glyph(gl, cx, 68, C["muted"], 0.62, 1.7)) - # left rail - g.append(f'' - f'') - g.append(f'FEATURES') - # A "fresh document" mockup that shows six existing features is a lie, and it is the one - # state the group will look at hardest — it is the first-run picture (B5). - tree = ([] if empty_doc else - [("Sketch1", 0), ("Extrude1", 0), ("Hole1", 1), ("Fillet1", 1), ("Sketch2", 0), ("Extrude2", 0)]) - if empty_doc: - g.append(f'nothing yet') - for i, (n, ind) in enumerate(tree): - yy = 150 + i * 26 - g.append(glyph("sketch" if n.startswith("Sketch") else "extrude", 30 + ind * 14, yy - 4, C["dim"], 0.42, 1.8)) - g.append(f'{n}') - # status bar - g.append(f'' - f'{status}') - g.append(f'{title}') - return "".join(g) - - -# ---------------------------------------------------------------- the ring -def ring(cx, cy, items, capacity, sel_name, radius=134): - """items: dict angle_index -> (glyph, label, key, count) or None for an empty slot.""" - # The scrim is deliberately faint. §4.1: the offer "never blocks the view of what it acts - # on" — at 0.55 the disc swallowed the very face the ring was opened on, which is the rule - # failing in its own mockup. - g = [f'', - f'', - f''] - step = 360.0 / capacity - for i in range(capacity): - ang = math.radians(-90 + i * step) - px, py = cx + radius * math.cos(ang), cy + radius * math.sin(ang) - it = items.get(i) - if not it: - g.append(f'') - continue - gl, label, key, count = it - g.append(f'') - g.append(glyph(gl, px, py - 2, C["text"], 0.92, 1.7)) - g.append(f'{label}') - if key: - kw = 13 + len(key) * 6.6 - g.append(f'' - f'{key}') - if count and count > 1: - g.append(f'' - f'{count}') - # The selection name sits on the LOWER rim of the centre hole, not above it: above, it - # landed on top of the north slot's shortcut chip and hid it. Below, the pick stays - # visible through the hole and the pill is clear of the south chip at cy+104. - w = 13 + len(sel_name) * 6.9 - g.append(f'' - f'{sel_name}') - return "".join(g) - - -# ---------------------------------------------------------------- enumeration -def load(): - with open(ATLAS, encoding="utf-8") as f: - return json.load(f) - - -def validate(A): - slot_ids = {s["id"] for s in A["slots"]} - sel_ids = {s["id"] for s in A["selections"]} - errs = [] - seen = {} - for v in A["verbs"]: - if v["slot"] not in slot_ids: - errs.append(f'{v["id"]}: unknown slot {v["slot"]!r}') - for a in v["accepts"]: - if a not in sel_ids: - errs.append(f'{v["id"]}: accepts unknown selection {a!r}') - if v["id"] in seen: - errs.append(f'{v["id"]}: duplicate verb id') - seen[v["id"]] = v["slot"] - if errs: - sys.exit("tool_atlas.json is inconsistent:\n " + "\n ".join(errs)) - return slot_ids, sel_ids - - -def eligible(A, sel_id, doc): - sel = next(s for s in A["selections"] if s["id"] == sel_id) - out = [] - for v in A["verbs"]: - if v.get("mode", "model") != sel["mode"]: - continue - if sel_id not in v["accepts"]: - continue - n = v.get("needs") or {} - if n.get("bodies", 0) > doc["bodies"]: - continue - if n.get("sketches", 0) > doc["sketches"]: - continue - if n.get("sheet") and not doc["sheet"]: - continue - out.append(v) - return out - - -def by_slot(A, verbs): - order = [s["id"] for s in A["slots"]] - d = defaultdict(list) - for v in verbs: - d[v["slot"]].append(v) - return {k: d[k] for k in order if d[k]} - - -def ring_items(A, grouped): - order = [s["id"] for s in A["slots"]] - slot_label = {s["id"]: s["label"] for s in A["slots"]} - items = {} - for i, sid in enumerate(order): - vs = grouped.get(sid) - if not vs: - continue - if len(vs) == 1: - v = vs[0] - items[i] = (VERB_GLYPH.get(v["id"], "fam_" + sid), v["name"], v.get("key"), 1) - else: - items[i] = ("fam_" + sid, slot_label[sid], None, len(vs)) - return items - - -# ---------------------------------------------------------------- rendering -def menu(cx, cy, header, rows, submenu=None, sub_at=None): - """Vertical list form of the offer. rows: (glyph, name, key, count, enabled, reason). - - The invariant is unchanged — a verb has one permanent row index, and rows that do not - apply are DISABLED IN PLACE, never removed. What changes against the ring is what an - unavailable slot can say: an empty circle says nothing, a greyed row says its own name - and the reason it is grey, in the words the product already ships. - """ - RW, RH, HD = 324, 34, 38 - # The reason line is the whole point of a disabled row, so it must FIT: at 10px italic a - # glyph is ~4.9px, and anything past the box edge is a promise the layout does not keep. - fit = int((RW - 62) / 4.9) - x, y = cx + 26, cy - 30 - h = HD + len(rows) * RH + 10 - if y + h > H - 44: - y = max(100, H - 44 - h) - g = [f'', - f'', - f'{header.upper()}', - f''] - # a leader from the pick point to the menu, so the list is visibly ABOUT that geometry - g.insert(0, f'') - g.insert(0, f'') - for i, (gl, name, key, count, on, reason) in enumerate(rows): - ry = y + HD + i * RH - op = "1" if on else "0.34" - if on and i == 0: - g.append(f'') - g.append(f'') - g.append(glyph(gl, x + 26, ry + RH / 2, C["text"], 0.72, 1.7)) - g.append(f'{name}') - if key: - kw = 13 + len(key) * 6.6 - g.append(f'' - f'{key}') - elif count and count > 1: - g.append(f'') - g.append(f'{count}') - g.append('') - if not on and reason: - r = reason if len(reason) <= fit else reason[:fit - 1].rstrip(" ,—-") + "…" - g.append(f'{r}') - if submenu: - sy = y + HD + (sub_at or 0) * RH - 6 - sh = 12 + len(submenu) * RH - sx = x + RW + 8 - g.append(f'') - g.append(f'') - for i, (gl, name, key, _c, on, _r) in enumerate(submenu): - ry = sy + 6 + i * RH - g.append(f'') - g.append(glyph(gl, sx + 24, ry + RH / 2, C["text"], 0.72, 1.7)) - g.append(f'{name}') - if key: - kw = 13 + len(key) * 6.6 - g.append(f'' - f'{key}') - g.append('') - return "".join(g) - - -OVERFLOWS = [] # (selection, doc state, family, verbs that did not fit the sub-ring) - - -def svg_doc(inner): - return (f'{inner}') - - -def render_list_state(A, sel, doc, expand=None): - """The vertical-list form: every family row always present, in the same order, with the - ones that do not apply disabled and carrying their reason.""" - verbs = eligible(A, sel["id"], doc) - grouped = by_slot(A, verbs) - fresh = doc["bodies"] == 0 and doc["sketches"] == 0 - shape = "origin" if (fresh and sel["shape"] == "empty") else sel["shape"] - art, (ax, ay) = scene(shape, 620, 360) - rows, sub, sub_at = [], None, None - for idx, s in enumerate(A["slots"]): - vs = grouped.get(s["id"], []) - if len(vs) == 1: - v = vs[0] - rows.append((VERB_GLYPH.get(v["id"], "fam_" + s["id"]), v["name"], v.get("key"), 1, True, None)) - elif len(vs) > 1: - rows.append(("fam_" + s["id"], s["label"], None, len(vs), True, None)) - if expand == s["id"]: - sub_at = idx - sub = [(VERB_GLYPH.get(v["id"], "fam_" + s["id"]), v["name"], v.get("key"), 1, True, None) - for v in vs] - else: - # Disabled in place, with the product's own refusal text — the thing an empty - # slot in the ring could never say. - cands = [v for v in A["verbs"] - if v["slot"] == s["id"] and v.get("mode", "model") == sel["mode"]] - why = next((v["refusal"] for v in cands if v.get("refusal")), None) - rows.append(("fam_" + s["id"], s["label"], None, 0, False, why)) - status = ("Right-click the geometry to see what you can do with it" if not expand - else f'{sel["name"]} — pick one') - inner = chrome(f'{doc["name"]} · vertical list', status, sel["mode"], empty_doc=fresh) - inner += art - inner += menu(ax, ay, sel["name"], rows, sub, sub_at) - return svg_doc(inner) - - -def render_state(A, sel, doc, capacity=8, secondary=None): - verbs = eligible(A, sel["id"], doc) - grouped = by_slot(A, verbs) - anchor_x, anchor_y = 810, 400 - fresh = doc["bodies"] == 0 and doc["sketches"] == 0 - shape = "origin" if (fresh and sel["shape"] == "empty") else sel["shape"] - art, (ax, ay) = scene(shape, anchor_x - 90, anchor_y - 40) - if secondary: - vs = grouped[secondary] - # A sub-ring ANCHORS ON ITS PARENT'S DIRECTION. Fanning from north instead put Extrude - # at N — Create's address in the primary map — so the second level contradicted the - # first. Anchored, an address is two consistent strokes: Add material is NE, and the - # first verb of Add material is NE again. - base = [s["id"] for s in A["slots"]].index(secondary) - overflow = [] - if len(vs) > capacity: - # Do NOT wrap: (base+i) % capacity would quietly put verb 9 on top of verb 1, which - # is the invariant failing silently — the one outcome worse than an ugly ring. Show - # what fits, mark the rest, and report it. - overflow = vs[capacity - 1:] - vs = vs[:capacity - 1] - items = {(base + i) % capacity: - (VERB_GLYPH.get(v["id"], "fam_" + secondary), v["name"], v.get("key"), 1) - for i, v in enumerate(vs)} - if overflow: - items[(base + capacity - 1) % capacity] = ("fam_" + secondary, "More", None, len(overflow)) - OVERFLOWS.append((sel["name"], doc["id"], secondary, [v["name"] for v in overflow])) - label = next(s["label"] for s in A["slots"] if s["id"] == secondary) - sel_name = f'{sel["name"]} · {label}' - status = f'{label} — pick one' - else: - items = ring_items(A, grouped) - sel_name = sel["name"] - status = ("Click a face or a reference plane, then a tool" - if sel["id"] == "none" else f'{sel["name"]} selected — pick what to do with it') - inner = chrome(f'{doc["name"]} · {capacity} slots', status, sel["mode"], empty_doc=fresh) - inner += art - inner += ring(ax, ay, items, capacity, sel_name) - return svg_doc(inner), verbs, grouped - - -def main(): - A = load() - validate(A) - docs = {d["id"]: d for d in A["doc_states"]} - outdir = HERE - rows, files = [], [] - total_primary = total_secondary = 0 - fill_sum = fill_n = 0 - - for d in A["doc_states"]: - for sel in A["selections"]: - svg, verbs, grouped = render_state(A, sel, d) - name = f'{d["id"]}__{sel["id"]}.svg' - with open(os.path.join(outdir, name), "w", encoding="utf-8") as f: - f.write(svg) - files.append((name, f'{sel["name"]} — {d["name"]}')) - total_primary += 1 - fill_sum += len(grouped) - fill_n += 1 - secondaries = [] - for sid, vs in grouped.items(): - if len(vs) > 1: - s2, _, _ = render_state(A, sel, d, secondary=sid) - n2 = f'{d["id"]}__{sel["id"]}__{sid}.svg' - with open(os.path.join(outdir, n2), "w", encoding="utf-8") as f: - f.write(s2) - label = next(s["label"] for s in A["slots"] if s["id"] == sid) - files.append((n2, f'{sel["name"]} — {label} ring')) - secondaries.append(sid) - total_secondary += 1 - rows.append({ - "doc": d["name"], "sel": sel["name"], "mode": sel["mode"], - "verbs": len(verbs), "slots": len(grouped), - "second": len(secondaries), - "detail": {sid: [v["name"] for v in vs] for sid, vs in grouped.items()}, - }) - - # Form-factor comparison: the SAME state as a ring and as a vertical list. - for sid in ("face_planar", "edge_str", "body_solid", "sk_line", "none"): - sel = next(s for s in A["selections"] if s["id"] == sid) - d = docs["fresh"] if sid == "none" else docs["rich"] - with open(os.path.join(outdir, f"list__{sid}.svg"), "w", encoding="utf-8") as f: - f.write(render_list_state(A, sel, d)) - for sid, fam in (("face_planar", "add"), ("sk_none", "create")): - sel = next(s for s in A["selections"] if s["id"] == sid) - with open(os.path.join(outdir, f"list__{sid}__{fam}.svg"), "w", encoding="utf-8") as f: - f.write(render_list_state(A, sel, docs["rich"], expand=fam)) - - # comparison sheet: 8 vs 12 slots on the same three selections - for cap in (8, 12): - for sid in ("face_planar", "edge_str", "sk_line"): - sel = next(s for s in A["selections"] if s["id"] == sid) - svg, _, _ = render_state(A, sel, docs["rich"], capacity=cap) - n = f'cmp_{cap}__{sid}.svg' - with open(os.path.join(outdir, n), "w", encoding="utf-8") as f: - f.write(svg) - - write_atlas(A, rows, files, total_primary, total_secondary, - fill_sum / max(1, fill_n)) - print(f"{total_primary} primary + {total_secondary} secondary = " - f"{total_primary+total_secondary} offer states rendered") - print(f"mean populated slots per primary ring: {fill_sum/max(1,fill_n):.2f} of 8") - if OVERFLOWS: - seen = {(f, tuple(v)) for _, _, f, v in OVERFLOWS} - print(f"OVERFLOW: {len(OVERFLOWS)} sub-rings did not fit 8 slots — " - f"{len(seen)} distinct case(s):") - for f, v in sorted(seen): - print(f" {f}: {', '.join(v)} pushed behind 'More'") - - -def write_atlas(A, rows, files, n_prim, n_sec, mean_fill): - gui_missing = [v["name"] for v in A["verbs"] if not v.get("gui", True)] - esc = lambda s: (s.replace("&", "&").replace("<", "<").replace(">", ">")) - cards = "".join( - f'
{esc(t)}
{esc(t)}
' - for n, t in files) - cmp8 = "".join(f'
' - f'
8 slots — {s}
' - for s in ("face_planar", "edge_str", "sk_line")) - cmp12 = "".join(f'
' - f'
12 slots — {s}
' - for s in ("face_planar", "edge_str", "sk_line")) - if OVERFLOWS: - cases = sorted({(f, tuple(v)) for _, _, f, v in OVERFLOWS}) - over_html = ("Measured, not predicted: " + "; ".join( - f'the {esc(f)} sub-ring needs {8 + len(v)} addresses, so ' - f'{esc(", ".join(v))} {"is" if len(v) == 1 else "are"} pushed behind a “More” slot' - for f, v in cases) + - ". This is the decision the ring size actually turns on — either a verb moves to " - "another family, or the tail goes to a third level, or the ring is not eight. " - "It affects sketch mode only; every model-mode family fits.") - else: - over_html = "No sub-ring exceeds the ring capacity. Eight slots is sufficient everywhere." - trs = "".join( - f'{esc(r["sel"])}{esc(r["doc"])}{r["verbs"]}' - f'{r["slots"]}/8{r["second"]}' - f'{esc(" · ".join(k + ": " + ", ".join(v) for k, v in r["detail"].items()))}' - for r in rows) - html = f"""Design tab — offer atlas - -
-

The offer — atlas of every state

-

Every state of the object-driven tool offer, generated from - docs/ux/tool_atlas.json. The invariant under test: a verb has one address, that - address is the same in every selection where it appears, and slots that do not apply are drawn - empty rather than compacted.

-
-
{len(A["verbs"])}verbs mapped
-
{len(A["selections"])}selection kinds
-
{n_prim}primary rings
-
{n_sec}secondary rings
-
{n_prim+n_sec}states total
-
{mean_fill:.1f}/8mean slots filled
-
{len(gui_missing)}verbs with no GUI yet
-
-
-
-

Decision 0 — ring or vertical list

-

The live question. Both forms carry the SAME map and the same invariant — fixed order, never - re-sorted, nothing compacted; only the geometry differs. Left-click selects; right-click opens the - offer. What the list buys: a disabled row can state its own reason in the words the product - already ships, where an empty slot in a ring is mute; nine sketch primitives fit without an - overflow; shortcuts line up in a readable column; long translated names fit; and it is navigable - by arrow key and by screen reader, which a radial is not. What it costs: no equidistant flick - gesture, and travel to the last row is longer than to the nearest direction. Pairs below — - list first, the same state as a ring second.

- -

Decision 1 — ring capacity

-

The same three selections at eight and at twelve. Eight keeps 45° between neighbours, which is - the reliable eyes-free pointing threshold and maps 1:1 to the numpad; twelve buys direct addresses - for more verbs at 30° spacing and positions that stop being nameable.

-
{cmp8}
-
{cmp12}
- -

Decision 2 — one map or one per mode

-

These renders use ONE shared map: sketch verbs occupy the same eight families as model verbs, - so dress-up is south-east whether you picked a solid edge or a sketch line. Compare the sketch-mode - states below against the model-mode ones — if a verb that exists in both modes ever appears at two - addresses, the shared map has failed and the split map is the answer.

- -

The matrix — every selection, every document state

-
- - {trs} -
SelectionDocumentVerbsSlots2nd ringsWhat lands where
- -

Overflow — the one place eight slots is not enough

-

{over_html}

- -

Not in the offer

-

{esc(", ".join(A["chrome_only"]["items"]))} — these act on the document, not on a selection, - so they stay in chrome. Verbs with kernel support but no GUI today, which still hold an address: - {esc(", ".join(gui_missing))}.

- -

All states

-
{cards}
-
-""" - with open(os.path.join(UX, "offer_atlas.html"), "w", encoding="utf-8") as f: - f.write(html) - - # A second, SELF-CONTAINED copy for review outside the repo: the artifact host blocks - # every external request, so relative would silently show nothing. Curated - # rather than all 113 states — the whole set inlined is 1.5 MB, which is a poor thing to - # send to the low-end machine this design is meant to serve (6.1). - def inline(name): - p = os.path.join(HERE, name) - if not os.path.exists(p): - return "" - s = open(p, encoding="utf-8").read() - return s.replace("{inline(n)}
{esc(t)}
' - for n, t in picks if inline(n)) - head, _, tail = html.partition('

All states

') - # The comparison grids in the head use , which resolves to nothing - # once the page is served on its own. Inline those too rather than shipping empty frames. - head = re.sub(r']*>', - lambda m: inline(m.group(1)), head) - inline_html = head + '

The states

\n
' + figs + '
\n' - with open(os.path.join(UX, "offer_atlas_inline.html"), "w", encoding="utf-8") as f: - f.write(inline_html) - - -if __name__ == "__main__": - main() diff --git a/docs/CAD/ux/mockups/list__body_solid.svg b/docs/CAD/ux/mockups/list__body_solid.svg deleted file mode 100644 index ec3b3a84f4..0000000000 --- a/docs/CAD/ux/mockups/list__body_solid.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Right-click the geometry to see what you can do with itWorking document · vertical listSOLID BODYCreateClick a face or a reference plane in the viewport, t…Add materialCreate a sketch, or pick a solid face, firstRemove3Dress-up2Repeat3MoveShift+YReference3Modify3 \ No newline at end of file diff --git a/docs/CAD/ux/mockups/list__edge_str.svg b/docs/CAD/ux/mockups/list__edge_str.svg deleted file mode 100644 index 3942000d25..0000000000 --- a/docs/CAD/ux/mockups/list__edge_str.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Right-click the geometry to see what you can do with itWorking document · vertical listSTRAIGHT EDGECreateClick a face or a reference plane in the viewport, t…Add materialCreate a sketch, or pick a solid face, firstRemovePick a face or a plane to drill intoDress-up2Pattern on CurveTransformTransform needs a body — add or import one firstReference3ModifyDelete Face needs a body — add or import one first \ No newline at end of file diff --git a/docs/CAD/ux/mockups/list__face_planar.svg b/docs/CAD/ux/mockups/list__face_planar.svg deleted file mode 100644 index 5ff6892902..0000000000 --- a/docs/CAD/ux/mockups/list__face_planar.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Right-click the geometry to see what you can do with itWorking document · vertical listPLANAR FACESketchShift+SAdd material2Remove2Dress-up3PatternShift+NTransform2Reference5Modify2 \ No newline at end of file diff --git a/docs/CAD/ux/mockups/list__face_planar__add.svg b/docs/CAD/ux/mockups/list__face_planar__add.svg deleted file mode 100644 index fb80f2a5c7..0000000000 --- a/docs/CAD/ux/mockups/list__face_planar__add.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Planar face — pick oneWorking document · vertical listPLANAR FACESketchShift+SAdd material2Remove2Dress-up3PatternShift+NTransform2Reference5Modify2ExtrudeShift+EThicken \ No newline at end of file diff --git a/docs/CAD/ux/mockups/list__none.svg b/docs/CAD/ux/mockups/list__none.svg deleted file mode 100644 index 1e68606383..0000000000 --- a/docs/CAD/ux/mockups/list__none.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESnothing yetRight-click the geometry to see what you can do with itFresh document · vertical listNOTHING SELECTEDSketchShift+SAdd materialCreate a sketch, or pick a solid face, firstRemovePick a face or a plane to drill intoDress-upPick an edge to roundRepeatCreate a solid body to pattern firstTransformTransform needs a body — add or import one firstReference4ModifyDelete Face needs a body — add or import one first \ No newline at end of file diff --git a/docs/CAD/ux/mockups/list__sk_line.svg b/docs/CAD/ux/mockups/list__sk_line.svg deleted file mode 100644 index 1b1c2e9cd2..0000000000 --- a/docs/CAD/ux/mockups/list__sk_line.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Right-click the geometry to see what you can do with itWorking document · vertical listSKETCH LINECreate9OffsetOTrimTDress-up2MirrorMMoveReference3Modify2 \ No newline at end of file diff --git a/docs/CAD/ux/mockups/list__sk_none__create.svg b/docs/CAD/ux/mockups/list__sk_none__create.svg deleted file mode 100644 index 0b41268fa6..0000000000 --- a/docs/CAD/ux/mockups/list__sk_none__create.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Sketch, nothing picked — pick oneWorking document · vertical listSKETCH, NOTHING PICKEDCreate9Add materialRemoveDress-upRepeatTransformReference2ModifyLineLRectangleRCircleCArcASlotSEllipseESplineBPolygonGPointP \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__art.svg b/docs/CAD/ux/mockups/rich__art.svg deleted file mode 100644 index e835ed1ce0..0000000000 --- a/docs/CAD/ux/mockups/rich__art.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Text / imported art selected — pick what to do with itWorking document · 8 slotsABCPatternShift+NMoveShift+YModify2Text / imported art \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__art__modify.svg b/docs/CAD/ux/mockups/rich__art__modify.svg deleted file mode 100644 index 4c8cdef2b5..0000000000 --- a/docs/CAD/ux/mockups/rich__art__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Modify — pick oneWorking document · 8 slotsABCDeleteDelEditText / imported art · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__bodies_2.svg b/docs/CAD/ux/mockups/rich__bodies_2.svg deleted file mode 100644 index a977b7f1de..0000000000 --- a/docs/CAD/ux/mockups/rich__bodies_2.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Two bodies selected — pick what to do with itWorking document · 8 slotsCombineShift+BMateReference2DeleteDelTwo bodies \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__bodies_2__reference.svg b/docs/CAD/ux/mockups/rich__bodies_2__reference.svg deleted file mode 100644 index 1bfbac0db0..0000000000 --- a/docs/CAD/ux/mockups/rich__bodies_2__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Reference — pick oneWorking document · 8 slotsMeasureInterferenceTwo bodies · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__body_sheet.svg b/docs/CAD/ux/mockups/rich__body_sheet.svg deleted file mode 100644 index a1cc27808d..0000000000 --- a/docs/CAD/ux/mockups/rich__body_sheet.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Sheet body selected — pick what to do with itWorking document · 8 slotsThicken SurfaceSurface OffsetMoveShift+YMeasureModify3Sheet body \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__body_sheet__modify.svg b/docs/CAD/ux/mockups/rich__body_sheet__modify.svg deleted file mode 100644 index 7ce8dbee54..0000000000 --- a/docs/CAD/ux/mockups/rich__body_sheet__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Modify — pick oneWorking document · 8 slotsColourDeleteDelEditSheet body · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__body_solid.svg b/docs/CAD/ux/mockups/rich__body_solid.svg deleted file mode 100644 index e958532347..0000000000 --- a/docs/CAD/ux/mockups/rich__body_solid.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Solid body selected — pick what to do with itWorking document · 8 slotsRemove3Dress-up2Repeat3MoveShift+YReference3Modify3Solid body \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__body_solid__dressup.svg b/docs/CAD/ux/mockups/rich__body_solid__dressup.svg deleted file mode 100644 index 7a9ceed334..0000000000 --- a/docs/CAD/ux/mockups/rich__body_solid__dressup.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Dress-up — pick oneWorking document · 8 slotsFilletShift+FChamferSolid body · Dress-up \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__body_solid__modify.svg b/docs/CAD/ux/mockups/rich__body_solid__modify.svg deleted file mode 100644 index 7a6340d40f..0000000000 --- a/docs/CAD/ux/mockups/rich__body_solid__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Modify — pick oneWorking document · 8 slotsColourDeleteDelEditSolid body · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__body_solid__reference.svg b/docs/CAD/ux/mockups/rich__body_solid__reference.svg deleted file mode 100644 index 27639836c6..0000000000 --- a/docs/CAD/ux/mockups/rich__body_solid__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Reference — pick oneWorking document · 8 slotsMassProjectMeasureSolid body · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__body_solid__remove.svg b/docs/CAD/ux/mockups/rich__body_solid__remove.svg deleted file mode 100644 index edbddb7115..0000000000 --- a/docs/CAD/ux/mockups/rich__body_solid__remove.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Remove — pick oneWorking document · 8 slotsShellShift+KCutShift+XSplitSolid body · Remove \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__body_solid__repeat.svg b/docs/CAD/ux/mockups/rich__body_solid__repeat.svg deleted file mode 100644 index 74dd67c7de..0000000000 --- a/docs/CAD/ux/mockups/rich__body_solid__repeat.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Repeat — pick oneWorking document · 8 slotsPatternShift+NMirrorShift+ZPattern on CurveSolid body · Repeat \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__coordsys.svg b/docs/CAD/ux/mockups/rich__coordsys.svg deleted file mode 100644 index 8cef106106..0000000000 --- a/docs/CAD/ux/mockups/rich__coordsys.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Coordinate system selected — pick what to do with itWorking document · 8 slotsMateModify2Coordinate system \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__coordsys__modify.svg b/docs/CAD/ux/mockups/rich__coordsys__modify.svg deleted file mode 100644 index 92eb28d3dc..0000000000 --- a/docs/CAD/ux/mockups/rich__coordsys__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Modify — pick oneWorking document · 8 slotsDeleteDelEditCoordinate system · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__datum_axis.svg b/docs/CAD/ux/mockups/rich__datum_axis.svg deleted file mode 100644 index c9da648ab7..0000000000 --- a/docs/CAD/ux/mockups/rich__datum_axis.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Datum axis selected — pick what to do with itWorking document · 8 slotsModify2Datum axis \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__datum_axis__modify.svg b/docs/CAD/ux/mockups/rich__datum_axis__modify.svg deleted file mode 100644 index fd0ae80c52..0000000000 --- a/docs/CAD/ux/mockups/rich__datum_axis__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Modify — pick oneWorking document · 8 slotsDeleteDelEditDatum axis · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__datum_plane.svg b/docs/CAD/ux/mockups/rich__datum_plane.svg deleted file mode 100644 index 8ef88bd9a3..0000000000 --- a/docs/CAD/ux/mockups/rich__datum_plane.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Datum plane selected — pick what to do with itWorking document · 8 slotsSketchShift+SRemove2MirrorShift+ZReference3Modify2Datum plane \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__datum_plane__modify.svg b/docs/CAD/ux/mockups/rich__datum_plane__modify.svg deleted file mode 100644 index 39e5cad41e..0000000000 --- a/docs/CAD/ux/mockups/rich__datum_plane__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Modify — pick oneWorking document · 8 slotsDeleteDelEditDatum plane · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__datum_plane__reference.svg b/docs/CAD/ux/mockups/rich__datum_plane__reference.svg deleted file mode 100644 index bae3119b6b..0000000000 --- a/docs/CAD/ux/mockups/rich__datum_plane__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Reference — pick oneWorking document · 8 slotsProjectPlaneShift+PHelixDatum plane · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__datum_plane__remove.svg b/docs/CAD/ux/mockups/rich__datum_plane__remove.svg deleted file mode 100644 index c98b33ad31..0000000000 --- a/docs/CAD/ux/mockups/rich__datum_plane__remove.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Remove — pick oneWorking document · 8 slotsHoleShift+HCutShift+XDatum plane · Remove \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__edge_circ.svg b/docs/CAD/ux/mockups/rich__edge_circ.svg deleted file mode 100644 index 1d07291623..0000000000 --- a/docs/CAD/ux/mockups/rich__edge_circ.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Circular edge selected — pick what to do with itWorking document · 8 slotsThreadShift+TDress-up2MeasureCircular edge \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__edge_circ__dressup.svg b/docs/CAD/ux/mockups/rich__edge_circ__dressup.svg deleted file mode 100644 index b315fc2e22..0000000000 --- a/docs/CAD/ux/mockups/rich__edge_circ__dressup.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Dress-up — pick oneWorking document · 8 slotsFilletShift+FChamferCircular edge · Dress-up \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__edge_str.svg b/docs/CAD/ux/mockups/rich__edge_str.svg deleted file mode 100644 index 2c41ce74fc..0000000000 --- a/docs/CAD/ux/mockups/rich__edge_str.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Straight edge selected — pick what to do with itWorking document · 8 slotsDress-up2Pattern on CurveReference3Straight edge \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__edge_str__dressup.svg b/docs/CAD/ux/mockups/rich__edge_str__dressup.svg deleted file mode 100644 index 8e674c044f..0000000000 --- a/docs/CAD/ux/mockups/rich__edge_str__dressup.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Dress-up — pick oneWorking document · 8 slotsFilletShift+FChamferStraight edge · Dress-up \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__edge_str__reference.svg b/docs/CAD/ux/mockups/rich__edge_str__reference.svg deleted file mode 100644 index bc99ca6a13..0000000000 --- a/docs/CAD/ux/mockups/rich__edge_str__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Reference — pick oneWorking document · 8 slotsMeasurePlaneShift+PAxisShift+AStraight edge · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__face_cyl.svg b/docs/CAD/ux/mockups/rich__face_cyl.svg deleted file mode 100644 index 3c2860373f..0000000000 --- a/docs/CAD/ux/mockups/rich__face_cyl.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Cylindrical face selected — pick what to do with itWorking document · 8 slotsThreadShift+TReference3Modify2Cylindrical face \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__face_cyl__modify.svg b/docs/CAD/ux/mockups/rich__face_cyl__modify.svg deleted file mode 100644 index 9f573682ad..0000000000 --- a/docs/CAD/ux/mockups/rich__face_cyl__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Modify — pick oneWorking document · 8 slotsDelete FaceEditCylindrical face · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__face_cyl__reference.svg b/docs/CAD/ux/mockups/rich__face_cyl__reference.svg deleted file mode 100644 index a62be81de8..0000000000 --- a/docs/CAD/ux/mockups/rich__face_cyl__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Reference — pick oneWorking document · 8 slotsMeasureAxisShift+AHelixCylindrical face · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__face_other.svg b/docs/CAD/ux/mockups/rich__face_other.svg deleted file mode 100644 index e5cff4eee5..0000000000 --- a/docs/CAD/ux/mockups/rich__face_other.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Curved face selected — pick what to do with itWorking document · 8 slotsThickenDraftShift+DMeasureModify2Curved face \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__face_other__modify.svg b/docs/CAD/ux/mockups/rich__face_other__modify.svg deleted file mode 100644 index 8b5fcfd63f..0000000000 --- a/docs/CAD/ux/mockups/rich__face_other__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Modify — pick oneWorking document · 8 slotsDelete FaceEditCurved face · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__face_planar.svg b/docs/CAD/ux/mockups/rich__face_planar.svg deleted file mode 100644 index 5a58e2fb72..0000000000 --- a/docs/CAD/ux/mockups/rich__face_planar.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Planar face selected — pick what to do with itWorking document · 8 slotsSketchShift+SAdd material2Remove2Dress-up3PatternShift+NTransform2Reference5Modify2Planar face \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__face_planar__add.svg b/docs/CAD/ux/mockups/rich__face_planar__add.svg deleted file mode 100644 index 82acc85038..0000000000 --- a/docs/CAD/ux/mockups/rich__face_planar__add.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Add material — pick oneWorking document · 8 slotsExtrudeShift+EThickenPlanar face · Add material \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__face_planar__dressup.svg b/docs/CAD/ux/mockups/rich__face_planar__dressup.svg deleted file mode 100644 index 8d32b9f212..0000000000 --- a/docs/CAD/ux/mockups/rich__face_planar__dressup.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Dress-up — pick oneWorking document · 8 slotsFilletShift+FChamferDraftShift+DPlanar face · Dress-up \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__face_planar__modify.svg b/docs/CAD/ux/mockups/rich__face_planar__modify.svg deleted file mode 100644 index bffd218131..0000000000 --- a/docs/CAD/ux/mockups/rich__face_planar__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Modify — pick oneWorking document · 8 slotsDelete FaceEditPlanar face · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__face_planar__reference.svg b/docs/CAD/ux/mockups/rich__face_planar__reference.svg deleted file mode 100644 index 4682f1e7b1..0000000000 --- a/docs/CAD/ux/mockups/rich__face_planar__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Reference — pick oneWorking document · 8 slotsCoord SysShift+CProjectMeasurePlaneShift+PAxisShift+APlanar face · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__face_planar__remove.svg b/docs/CAD/ux/mockups/rich__face_planar__remove.svg deleted file mode 100644 index b77a5a0c50..0000000000 --- a/docs/CAD/ux/mockups/rich__face_planar__remove.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Remove — pick oneWorking document · 8 slotsHoleShift+HShellShift+KPlanar face · Remove \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__face_planar__transform.svg b/docs/CAD/ux/mockups/rich__face_planar__transform.svg deleted file mode 100644 index 2e27983c8b..0000000000 --- a/docs/CAD/ux/mockups/rich__face_planar__transform.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Transform — pick oneWorking document · 8 slotsMateAlign toPlanar face · Transform \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__none.svg b/docs/CAD/ux/mockups/rich__none.svg deleted file mode 100644 index 4e8a9dc640..0000000000 --- a/docs/CAD/ux/mockups/rich__none.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Click a face or a reference plane, then a toolWorking document · 8 slotsSketchShift+SReference4Nothing selected \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__none__reference.svg b/docs/CAD/ux/mockups/rich__none__reference.svg deleted file mode 100644 index 14c47c4b27..0000000000 --- a/docs/CAD/ux/mockups/rich__none__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Reference — pick oneWorking document · 8 slotsCoord SysShift+CHelixPlaneShift+PAxisShift+ANothing selected · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_2ent.svg b/docs/CAD/ux/mockups/rich__sk_2ent.svg deleted file mode 100644 index 738b01ecf2..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_2ent.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Two sketch entities selected — pick what to do with itWorking document · 8 slotsCreate9OffsetOTrimTDress-up2MirrorMMoveReference3Modify2Two sketch entities \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_2ent__create.svg b/docs/CAD/ux/mockups/rich__sk_2ent__create.svg deleted file mode 100644 index 6d27ffa34a..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_2ent__create.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Create — pick oneWorking document · 8 slotsLineLRectangleRCircleCArcASlotSEllipseESplineBMore2Two sketch entities · Create \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_2ent__dressup.svg b/docs/CAD/ux/mockups/rich__sk_2ent__dressup.svg deleted file mode 100644 index 929520b8d2..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_2ent__dressup.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Dress-up — pick oneWorking document · 8 slotsFilletFChamferHTwo sketch entities · Dress-up \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_2ent__modify.svg b/docs/CAD/ux/mockups/rich__sk_2ent__modify.svg deleted file mode 100644 index d8be7c2d4f..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_2ent__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Modify — pick oneWorking document · 8 slotsDeleteDelExtendXTwo sketch entities · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_2ent__reference.svg b/docs/CAD/ux/mockups/rich__sk_2ent__reference.svg deleted file mode 100644 index 9ffc2592a8..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_2ent__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Reference — pick oneWorking document · 8 slotsConstructionQDimensionDConstrainKTwo sketch entities · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_arc.svg b/docs/CAD/ux/mockups/rich__sk_arc.svg deleted file mode 100644 index d470369f1a..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_arc.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Sketch arc or circle selected — pick what to do with itWorking document · 8 slotsCreate9OffsetOTrimTMirrorMMoveReference3Modify2Sketch arc or circle \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_arc__create.svg b/docs/CAD/ux/mockups/rich__sk_arc__create.svg deleted file mode 100644 index 1f901596ec..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_arc__create.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Create — pick oneWorking document · 8 slotsLineLRectangleRCircleCArcASlotSEllipseESplineBMore2Sketch arc or circle · Create \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_arc__modify.svg b/docs/CAD/ux/mockups/rich__sk_arc__modify.svg deleted file mode 100644 index 52f9d7d3e5..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_arc__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Modify — pick oneWorking document · 8 slotsDeleteDelExtendXSketch arc or circle · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_arc__reference.svg b/docs/CAD/ux/mockups/rich__sk_arc__reference.svg deleted file mode 100644 index 53e949c184..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_arc__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Reference — pick oneWorking document · 8 slotsConstructionQDimensionDConstrainKSketch arc or circle · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_line.svg b/docs/CAD/ux/mockups/rich__sk_line.svg deleted file mode 100644 index 4ca4000d59..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_line.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Sketch line selected — pick what to do with itWorking document · 8 slotsCreate9OffsetOTrimTDress-up2MirrorMMoveReference3Modify2Sketch line \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_line__create.svg b/docs/CAD/ux/mockups/rich__sk_line__create.svg deleted file mode 100644 index c37f2d0e45..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_line__create.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Create — pick oneWorking document · 8 slotsLineLRectangleRCircleCArcASlotSEllipseESplineBMore2Sketch line · Create \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_line__dressup.svg b/docs/CAD/ux/mockups/rich__sk_line__dressup.svg deleted file mode 100644 index 29e86762d2..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_line__dressup.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Dress-up — pick oneWorking document · 8 slotsFilletFChamferHSketch line · Dress-up \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_line__modify.svg b/docs/CAD/ux/mockups/rich__sk_line__modify.svg deleted file mode 100644 index b9775b7d94..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_line__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Modify — pick oneWorking document · 8 slotsDeleteDelExtendXSketch line · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_line__reference.svg b/docs/CAD/ux/mockups/rich__sk_line__reference.svg deleted file mode 100644 index 6eb1ef621f..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_line__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Reference — pick oneWorking document · 8 slotsConstructionQDimensionDConstrainKSketch line · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_loop.svg b/docs/CAD/ux/mockups/rich__sk_loop.svg deleted file mode 100644 index c07d1c09c3..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_loop.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Closed sketch loop selected — pick what to do with itWorking document · 8 slotsAdd material8PatternShift+NModify2Closed sketch loop \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_loop__add.svg b/docs/CAD/ux/mockups/rich__sk_loop__add.svg deleted file mode 100644 index bf9f429b81..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_loop__add.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Add material — pick oneWorking document · 8 slotsSurface FillExtrudeShift+ERevolveShift+RSweepShift+WLoftShift+LSurface ExtrudeShift+GSurface RevolveSurface LoftClosed sketch loop · Add material \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_loop__modify.svg b/docs/CAD/ux/mockups/rich__sk_loop__modify.svg deleted file mode 100644 index c466379c9e..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_loop__modify.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Modify — pick oneWorking document · 8 slotsDeleteDelEditClosed sketch loop · Modify \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_none.svg b/docs/CAD/ux/mockups/rich__sk_none.svg deleted file mode 100644 index ffe49ab9a0..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_none.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Sketch, nothing picked selected — pick what to do with itWorking document · 8 slotsCreate9Reference2Sketch, nothing picked \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_none__create.svg b/docs/CAD/ux/mockups/rich__sk_none__create.svg deleted file mode 100644 index c9a2bc698d..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_none__create.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Create — pick oneWorking document · 8 slotsLineLRectangleRCircleCArcASlotSEllipseESplineBMore2Sketch, nothing picked · Create \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_none__reference.svg b/docs/CAD/ux/mockups/rich__sk_none__reference.svg deleted file mode 100644 index cc9d465fa4..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_none__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Reference — pick oneWorking document · 8 slotsDimensionDConstructionQSketch, nothing picked · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_point.svg b/docs/CAD/ux/mockups/rich__sk_point.svg deleted file mode 100644 index f089787bca..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_point.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Sketch point selected — pick what to do with itWorking document · 8 slotsCreate9MoveReference2DeleteDelSketch point \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_point__create.svg b/docs/CAD/ux/mockups/rich__sk_point__create.svg deleted file mode 100644 index 5d2070a6c5..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_point__create.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Create — pick oneWorking document · 8 slotsLineLRectangleRCircleCArcASlotSEllipseESplineBMore2Sketch point · Create \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__sk_point__reference.svg b/docs/CAD/ux/mockups/rich__sk_point__reference.svg deleted file mode 100644 index e090382a24..0000000000 --- a/docs/CAD/ux/mockups/rich__sk_point__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Reference — pick oneWorking document · 8 slotsDimensionDConstrainKSketch point · Reference \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__vertex.svg b/docs/CAD/ux/mockups/rich__vertex.svg deleted file mode 100644 index f37bbd7d47..0000000000 --- a/docs/CAD/ux/mockups/rich__vertex.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Vertex selected — pick what to do with itWorking document · 8 slotsReference4Vertex \ No newline at end of file diff --git a/docs/CAD/ux/mockups/rich__vertex__reference.svg b/docs/CAD/ux/mockups/rich__vertex__reference.svg deleted file mode 100644 index ceabba032e..0000000000 --- a/docs/CAD/ux/mockups/rich__vertex__reference.svg +++ /dev/null @@ -1 +0,0 @@ -PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Reference — pick oneWorking document · 8 slotsCoord SysShift+CMeasurePlaneShift+PAxisShift+AVertex · Reference \ No newline at end of file diff --git a/docs/CAD/ux/offer_atlas.html b/docs/CAD/ux/offer_atlas.html deleted file mode 100644 index 35c4f5860c..0000000000 --- a/docs/CAD/ux/offer_atlas.html +++ /dev/null @@ -1,78 +0,0 @@ -Design tab — offer atlas - -
-

The offer — atlas of every state

-

Every state of the object-driven tool offer, generated from - docs/ux/tool_atlas.json. The invariant under test: a verb has one address, that - address is the same in every selection where it appears, and slots that do not apply are drawn - empty rather than compacted.

-
-
60verbs mapped
-
20selection kinds
-
40primary rings
-
73secondary rings
-
113states total
-
3.5/8mean slots filled
-
5verbs with no GUI yet
-
-
-
-

Decision 0 — ring or vertical list

-

The live question. Both forms carry the SAME map and the same invariant — fixed order, never - re-sorted, nothing compacted; only the geometry differs. Left-click selects; right-click opens the - offer. What the list buys: a disabled row can state its own reason in the words the product - already ships, where an empty slot in a ring is mute; nine sketch primitives fit without an - overflow; shortcuts line up in a readable column; long translated names fit; and it is navigable - by arrow key and by screen reader, which a radial is not. What it costs: no equidistant flick - gesture, and travel to the last row is longer than to the nearest direction. Pairs below — - list first, the same state as a ring second.

- -

Decision 1 — ring capacity

-

The same three selections at eight and at twelve. Eight keeps 45° between neighbours, which is - the reliable eyes-free pointing threshold and maps 1:1 to the numpad; twelve buys direct addresses - for more verbs at 30° spacing and positions that stop being nameable.

-
8 slots — face_planar
8 slots — edge_str
8 slots — sk_line
-
12 slots — face_planar
12 slots — edge_str
12 slots — sk_line
- -

Decision 2 — one map or one per mode

-

These renders use ONE shared map: sketch verbs occupy the same eight families as model verbs, - so dress-up is south-east whether you picked a solid edge or a sketch line. Compare the sketch-mode - states below against the model-mode ones — if a verb that exists in both modes ever appears at two - addresses, the shared map has failed and the split map is the answer.

- -

The matrix — every selection, every document state

-
- - -
SelectionDocumentVerbsSlots2nd ringsWhat lands where
Nothing selectedWorking document52/81create: Sketch · reference: Plane, Axis, Coord Sys, Helix
Planar faceWorking document188/86create: Sketch · add: Extrude, Thicken · remove: Hole, Shell · dressup: Fillet, Chamfer, Draft · repeat: Pattern · transform: Mate, Align to · reference: Plane, Axis, Coord Sys, Project, Measure · modify: Edit, Delete Face
Cylindrical faceWorking document63/82remove: Thread · reference: Axis, Helix, Measure · modify: Edit, Delete Face
Curved faceWorking document54/81add: Thicken · dressup: Draft · reference: Measure · modify: Edit, Delete Face
Straight edgeWorking document63/82dressup: Fillet, Chamfer · repeat: Pattern on Curve · reference: Plane, Axis, Measure
Circular edgeWorking document43/81remove: Thread · dressup: Fillet, Chamfer · reference: Measure
VertexWorking document41/81reference: Plane, Axis, Coord Sys, Measure
Solid bodyWorking document156/85remove: Shell, Cut, Split · dressup: Fillet, Chamfer · repeat: Pattern, Mirror, Pattern on Curve · transform: Move · reference: Project, Measure, Mass · modify: Edit, Colour, Delete
Sheet bodyWorking document75/81add: Thicken Surface · dressup: Surface Offset · transform: Move · reference: Measure · modify: Edit, Colour, Delete
Two bodiesWorking document54/81add: Combine · transform: Mate · reference: Measure, Interference · modify: Delete
Datum planeWorking document95/83create: Sketch · remove: Hole, Cut · repeat: Mirror · reference: Plane, Helix, Project · modify: Edit, Delete
Datum axisWorking document21/81modify: Edit, Delete
Coordinate systemWorking document32/81transform: Mate · modify: Edit, Delete
Text / imported artWorking document43/81repeat: Pattern · transform: Move · modify: Edit, Delete
Closed sketch loopWorking document113/82add: Extrude, Revolve, Sweep, Loft, Surface Extrude, Surface Revolve, Surface Loft, Surface Fill · repeat: Pattern · modify: Edit, Delete
Sketch, nothing pickedWorking document112/82create: Line, Rectangle, Circle, Arc, Slot, Ellipse, Spline, Polygon, Point · reference: Dimension, Construction
Sketch lineWorking document208/84create: Line, Rectangle, Circle, Arc, Slot, Ellipse, Spline, Polygon, Point · add: Offset · remove: Trim · dressup: Fillet, Chamfer · repeat: Mirror · transform: Move · reference: Dimension, Constrain, Construction · modify: Extend, Delete
Sketch arc or circleWorking document187/83create: Line, Rectangle, Circle, Arc, Slot, Ellipse, Spline, Polygon, Point · add: Offset · remove: Trim · repeat: Mirror · transform: Move · reference: Dimension, Constrain, Construction · modify: Extend, Delete
Sketch pointWorking document134/82create: Line, Rectangle, Circle, Arc, Slot, Ellipse, Spline, Polygon, Point · transform: Move · reference: Dimension, Constrain · modify: Delete
Two sketch entitiesWorking document208/84create: Line, Rectangle, Circle, Arc, Slot, Ellipse, Spline, Polygon, Point · add: Offset · remove: Trim · dressup: Fillet, Chamfer · repeat: Mirror · transform: Move · reference: Dimension, Constrain, Construction · modify: Extend, Delete
Nothing selectedFresh document52/81create: Sketch · reference: Plane, Axis, Coord Sys, Helix
Planar faceFresh document74/81create: Sketch · add: Extrude · reference: Plane, Axis, Coord Sys, Measure · modify: Edit
Cylindrical faceFresh document42/81reference: Axis, Helix, Measure · modify: Edit
Curved faceFresh document22/80reference: Measure · modify: Edit
Straight edgeFresh document31/81reference: Plane, Axis, Measure
Circular edgeFresh document11/80reference: Measure
VertexFresh document41/81reference: Plane, Axis, Coord Sys, Measure
Solid bodyFresh document32/81reference: Measure · modify: Edit, Delete
Sheet bodyFresh document32/81reference: Measure · modify: Edit, Delete
Two bodiesFresh document22/80reference: Measure · modify: Delete
Datum planeFresh document53/82create: Sketch · reference: Plane, Helix · modify: Edit, Delete
Datum axisFresh document21/81modify: Edit, Delete
Coordinate systemFresh document21/81modify: Edit, Delete
Text / imported artFresh document21/81modify: Edit, Delete
Closed sketch loopFresh document72/82add: Extrude, Revolve, Surface Extrude, Surface Revolve, Surface Fill · modify: Edit, Delete
Sketch, nothing pickedFresh document112/82create: Line, Rectangle, Circle, Arc, Slot, Ellipse, Spline, Polygon, Point · reference: Dimension, Construction
Sketch lineFresh document208/84create: Line, Rectangle, Circle, Arc, Slot, Ellipse, Spline, Polygon, Point · add: Offset · remove: Trim · dressup: Fillet, Chamfer · repeat: Mirror · transform: Move · reference: Dimension, Constrain, Construction · modify: Extend, Delete
Sketch arc or circleFresh document187/83create: Line, Rectangle, Circle, Arc, Slot, Ellipse, Spline, Polygon, Point · add: Offset · remove: Trim · repeat: Mirror · transform: Move · reference: Dimension, Constrain, Construction · modify: Extend, Delete
Sketch pointFresh document134/82create: Line, Rectangle, Circle, Arc, Slot, Ellipse, Spline, Polygon, Point · transform: Move · reference: Dimension, Constrain · modify: Delete
Two sketch entitiesFresh document208/84create: Line, Rectangle, Circle, Arc, Slot, Ellipse, Spline, Polygon, Point · add: Offset · remove: Trim · dressup: Fillet, Chamfer · repeat: Mirror · transform: Move · reference: Dimension, Constrain, Construction · modify: Extend, Delete
- -

Overflow — the one place eight slots is not enough

-

Measured, not predicted: the create sub-ring needs 10 addresses, so Polygon, Point are pushed behind a “More” slot. This is the decision the ring size actually turns on — either a verb moves to another family, or the tail goes to a third level, or the ring is not eight. It affects sketch mode only; every model-mode family fits.

- -

Not in the offer

-

Import STEP, Import mesh, Text, SVG, Export STEP, Commit to Plate, Undo, Redo, Variables, Section view, Origin planes, World axes — these act on the document, not on a selection, - so they stay in chrome. Verbs with kernel support but no GUI today, which still hold an address: - Split, Pattern on Curve, Align to, Measure, Interference.

- -

All states

-
Nothing selected — Working document
Nothing selected — Working document
Nothing selected — Reference ring
Nothing selected — Reference ring
Planar face — Working document
Planar face — Working document
Planar face — Add material ring
Planar face — Add material ring
Planar face — Remove ring
Planar face — Remove ring
Planar face — Dress-up ring
Planar face — Dress-up ring
Planar face — Transform ring
Planar face — Transform ring
Planar face — Reference ring
Planar face — Reference ring
Planar face — Modify ring
Planar face — Modify ring
Cylindrical face — Working document
Cylindrical face — Working document
Cylindrical face — Reference ring
Cylindrical face — Reference ring
Cylindrical face — Modify ring
Cylindrical face — Modify ring
Curved face — Working document
Curved face — Working document
Curved face — Modify ring
Curved face — Modify ring
Straight edge — Working document
Straight edge — Working document
Straight edge — Dress-up ring
Straight edge — Dress-up ring
Straight edge — Reference ring
Straight edge — Reference ring
Circular edge — Working document
Circular edge — Working document
Circular edge — Dress-up ring
Circular edge — Dress-up ring
Vertex — Working document
Vertex — Working document
Vertex — Reference ring
Vertex — Reference ring
Solid body — Working document
Solid body — Working document
Solid body — Remove ring
Solid body — Remove ring
Solid body — Dress-up ring
Solid body — Dress-up ring
Solid body — Repeat ring
Solid body — Repeat ring
Solid body — Reference ring
Solid body — Reference ring
Solid body — Modify ring
Solid body — Modify ring
Sheet body — Working document
Sheet body — Working document
Sheet body — Modify ring
Sheet body — Modify ring
Two bodies — Working document
Two bodies — Working document
Two bodies — Reference ring
Two bodies — Reference ring
Datum plane — Working document
Datum plane — Working document
Datum plane — Remove ring
Datum plane — Remove ring
Datum plane — Reference ring
Datum plane — Reference ring
Datum plane — Modify ring
Datum plane — Modify ring
Datum axis — Working document
Datum axis — Working document
Datum axis — Modify ring
Datum axis — Modify ring
Coordinate system — Working document
Coordinate system — Working document
Coordinate system — Modify ring
Coordinate system — Modify ring
Text / imported art — Working document
Text / imported art — Working document
Text / imported art — Modify ring
Text / imported art — Modify ring
Closed sketch loop — Working document
Closed sketch loop — Working document
Closed sketch loop — Add material ring
Closed sketch loop — Add material ring
Closed sketch loop — Modify ring
Closed sketch loop — Modify ring
Sketch, nothing picked — Working document
Sketch, nothing picked — Working document
Sketch, nothing picked — Create ring
Sketch, nothing picked — Create ring
Sketch, nothing picked — Reference ring
Sketch, nothing picked — Reference ring
Sketch line — Working document
Sketch line — Working document
Sketch line — Create ring
Sketch line — Create ring
Sketch line — Dress-up ring
Sketch line — Dress-up ring
Sketch line — Reference ring
Sketch line — Reference ring
Sketch line — Modify ring
Sketch line — Modify ring
Sketch arc or circle — Working document
Sketch arc or circle — Working document
Sketch arc or circle — Create ring
Sketch arc or circle — Create ring
Sketch arc or circle — Reference ring
Sketch arc or circle — Reference ring
Sketch arc or circle — Modify ring
Sketch arc or circle — Modify ring
Sketch point — Working document
Sketch point — Working document
Sketch point — Create ring
Sketch point — Create ring
Sketch point — Reference ring
Sketch point — Reference ring
Two sketch entities — Working document
Two sketch entities — Working document
Two sketch entities — Create ring
Two sketch entities — Create ring
Two sketch entities — Dress-up ring
Two sketch entities — Dress-up ring
Two sketch entities — Reference ring
Two sketch entities — Reference ring
Two sketch entities — Modify ring
Two sketch entities — Modify ring
Nothing selected — Fresh document
Nothing selected — Fresh document
Nothing selected — Reference ring
Nothing selected — Reference ring
Planar face — Fresh document
Planar face — Fresh document
Planar face — Reference ring
Planar face — Reference ring
Cylindrical face — Fresh document
Cylindrical face — Fresh document
Cylindrical face — Reference ring
Cylindrical face — Reference ring
Curved face — Fresh document
Curved face — Fresh document
Straight edge — Fresh document
Straight edge — Fresh document
Straight edge — Reference ring
Straight edge — Reference ring
Circular edge — Fresh document
Circular edge — Fresh document
Vertex — Fresh document
Vertex — Fresh document
Vertex — Reference ring
Vertex — Reference ring
Solid body — Fresh document
Solid body — Fresh document
Solid body — Modify ring
Solid body — Modify ring
Sheet body — Fresh document
Sheet body — Fresh document
Sheet body — Modify ring
Sheet body — Modify ring
Two bodies — Fresh document
Two bodies — Fresh document
Datum plane — Fresh document
Datum plane — Fresh document
Datum plane — Reference ring
Datum plane — Reference ring
Datum plane — Modify ring
Datum plane — Modify ring
Datum axis — Fresh document
Datum axis — Fresh document
Datum axis — Modify ring
Datum axis — Modify ring
Coordinate system — Fresh document
Coordinate system — Fresh document
Coordinate system — Modify ring
Coordinate system — Modify ring
Text / imported art — Fresh document
Text / imported art — Fresh document
Text / imported art — Modify ring
Text / imported art — Modify ring
Closed sketch loop — Fresh document
Closed sketch loop — Fresh document
Closed sketch loop — Add material ring
Closed sketch loop — Add material ring
Closed sketch loop — Modify ring
Closed sketch loop — Modify ring
Sketch, nothing picked — Fresh document
Sketch, nothing picked — Fresh document
Sketch, nothing picked — Create ring
Sketch, nothing picked — Create ring
Sketch, nothing picked — Reference ring
Sketch, nothing picked — Reference ring
Sketch line — Fresh document
Sketch line — Fresh document
Sketch line — Create ring
Sketch line — Create ring
Sketch line — Dress-up ring
Sketch line — Dress-up ring
Sketch line — Reference ring
Sketch line — Reference ring
Sketch line — Modify ring
Sketch line — Modify ring
Sketch arc or circle — Fresh document
Sketch arc or circle — Fresh document
Sketch arc or circle — Create ring
Sketch arc or circle — Create ring
Sketch arc or circle — Reference ring
Sketch arc or circle — Reference ring
Sketch arc or circle — Modify ring
Sketch arc or circle — Modify ring
Sketch point — Fresh document
Sketch point — Fresh document
Sketch point — Create ring
Sketch point — Create ring
Sketch point — Reference ring
Sketch point — Reference ring
Two sketch entities — Fresh document
Two sketch entities — Fresh document
Two sketch entities — Create ring
Two sketch entities — Create ring
Two sketch entities — Dress-up ring
Two sketch entities — Dress-up ring
Two sketch entities — Reference ring
Two sketch entities — Reference ring
Two sketch entities — Modify ring
Two sketch entities — Modify ring
-
diff --git a/docs/CAD/ux/offer_atlas_inline.html b/docs/CAD/ux/offer_atlas_inline.html deleted file mode 100644 index 3bbfb1b303..0000000000 --- a/docs/CAD/ux/offer_atlas_inline.html +++ /dev/null @@ -1,77 +0,0 @@ -Design tab — offer atlas - -
-

The offer — atlas of every state

-

Every state of the object-driven tool offer, generated from - docs/ux/tool_atlas.json. The invariant under test: a verb has one address, that - address is the same in every selection where it appears, and slots that do not apply are drawn - empty rather than compacted.

-
-
60verbs mapped
-
20selection kinds
-
40primary rings
-
73secondary rings
-
113states total
-
3.5/8mean slots filled
-
5verbs with no GUI yet
-
-
-
-

Decision 0 — ring or vertical list

-

The live question. Both forms carry the SAME map and the same invariant — fixed order, never - re-sorted, nothing compacted; only the geometry differs. Left-click selects; right-click opens the - offer. What the list buys: a disabled row can state its own reason in the words the product - already ships, where an empty slot in a ring is mute; nine sketch primitives fit without an - overflow; shortcuts line up in a readable column; long translated names fit; and it is navigable - by arrow key and by screen reader, which a radial is not. What it costs: no equidistant flick - gesture, and travel to the last row is longer than to the nearest direction. Pairs below — - list first, the same state as a ring second.

- -

Decision 1 — ring capacity

-

The same three selections at eight and at twelve. Eight keeps 45° between neighbours, which is - the reliable eyes-free pointing threshold and maps 1:1 to the numpad; twelve buys direct addresses - for more verbs at 30° spacing and positions that stop being nameable.

-
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Planar face selected — pick what to do with itWorking document · 8 slotsSketchShift+SAdd material2Remove2Dress-up3PatternShift+NTransform2Reference5Modify2Planar face
8 slots — face_planar
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Straight edge selected — pick what to do with itWorking document · 8 slotsDress-up2Pattern on CurveReference3Straight edge
8 slots — edge_str
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Sketch line selected — pick what to do with itWorking document · 8 slotsCreate9OffsetOTrimTDress-up2MirrorMMoveReference3Modify2Sketch line
8 slots — sk_line
-
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Planar face selected — pick what to do with itWorking document · 12 slotsSketchShift+SAdd material2Remove2Dress-up3PatternShift+NTransform2Reference5Modify2Planar face
12 slots — face_planar
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Straight edge selected — pick what to do with itWorking document · 12 slotsDress-up2Pattern on CurveReference3Straight edge
12 slots — edge_str
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Sketch line selected — pick what to do with itWorking document · 12 slotsCreate9OffsetOTrimTDress-up2MirrorMMoveReference3Modify2Sketch line
12 slots — sk_line
- -

Decision 2 — one map or one per mode

-

These renders use ONE shared map: sketch verbs occupy the same eight families as model verbs, - so dress-up is south-east whether you picked a solid edge or a sketch line. Compare the sketch-mode - states below against the model-mode ones — if a verb that exists in both modes ever appears at two - addresses, the shared map has failed and the split map is the answer.

- -

The matrix — every selection, every document state

-
- - -
SelectionDocumentVerbsSlots2nd ringsWhat lands where
Nothing selectedWorking document52/81create: Sketch · reference: Plane, Axis, Coord Sys, Helix
Planar faceWorking document188/86create: Sketch · add: Extrude, Thicken · remove: Hole, Shell · dressup: Fillet, Chamfer, Draft · repeat: Pattern · transform: Mate, Align to · reference: Plane, Axis, Coord Sys, Project, Measure · modify: Edit, Delete Face
Cylindrical faceWorking document63/82remove: Thread · reference: Axis, Helix, Measure · modify: Edit, Delete Face
Curved faceWorking document54/81add: Thicken · dressup: Draft · reference: Measure · modify: Edit, Delete Face
Straight edgeWorking document63/82dressup: Fillet, Chamfer · repeat: Pattern on Curve · reference: Plane, Axis, Measure
Circular edgeWorking document43/81remove: Thread · dressup: Fillet, Chamfer · reference: Measure
VertexWorking document41/81reference: Plane, Axis, Coord Sys, Measure
Solid bodyWorking document156/85remove: Shell, Cut, Split · dressup: Fillet, Chamfer · repeat: Pattern, Mirror, Pattern on Curve · transform: Move · reference: Project, Measure, Mass · modify: Edit, Colour, Delete
Sheet bodyWorking document75/81add: Thicken Surface · dressup: Surface Offset · transform: Move · reference: Measure · modify: Edit, Colour, Delete
Two bodiesWorking document54/81add: Combine · transform: Mate · reference: Measure, Interference · modify: Delete
Datum planeWorking document95/83create: Sketch · remove: Hole, Cut · repeat: Mirror · reference: Plane, Helix, Project · modify: Edit, Delete
Datum axisWorking document21/81modify: Edit, Delete
Coordinate systemWorking document32/81transform: Mate · modify: Edit, Delete
Text / imported artWorking document43/81repeat: Pattern · transform: Move · modify: Edit, Delete
Closed sketch loopWorking document113/82add: Extrude, Revolve, Sweep, Loft, Surface Extrude, Surface Revolve, Surface Loft, Surface Fill · repeat: Pattern · modify: Edit, Delete
Sketch, nothing pickedWorking document112/82create: Line, Rectangle, Circle, Arc, Slot, Ellipse, Spline, Polygon, Point · reference: Dimension, Construction
Sketch lineWorking document208/84create: Line, Rectangle, Circle, Arc, Slot, Ellipse, Spline, Polygon, Point · add: Offset · remove: Trim · dressup: Fillet, Chamfer · repeat: Mirror · transform: Move · reference: Dimension, Constrain, Construction · modify: Extend, Delete
Sketch arc or circleWorking document187/83create: Line, Rectangle, Circle, Arc, Slot, Ellipse, Spline, Polygon, Point · add: Offset · remove: Trim · repeat: Mirror · transform: Move · reference: Dimension, Constrain, Construction · modify: Extend, Delete
Sketch pointWorking document134/82create: Line, Rectangle, Circle, Arc, Slot, Ellipse, Spline, Polygon, Point · transform: Move · reference: Dimension, Constrain · modify: Delete
Two sketch entitiesWorking document208/84create: Line, Rectangle, Circle, Arc, Slot, Ellipse, Spline, Polygon, Point · add: Offset · remove: Trim · dressup: Fillet, Chamfer · repeat: Mirror · transform: Move · reference: Dimension, Constrain, Construction · modify: Extend, Delete
Nothing selectedFresh document52/81create: Sketch · reference: Plane, Axis, Coord Sys, Helix
Planar faceFresh document74/81create: Sketch · add: Extrude · reference: Plane, Axis, Coord Sys, Measure · modify: Edit
Cylindrical faceFresh document42/81reference: Axis, Helix, Measure · modify: Edit
Curved faceFresh document22/80reference: Measure · modify: Edit
Straight edgeFresh document31/81reference: Plane, Axis, Measure
Circular edgeFresh document11/80reference: Measure
VertexFresh document41/81reference: Plane, Axis, Coord Sys, Measure
Solid bodyFresh document32/81reference: Measure · modify: Edit, Delete
Sheet bodyFresh document32/81reference: Measure · modify: Edit, Delete
Two bodiesFresh document22/80reference: Measure · modify: Delete
Datum planeFresh document53/82create: Sketch · reference: Plane, Helix · modify: Edit, Delete
Datum axisFresh document21/81modify: Edit, Delete
Coordinate systemFresh document21/81modify: Edit, Delete
Text / imported artFresh document21/81modify: Edit, Delete
Closed sketch loopFresh document72/82add: Extrude, Revolve, Surface Extrude, Surface Revolve, Surface Fill · modify: Edit, Delete
Sketch, nothing pickedFresh document112/82create: Line, Rectangle, Circle, Arc, Slot, Ellipse, Spline, Polygon, Point · reference: Dimension, Construction
Sketch lineFresh document208/84create: Line, Rectangle, Circle, Arc, Slot, Ellipse, Spline, Polygon, Point · add: Offset · remove: Trim · dressup: Fillet, Chamfer · repeat: Mirror · transform: Move · reference: Dimension, Constrain, Construction · modify: Extend, Delete
Sketch arc or circleFresh document187/83create: Line, Rectangle, Circle, Arc, Slot, Ellipse, Spline, Polygon, Point · add: Offset · remove: Trim · repeat: Mirror · transform: Move · reference: Dimension, Constrain, Construction · modify: Extend, Delete
Sketch pointFresh document134/82create: Line, Rectangle, Circle, Arc, Slot, Ellipse, Spline, Polygon, Point · transform: Move · reference: Dimension, Constrain · modify: Delete
Two sketch entitiesFresh document208/84create: Line, Rectangle, Circle, Arc, Slot, Ellipse, Spline, Polygon, Point · add: Offset · remove: Trim · dressup: Fillet, Chamfer · repeat: Mirror · transform: Move · reference: Dimension, Constrain, Construction · modify: Extend, Delete
- -

Overflow — the one place eight slots is not enough

-

Measured, not predicted: the create sub-ring needs 10 addresses, so Polygon, Point are pushed behind a “More” slot. This is the decision the ring size actually turns on — either a verb moves to another family, or the tail goes to a third level, or the ring is not eight. It affects sketch mode only; every model-mode family fits.

- -

Not in the offer

-

Import STEP, Import mesh, Text, SVG, Export STEP, Commit to Plate, Undo, Redo, Variables, Section view, Origin planes, World axes — these act on the document, not on a selection, - so they stay in chrome. Verbs with kernel support but no GUI today, which still hold an address: - Split, Pattern on Curve, Align to, Measure, Interference.

- -

The states

-
PreparePreviewDesignFEATURESnothing yetClick a face or a reference plane, then a toolFresh document · 8 slotsSketchShift+SReference4Nothing selected
Fresh document, nothing selected — the first-run picture
PreparePreviewDesignFEATURESnothing yetRight-click the geometry to see what you can do with itFresh document · vertical listNOTHING SELECTEDSketchShift+SAdd materialCreate a sketch, or pick a solid face, firstRemovePick a face or a plane to drill intoDress-upPick an edge to roundRepeatCreate a solid body to pattern firstTransformTransform needs a body — add or import one firstReference4ModifyDelete Face needs a body — add or import one first
LIST · fresh document — every family present, the unavailable ones say why
PreparePreviewDesignFEATURESnothing yetClick a face or a reference plane, then a toolFresh document · 8 slotsSketchShift+SReference4Nothing selected
RING · the same state — an empty slot cannot say anything
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Right-click the geometry to see what you can do with itWorking document · vertical listPLANAR FACESketchShift+SAdd material2Remove2Dress-up3PatternShift+NTransform2Reference5Modify2
LIST · planar face
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Planar face selected — pick what to do with itWorking document · 8 slotsSketchShift+SAdd material2Remove2Dress-up3PatternShift+NTransform2Reference5Modify2Planar face
RING · planar face
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Sketch, nothing picked — pick oneWorking document · vertical listSKETCH, NOTHING PICKEDCreate9Add materialRemoveDress-upRepeatTransformReference2ModifyLineLRectangleRCircleCArcASlotSEllipseESplineBPolygonGPointP
LIST · sketch Create submenu — all 9 primitives fit, no overflow
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Create — pick oneWorking document · 8 slotsLineLRectangleRCircleCArcASlotSEllipseESplineBMore2Sketch, nothing picked · Create
RING · the same submenu — 2 verbs pushed behind “More”
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Planar face — pick oneWorking document · vertical listPLANAR FACESketchShift+SAdd material2Remove2Dress-up3PatternShift+NTransform2Reference5Modify2ExtrudeShift+EThicken
LIST · planar face, Add material submenu
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Add material — pick oneWorking document · 8 slotsExtrudeShift+EThickenPlanar face · Add material
RING · planar face, Add material sub-ring
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Right-click the geometry to see what you can do with itWorking document · vertical listSOLID BODYCreateClick a face or a reference plane in the viewport, t…Add materialCreate a sketch, or pick a solid face, firstRemove3Dress-up2Repeat3MoveShift+YReference3Modify3
LIST · solid body
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Right-click the geometry to see what you can do with itWorking document · vertical listSTRAIGHT EDGECreateClick a face or a reference plane in the viewport, t…Add materialCreate a sketch, or pick a solid face, firstRemovePick a face or a plane to drill intoDress-up2Pattern on CurveTransformTransform needs a body — add or import one firstReference3ModifyDelete Face needs a body — add or import one first
LIST · straight edge
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Right-click the geometry to see what you can do with itWorking document · vertical listSKETCH LINECreate9OffsetOTrimTDress-up2MirrorMMoveReference3Modify2
LIST · sketch line
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Click a face or a reference plane, then a toolWorking document · 8 slotsSketchShift+SReference4Nothing selected
RING · Nothing selected
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Planar face selected — pick what to do with itWorking document · 8 slotsSketchShift+SAdd material2Remove2Dress-up3PatternShift+NTransform2Reference5Modify2Planar face
RING · Planar face
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Cylindrical face selected — pick what to do with itWorking document · 8 slotsThreadShift+TReference3Modify2Cylindrical face
RING · Cylindrical face
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Curved face selected — pick what to do with itWorking document · 8 slotsThickenDraftShift+DMeasureModify2Curved face
RING · Curved face
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Straight edge selected — pick what to do with itWorking document · 8 slotsDress-up2Pattern on CurveReference3Straight edge
RING · Straight edge
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Circular edge selected — pick what to do with itWorking document · 8 slotsThreadShift+TDress-up2MeasureCircular edge
RING · Circular edge
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Vertex selected — pick what to do with itWorking document · 8 slotsReference4Vertex
RING · Vertex
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Solid body selected — pick what to do with itWorking document · 8 slotsRemove3Dress-up2Repeat3MoveShift+YReference3Modify3Solid body
RING · Solid body
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Sheet body selected — pick what to do with itWorking document · 8 slotsThicken SurfaceSurface OffsetMoveShift+YMeasureModify3Sheet body
RING · Sheet body
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Two bodies selected — pick what to do with itWorking document · 8 slotsCombineShift+BMateReference2DeleteDelTwo bodies
RING · Two bodies
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Datum plane selected — pick what to do with itWorking document · 8 slotsSketchShift+SRemove2MirrorShift+ZReference3Modify2Datum plane
RING · Datum plane
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Datum axis selected — pick what to do with itWorking document · 8 slotsModify2Datum axis
RING · Datum axis
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Coordinate system selected — pick what to do with itWorking document · 8 slotsMateModify2Coordinate system
RING · Coordinate system
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Text / imported art selected — pick what to do with itWorking document · 8 slotsABCPatternShift+NMoveShift+YModify2Text / imported art
RING · Text / imported art
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Closed sketch loop selected — pick what to do with itWorking document · 8 slotsAdd material8PatternShift+NModify2Closed sketch loop
RING · Closed sketch loop
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Sketch, nothing picked selected — pick what to do with itWorking document · 8 slotsCreate9Reference2Sketch, nothing picked
RING · Sketch, nothing picked
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Sketch line selected — pick what to do with itWorking document · 8 slotsCreate9OffsetOTrimTDress-up2MirrorMMoveReference3Modify2Sketch line
RING · Sketch line
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Sketch arc or circle selected — pick what to do with itWorking document · 8 slotsCreate9OffsetOTrimTMirrorMMoveReference3Modify2Sketch arc or circle
RING · Sketch arc or circle
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Sketch point selected — pick what to do with itWorking document · 8 slotsCreate9MoveReference2DeleteDelSketch point
RING · Sketch point
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Two sketch entities selected — pick what to do with itWorking document · 8 slotsCreate9OffsetOTrimTDress-up2MirrorMMoveReference3Modify2Two sketch entities
RING · Two sketch entities
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Add material — pick oneWorking document · 8 slotsExtrudeShift+EThickenPlanar face · Add material
Planar face · Add material sub-ring
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Reference — pick oneWorking document · 8 slotsCoord SysShift+CProjectMeasurePlaneShift+PAxisShift+APlanar face · Reference
Planar face · Reference sub-ring
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Create — pick oneWorking document · 8 slotsLineLRectangleRCircleCArcASlotSEllipseESplineBMore2Sketch, nothing picked · Create
Sketch · Create sub-ring (the overflow case)
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Remove — pick oneWorking document · 8 slotsShellShift+KCutShift+XSplitSolid body · Remove
Solid body · Remove sub-ring
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Planar face selected — pick what to do with itWorking document · 8 slotsSketchShift+SAdd material2Remove2Dress-up3PatternShift+NTransform2Reference5Modify2Planar face
8 slots — planar face
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Planar face selected — pick what to do with itWorking document · 12 slotsSketchShift+SAdd material2Remove2Dress-up3PatternShift+NTransform2Reference5Modify2Planar face
12 slots — planar face
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Sketch line selected — pick what to do with itWorking document · 8 slotsCreate9OffsetOTrimTDress-up2MirrorMMoveReference3Modify2Sketch line
8 slots — sketch line
PreparePreviewDesignFEATURESSketch1Extrude1Hole1Fillet1Sketch2Extrude2Sketch line selected — pick what to do with itWorking document · 12 slotsCreate9OffsetOTrimTDress-up2MirrorMMoveReference3Modify2Sketch line
12 slots — sketch line
diff --git a/docs/HLSD/design-tab.md b/docs/HLSD/design-tab.md new file mode 100644 index 0000000000..67589be328 --- /dev/null +++ b/docs/HLSD/design-tab.md @@ -0,0 +1,213 @@ +# Design tab — High Level Design + +## Purpose and scope + +The Design tab is a parametric CAD environment inside the slicer: sketch, constrain, build +solid features, commit the result to the plate. It exists because the alternative is a round +trip through an external CAD application, and that round trip discards design intent at both +ends — a part edited after slicing comes back as a mesh rather than as the feature history +that produced it. Keeping the model in the project means a dimension can be changed after the +part has been sliced, with the nozzle diameter, the build volume and the material already +known. + +Its coupling to the rest of the application is deliberately narrow. It adds no stage to the +slicing pipeline and touches neither the preset system nor `Tab`. It reaches the rest of Orca +in two places: **Commit to Plate**, which hands finished solids to Prepare as ordinary model +objects, and one optional 3MF archive entry that carries the recipe. Everything else is +contained in `src/libslic3r/CAD/` and `src/slic3r/GUI/CAD/`. + +The user-facing manual lives in the wiki +([Design Tab](https://www.orcaslicer.com/wiki/design_tab)), not here. This document covers the +parts of the design that the code does not make evident. + +## The model is a recipe + +`CadDocument` holds an ordered list of `CadFeature` and nothing else that matters. Bodies, +meshes and display geometry are **derived**: `recompute()` replays the feature list from the +start and rebuilds them. Editing a dimension set twenty features ago is therefore an ordinary +edit — everything downstream is rebuilt by the same replay that built it the first time. + +Two consequences follow from deriving rather than storing: + +- **Undo is a snapshot of `features` alone.** The caller calls `checkpoint()` before the + mutations that make up one user action; undo restores that snapshot and recomputes. Because + everything else is derived, one checkpoint is exactly one `Ctrl+Z` step and the restored + state is exact rather than approximately reconstructed. The tab keeps this stack itself; it + is not Orca's project snapshot system, which operates on `Model` objects the Design tab does + not own until Commit. +- **Face and edge ids are session-scoped.** They are indices into `TopExp::MapShapes`, so a + rebuild invalidates every one of them. `CadDocument::topo_generation` is bumped on every + rebuild so a holder of an id can discover that it is stale instead of silently addressing a + different edge. The counter is deliberately not serialized: an id means nothing outside the + run that produced it. + +## Geometry kernel and dependency surface + +The kernel is OCCT, which OrcaSlicer **already** links — `Format/STEP.cpp`, `Format/svg.cpp` +and `Shape/TextShape.cpp` use it upstream. The Design tab adds no third-party dependency; it +widens the existing OCCT build by one module flag in `deps/OCCT/OCCT.cmake`: + +```cmake +-DBUILD_MODULE_ModelingAlgorithms=${SLIC3R_CAD} +``` + +Most of that module's twelve toolkits were already being built, because `DataExchange` — the +STEP path upstream ships — depends on them. The delta is `TKFillet` (used through +`BRepFilletAPI`), `TKOffset` (`BRepOffsetAPI`) and `TKFeat`, which nothing here references but +which the module flag builds anyway, because OCCT's module flags are all-or-nothing. On macOS +and Linux OCCT links statically, so an unreferenced toolkit costs build time and no shipped +bytes; on Windows OCCT builds shared, so the cost there is real DLL bytes. That Windows figure +has not been measured, and `OCCT.cmake` says so rather than carrying a number that was derived +from an incomplete toolkit list. + +On Windows the packaging step asserts that every linked OCCT toolkit has a shipped DLL and +fails the configure with the name of any that is missing, because the alternative failure — a +deps prefix built with a different `SLIC3R_CAD` setting than the app — otherwise surfaces as a +missing DLL at first launch. + +## The sketch constraint solver + +`src/libslic3r/slvs/` is a vendored subset of SolveSpace's `libslvs`: self-contained, no +external dependencies, **GPL-3.0**, with its `LICENSE` preserved verbatim in the directory. +`SketchSolver.cpp` is its only consumer and drives every sketch constraint in the tab. + +OrcaSlicer is AGPL-3.0. GPLv3 §13 permits combining a GPLv3 work with an AGPLv3 work and +AGPLv3 §13 grants the converse, so the combined work is distributable under AGPL-3.0 with the +solver's GPLv3 terms preserved. The solver is vendored rather than fetched as a dependency +because it is a pinned subset with no build system of its own; the cost of that choice is +upstream-sync burden, paid deliberately to keep `deps/` unchanged. + +## The SLIC3R_CAD gate + +`SLIC3R_CAD` (default ON) compiles the tab and selects the OCCT module flag above. With it OFF +the tab is not built and the deps prefix matches upstream exactly. The gate is cheap because +the hooks the Design tab adds to shared GUI code — chiefly the `m_design_sketch_tool` member +and the render, mouse and key hooks in `GLCanvas3D` — are null-guarded on the path they extend, +so removing the tab removes behaviour rather than requiring the host code to be rewritten. + +The flag has to agree between the dependencies and the application; that is what the DLL +assertion above is checking. + +## Project persistence + +A project stores the recipe as one optional archive entry, `Metadata/orca_cad.bin`, backed by +a single `std::string cad_recipe` on `Model`. The entry is written only when the string is +non-empty, and readers that do not know it ignore it, so projects that contain no CAD model are +byte-identical to what upstream would have written and older readers are unaffected. + +The blob is a cereal binary archive whose layout is the field order of `CadFeature`'s +save/load. That makes the format the one irreversible decision in the subsystem, and the rules +that keep it survivable are: + +- **Append only, never reorder.** Enums serialize positionally as their underlying integer, so + inserting a value in the middle of `SketchConstraintType` or `CadFeatureType` reinterprets + every constraint in every saved project. New fields go at the end. +- **Features are length-framed.** Since v5 each feature is a length-prefixed, self-contained + cereal stream, so a reader can skip a feature written by a newer build and stop cleanly on an + older one. This is what makes appending a field a non-breaking change from here on. v4 and + earlier still open through the pre-framing flat path; v1 is deliberately not loadable and has + no migration path. +- **A newer stamp is refused, not guessed at.** `deserialize_recipe` rejects a blob whose + version exceeds `ORCA_CAD_RECIPE_VERSION` with a message naming both versions. +- **The rules are held by fixtures, not by discipline.** `tests/data/cad_recipe_v{3,4,5}.bin` + are checked-in blobs from the builds that wrote them, and the tests that load them fail if a + field is reordered — which the in-memory round-trip test cannot detect. A regeneration test + (`[.regen]`, not run by default) produces a fresh fixture when a new version is stamped. + +`Import` features embed the imported solid as an OCCT BRep string inside the recipe rather than +referencing the source file, so a project opens without the STEP or mesh it was built from. +The cost is that saved projects are coupled to an OCCT BRep revision. + +## The interaction contract + +Three inputs carry the whole modelling loop — left click, right click and `Esc` — and the +contract between them is stated in code rather than spread across handlers. + +`DesignInteraction.hpp` defines a four-level LIFO stack whose enum value *is* the depth, so +"which level does this press belong to" is a comparison: + +| Level | Holds | One `Esc` press | +| --- | --- | --- | +| `Transient` | a value field or a popup menu | closes it; the tool stays armed | +| `Gesture` | an uncommitted delta — an entity being drawn, a body being dragged | reverts it; committed work is untouched | +| `Tool` | a feature card, an armed sketch tool, a constrain session | exits it; drawn entities survive | +| `Idle` | nothing transient | clears the selection; leaves a sketch session only if it is empty | + +`cad_escape_level()` is a `constexpr` free function over a POD of four booleans rather than a +method on the panel, so the ordering that is the entire contract is checkable without a window, +a GL context or an event loop — five `static_assert`s in the header do exactly that at compile +time. + +**The strict invariant: no level of `Esc` deletes a feature, discards a sketch that holds +geometry, or rolls history back.** Destroying work needs a gesture that says so — `Del` on an +explicit selection, the sketch ribbon's Cancel, which asks first, or `Ctrl+Z`. A sketch +*session* is deliberately not a `Tool` level; it is the environment the `Idle` level lives in, +which makes the destructive path unrepresentable rather than merely unlikely. + +Right-click is read at button-up against two independent budgets — 3 px of drift and 200 ms — +because drift alone still popped a menu at the end of a slow, careful orbit. The raycast uses +the press position, not the release. An armed sketch tool that already consumed the right +button (to terminate a chain, say) declines to also open a menu, through a read-and-clear flag. +Past either budget the event is navigation, and navigation does not transition the state +machine. + +Entering a sketch changes three things at once so the mode is legible: a banner above the +canvas (a sibling of the canvas, not a child over it — on GTK a child window over a +`wxGLCanvas` is a native window and does not reliably stack over GL), the printer bed muted so +a plate grid is never read as a sketch grid, and `N` to look normal to the plane. Code that +changes any of the three belongs with a change to this section. + +## The offer is generated, not hand-written + +Right-clicking geometry opens the *offer*: eight families in a fixed order, each verb at a +permanent row index, verbs that do not apply shown disabled **in place with their reason** +rather than removed. The invariant is that a verb's row index is identical in every selection +where it appears and that adding a verb never moves an existing one — the hand learns the +position, so the menu is never re-sorted, compacted or adaptively ordered. + +An invariant across 92 verbs and 20 selection kinds does not survive by review, so the map +exists once, as data: `scripts/CAD/tool_atlas.json` carries every verb with its row, key, icon, +accepted selections, preconditions and refusal string, and `scripts/CAD/gen_offer_table.py` +emits `src/slic3r/GUI/CAD/DesignOffer.hpp` from it. The header is checked in and never +hand-edited; `scripts/CAD/run-all-checks.sh` runs the generator with `--check` as its first +rung, which is what makes "GENERATED — DO NOT EDIT" a fact rather than a request. The generator +also refuses an atlas with a duplicate verb id, since `mcp_run_verb` resolves a verb by id and +would make the second one unreachable. + +The atlas and its generator sit in `scripts/CAD/` rather than in `docs/`: they are build inputs +for a checked-in header, not documentation. + +## Automation surface + +`McpControl` exposes the document over JSON-RPC when `ORCA_CAD_MCP` is set in the environment, +with `tools/orca_cad_mcp_bridge.py` as the client side. It describes the scene, queries +topology, measures, and runs the same verbs the offer does — it re-implements nothing, so a +scripted action and a clicked one cannot diverge. It is off unless the variable is set. + +## Where the code lives + +| Path | Role | +| --- | --- | +| `src/libslic3r/CAD/CadDocument.*` | the feature recipe, its replay, undo and serialization | +| `src/libslic3r/CAD/GeometryEngine.*` | OCCT wrapper — faces, edges, booleans, healing | +| `src/libslic3r/CAD/SketchEngine.*` | profile → wire → solid | +| `src/libslic3r/CAD/SketchSolver.*` | constraint solving, over the vendored solver | +| `src/libslic3r/slvs/` | vendored 2D constraint solver (GPLv3) | +| `src/slic3r/GUI/CAD/DesignPanel.*` | the tab: toolbar, feature cards, tree, key maps | +| `src/slic3r/GUI/CAD/DesignCanvas.*` | viewport integration | +| `src/slic3r/GUI/CAD/DesignSketchTool.*` | in-canvas sketching | +| `src/slic3r/GUI/CAD/DesignInteraction.hpp` | the Esc level contract | +| `src/slic3r/GUI/CAD/DesignOffer.hpp` | generated offer table | +| `scripts/CAD/tool_atlas.json` | source of truth for the offer | + +## Verification + +The kernel is covered by Catch2 suites in `tests/libslic3r/` (`test_caddocument`, +`test_sketchconstraints`, `test_sketchedit`, `test_sketchimport`, `test_sketchinference`, +`test_sketchprofile`, `test_slvs_constraints`), which need no display; +`scripts/CAD/run-kernel-tests.sh` builds only `libslic3r_tests` and runs them headless. + +The GUI half is not covered by CI, which has no OpenGL canvas or synthetic input: the ladders +in `scripts/CAD/` drive a running application in a local rig instead, and +`scripts/CAD/run-all-checks.sh` is the gate that runs all of them. A green kernel run says +nothing about the viewport, so the two are reported separately rather than as one number. diff --git a/resources/images/texture_displacement_add.svg b/resources/images/texture_displacement_add.svg new file mode 100644 index 0000000000..bb12b2e953 --- /dev/null +++ b/resources/images/texture_displacement_add.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_add_negative.svg b/resources/images/texture_displacement_add_negative.svg new file mode 100644 index 0000000000..a3fc829863 --- /dev/null +++ b/resources/images/texture_displacement_add_negative.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_adjust.svg b/resources/images/texture_displacement_adjust.svg new file mode 100644 index 0000000000..f3632b3f19 --- /dev/null +++ b/resources/images/texture_displacement_adjust.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_avg_scale.svg b/resources/images/texture_displacement_avg_scale.svg new file mode 100644 index 0000000000..c94a62709c --- /dev/null +++ b/resources/images/texture_displacement_avg_scale.svg @@ -0,0 +1,330 @@ + + diff --git a/resources/images/texture_displacement_brush.svg b/resources/images/texture_displacement_brush.svg new file mode 100644 index 0000000000..cc3cd3740e --- /dev/null +++ b/resources/images/texture_displacement_brush.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_checker.svg b/resources/images/texture_displacement_checker.svg new file mode 100644 index 0000000000..833a76288e --- /dev/null +++ b/resources/images/texture_displacement_checker.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_connected_area.svg b/resources/images/texture_displacement_connected_area.svg new file mode 100644 index 0000000000..8c066f7be0 --- /dev/null +++ b/resources/images/texture_displacement_connected_area.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_cross.svg b/resources/images/texture_displacement_cross.svg new file mode 100644 index 0000000000..d7d2cfeace --- /dev/null +++ b/resources/images/texture_displacement_cross.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_cut.svg b/resources/images/texture_displacement_cut.svg new file mode 100644 index 0000000000..532793eb25 --- /dev/null +++ b/resources/images/texture_displacement_cut.svg @@ -0,0 +1,359 @@ + + diff --git a/resources/images/texture_displacement_distortion.svg b/resources/images/texture_displacement_distortion.svg new file mode 100644 index 0000000000..441baf0964 --- /dev/null +++ b/resources/images/texture_displacement_distortion.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_drag.svg b/resources/images/texture_displacement_drag.svg new file mode 100644 index 0000000000..75a3efe419 --- /dev/null +++ b/resources/images/texture_displacement_drag.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_erase_all.svg b/resources/images/texture_displacement_erase_all.svg new file mode 100644 index 0000000000..d45e6c7d35 --- /dev/null +++ b/resources/images/texture_displacement_erase_all.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_face.svg b/resources/images/texture_displacement_face.svg new file mode 100644 index 0000000000..a0c5d0a4b7 --- /dev/null +++ b/resources/images/texture_displacement_face.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_fast_preview.svg b/resources/images/texture_displacement_fast_preview.svg new file mode 100644 index 0000000000..84286f0c7f --- /dev/null +++ b/resources/images/texture_displacement_fast_preview.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_frame.svg b/resources/images/texture_displacement_frame.svg new file mode 100644 index 0000000000..3bf29bcc5a --- /dev/null +++ b/resources/images/texture_displacement_frame.svg @@ -0,0 +1,242 @@ + + diff --git a/resources/images/texture_displacement_join.svg b/resources/images/texture_displacement_join.svg new file mode 100644 index 0000000000..ff5e9a907f --- /dev/null +++ b/resources/images/texture_displacement_join.svg @@ -0,0 +1,387 @@ + + diff --git a/resources/images/texture_displacement_map_unwrap.svg b/resources/images/texture_displacement_map_unwrap.svg new file mode 100644 index 0000000000..a1af920f2b --- /dev/null +++ b/resources/images/texture_displacement_map_unwrap.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_map_view.svg b/resources/images/texture_displacement_map_view.svg new file mode 100644 index 0000000000..70ef64c198 --- /dev/null +++ b/resources/images/texture_displacement_map_view.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_move_down.svg b/resources/images/texture_displacement_move_down.svg new file mode 100644 index 0000000000..8f15b7c181 --- /dev/null +++ b/resources/images/texture_displacement_move_down.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_move_up.svg b/resources/images/texture_displacement_move_up.svg new file mode 100644 index 0000000000..32dfd19011 --- /dev/null +++ b/resources/images/texture_displacement_move_up.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_real_preview.svg b/resources/images/texture_displacement_real_preview.svg new file mode 100644 index 0000000000..0d132e6950 --- /dev/null +++ b/resources/images/texture_displacement_real_preview.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_select_all.svg b/resources/images/texture_displacement_select_all.svg new file mode 100644 index 0000000000..bf420df6c6 --- /dev/null +++ b/resources/images/texture_displacement_select_all.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_snap.svg b/resources/images/texture_displacement_snap.svg new file mode 100644 index 0000000000..f6abea8a3f --- /dev/null +++ b/resources/images/texture_displacement_snap.svg @@ -0,0 +1,260 @@ + + diff --git a/resources/images/texture_displacement_tile_repeat.svg b/resources/images/texture_displacement_tile_repeat.svg new file mode 100644 index 0000000000..9a4fc8d3c8 --- /dev/null +++ b/resources/images/texture_displacement_tile_repeat.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_unjoin.svg b/resources/images/texture_displacement_unjoin.svg new file mode 100644 index 0000000000..2f50d35767 --- /dev/null +++ b/resources/images/texture_displacement_unjoin.svg @@ -0,0 +1,387 @@ + + diff --git a/resources/images/texture_displacement_uv_avg_scale.svg b/resources/images/texture_displacement_uv_avg_scale.svg new file mode 100644 index 0000000000..4734381399 --- /dev/null +++ b/resources/images/texture_displacement_uv_avg_scale.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_uv_bg_height.svg b/resources/images/texture_displacement_uv_bg_height.svg new file mode 100644 index 0000000000..354f12d9f4 --- /dev/null +++ b/resources/images/texture_displacement_uv_bg_height.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_uv_clear_edits.svg b/resources/images/texture_displacement_uv_clear_edits.svg new file mode 100644 index 0000000000..3d1c6c0e87 --- /dev/null +++ b/resources/images/texture_displacement_uv_clear_edits.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_uv_cut.svg b/resources/images/texture_displacement_uv_cut.svg new file mode 100644 index 0000000000..9ae7d5e273 --- /dev/null +++ b/resources/images/texture_displacement_uv_cut.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_uv_frame.svg b/resources/images/texture_displacement_uv_frame.svg new file mode 100644 index 0000000000..c74e648fe0 --- /dev/null +++ b/resources/images/texture_displacement_uv_frame.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_uv_join.svg b/resources/images/texture_displacement_uv_join.svg new file mode 100644 index 0000000000..99cc51dbc0 --- /dev/null +++ b/resources/images/texture_displacement_uv_join.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_uv_path.svg b/resources/images/texture_displacement_uv_path.svg new file mode 100644 index 0000000000..2bdf164197 --- /dev/null +++ b/resources/images/texture_displacement_uv_path.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_uv_seam.svg b/resources/images/texture_displacement_uv_seam.svg new file mode 100644 index 0000000000..755fa231fa --- /dev/null +++ b/resources/images/texture_displacement_uv_seam.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_uv_select_edge.svg b/resources/images/texture_displacement_uv_select_edge.svg new file mode 100644 index 0000000000..d309addbd5 --- /dev/null +++ b/resources/images/texture_displacement_uv_select_edge.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_uv_select_island.svg b/resources/images/texture_displacement_uv_select_island.svg new file mode 100644 index 0000000000..430d187965 --- /dev/null +++ b/resources/images/texture_displacement_uv_select_island.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_uv_select_vertex.svg b/resources/images/texture_displacement_uv_select_vertex.svg new file mode 100644 index 0000000000..f9d5318189 --- /dev/null +++ b/resources/images/texture_displacement_uv_select_vertex.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_uv_snap.svg b/resources/images/texture_displacement_uv_snap.svg new file mode 100644 index 0000000000..b3beeaaf5a --- /dev/null +++ b/resources/images/texture_displacement_uv_snap.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_uv_unjoin.svg b/resources/images/texture_displacement_uv_unjoin.svg new file mode 100644 index 0000000000..bde79aec0a --- /dev/null +++ b/resources/images/texture_displacement_uv_unjoin.svg @@ -0,0 +1 @@ + diff --git a/resources/images/texture_displacement_wireframe.svg b/resources/images/texture_displacement_wireframe.svg new file mode 100644 index 0000000000..44d1c0c3e0 --- /dev/null +++ b/resources/images/texture_displacement_wireframe.svg @@ -0,0 +1 @@ + diff --git a/resources/images/toolbar_big_brush.svg b/resources/images/toolbar_big_brush.svg new file mode 100644 index 0000000000..20be9ec53b --- /dev/null +++ b/resources/images/toolbar_big_brush.svg @@ -0,0 +1,60 @@ + + + + + + + + + + diff --git a/resources/images/toolbar_face.svg b/resources/images/toolbar_face.svg new file mode 100644 index 0000000000..6975e0f6e4 --- /dev/null +++ b/resources/images/toolbar_face.svg @@ -0,0 +1,97 @@ + + + + + + + + + + + + + diff --git a/resources/images/toolbar_texture_displacement.svg b/resources/images/toolbar_texture_displacement.svg new file mode 100644 index 0000000000..c154258c75 --- /dev/null +++ b/resources/images/toolbar_texture_displacement.svg @@ -0,0 +1,240 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/profiles/BBL.json b/resources/profiles/BBL.json index 3f4f91efac..68d2028473 100644 --- a/resources/profiles/BBL.json +++ b/resources/profiles/BBL.json @@ -1,7 +1,7 @@ { "name": "Bambulab", "url": "http://www.bambulab.com/Parameters/vendor/BBL.json", - "version": "02.08.00.11", + "version": "02.08.00.12", "force_update": "0", "description": "the initial version of BBL configurations", "machine_model_list": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL H2C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL H2C 0.2 nozzle.json index fd561c9b02..c00681818c 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL H2C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL H2C 0.2 nozzle.json @@ -21,9 +21,11 @@ "0.0241" ], "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "40", "40" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL H2C 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL H2C 0.6 nozzle.json index 43827c7a5e..538d2920ec 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL H2C 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL H2C 0.6 nozzle.json @@ -21,6 +21,8 @@ "0.3341" ], "fan_max_speed": [ + "60", + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL H2C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL H2C 0.8 nozzle.json index 7fd3b25a7e..798ed443ba 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL H2C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL H2C 0.8 nozzle.json @@ -21,6 +21,7 @@ "0.0241" ], "fan_max_speed": [ + "60", "60" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL H2C.json b/resources/profiles/BBL/filament/Bambu ABS @BBL H2C.json index 410db132d7..28b7404dc2 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL H2C.json @@ -21,6 +21,8 @@ "0.3341" ], "fan_max_speed": [ + "60", + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL H2D 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL H2D 0.2 nozzle.json index e357dc5e05..ba4c0fd8b8 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL H2D 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL H2D 0.2 nozzle.json @@ -21,6 +21,7 @@ "0.0241" ], "fan_max_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL H2D 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL H2D 0.6 nozzle.json index 328543f9c0..9434bd4159 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL H2D 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL H2D 0.6 nozzle.json @@ -21,6 +21,8 @@ "0.0241" ], "fan_max_speed": [ + "60", + "60", "60" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL H2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL H2D 0.8 nozzle.json index 5d75f70a48..6cc753de96 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL H2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL H2D 0.8 nozzle.json @@ -21,6 +21,7 @@ "0.0241" ], "fan_max_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL H2D.json b/resources/profiles/BBL/filament/Bambu ABS @BBL H2D.json index dcbe4c9c9b..10b4722918 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL H2D.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL H2D.json @@ -21,6 +21,8 @@ "0.0241" ], "fan_max_speed": [ + "60", + "60", "60" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL H2DP 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL H2DP 0.2 nozzle.json index afb7d2af99..a13bfa07e8 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL H2DP 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL H2DP 0.2 nozzle.json @@ -9,6 +9,7 @@ "65" ], "fan_max_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL H2DP 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL H2DP 0.6 nozzle.json index 5ed4484f7f..980bdef186 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL H2DP 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL H2DP 0.6 nozzle.json @@ -9,6 +9,7 @@ "65" ], "fan_max_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL H2DP 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL H2DP 0.8 nozzle.json index 5f5a3435d7..284578284a 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL H2DP 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL H2DP 0.8 nozzle.json @@ -9,6 +9,7 @@ "65" ], "fan_max_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL H2DP.json b/resources/profiles/BBL/filament/Bambu ABS @BBL H2DP.json index ed170c8a30..9e9162545e 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL H2DP.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL H2DP.json @@ -9,6 +9,7 @@ "65" ], "fan_max_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL H2S 0.2 nozzle.json index 6f18702239..2945800452 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL H2S 0.2 nozzle.json @@ -21,9 +21,11 @@ "0.3341" ], "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL H2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL H2S 0.6 nozzle.json index d995f5cf2f..22595bb69d 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL H2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL H2S 0.6 nozzle.json @@ -21,6 +21,8 @@ "0.3341" ], "fan_max_speed": [ + "60", + "60", "60" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL H2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL H2S 0.8 nozzle.json index af38270fbb..d6ab8201db 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL H2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL H2S 0.8 nozzle.json @@ -21,6 +21,7 @@ "0.3341" ], "fan_max_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL H2S.json b/resources/profiles/BBL/filament/Bambu ABS @BBL H2S.json index 35a8d05943..84027cf8dc 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL H2S.json @@ -21,6 +21,8 @@ "0.3341" ], "fan_max_speed": [ + "60", + "60", "60" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL P1S 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL P1S 0.4 nozzle.json index d2b3ac673b..025925b578 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL P1S 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL P1S 0.4 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSB00_38", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL P1S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL P1S 0.6 nozzle.json index df087aa06b..e83e751cb1 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL P1S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL P1S 0.6 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSB00_39", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL P2S 0.2 nozzle.json index c82f324007..4788f5e566 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL P2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL P2S 0.2 nozzle.json @@ -25,6 +25,7 @@ "0.0241" ], "fan_min_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL P2S 0.6 nozzle.json index 6627fb2e62..98be4a6887 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL P2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL P2S 0.6 nozzle.json @@ -21,6 +21,8 @@ "0.3341" ], "fan_max_speed": [ + "60", + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL P2S 0.8 nozzle.json index de71006ade..3c42f060f5 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL P2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL P2S 0.8 nozzle.json @@ -25,6 +25,7 @@ "0.0241" ], "fan_max_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL P2S.json b/resources/profiles/BBL/filament/Bambu ABS @BBL P2S.json index 31fa4e25a1..c22e0202c5 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL P2S.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL P2S.json @@ -26,6 +26,8 @@ "0.3341" ], "fan_max_speed": [ + "60", + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL X1C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL X1C 0.2 nozzle.json index ef901b0dc1..a55b483e20 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL X1C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL X1C 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSB00_00", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL X1C 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL X1C 0.6 nozzle.json index b7d3a06ea2..aecbbff44d 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL X1C 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL X1C 0.6 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSB00_40", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL X1C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL X1C 0.8 nozzle.json index c25c690d8b..597c843568 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL X1C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL X1C 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSB00_01", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL X1C.json b/resources/profiles/BBL/filament/Bambu ABS @BBL X1C.json index 4632ca054f..d95cb4a119 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL X1C.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL X1C.json @@ -6,6 +6,7 @@ "setting_id": "GFSB00", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL X1E 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL X1E 0.2 nozzle.json index 63b7b1ea67..ba65ac0e69 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL X1E 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL X1E 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSB00_05", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL X1E 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL X1E 0.8 nozzle.json index 5022a70244..18639ac21f 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL X1E 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL X1E 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSB00_06", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL X1E.json b/resources/profiles/BBL/filament/Bambu ABS @BBL X1E.json index 996212ace3..22b568affb 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL X1E.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL X1E.json @@ -6,6 +6,7 @@ "setting_id": "GFSB00_04", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL X2D 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL X2D 0.2 nozzle.json index 31edcc6add..bb895bc66a 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL X2D 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL X2D 0.2 nozzle.json @@ -21,6 +21,9 @@ "0.0241" ], "fan_min_speed": [ + "80", + "80", + "80", "80" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL X2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL X2D 0.4 nozzle.json index e647fa477c..0defaafc07 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL X2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL X2D 0.4 nozzle.json @@ -21,6 +21,11 @@ "0.0241" ], "fan_max_speed": [ + "60", + "60", + "60", + "60", + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL X2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu ABS @BBL X2D 0.8 nozzle.json index ab10c78829..33bf55cdf1 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL X2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL X2D 0.8 nozzle.json @@ -21,6 +21,9 @@ "0.0241" ], "fan_max_speed": [ + "60", + "60", + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ABS @BBL X2D.json b/resources/profiles/BBL/filament/Bambu ABS @BBL X2D.json index 6a0850497b..6f9adbd82a 100644 --- a/resources/profiles/BBL/filament/Bambu ABS @BBL X2D.json +++ b/resources/profiles/BBL/filament/Bambu ABS @BBL X2D.json @@ -21,6 +21,11 @@ "0.0241" ], "fan_max_speed": [ + "60", + "60", + "60", + "60", + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ASA @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu ASA @BBL P2S 0.2 nozzle.json index 53676c7bc2..3b07712c16 100644 --- a/resources/profiles/BBL/filament/Bambu ASA @BBL P2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ASA @BBL P2S 0.2 nozzle.json @@ -25,9 +25,11 @@ "-0.0092" ], "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ASA @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu ASA @BBL P2S 0.6 nozzle.json index 463333a491..035e6e1269 100644 --- a/resources/profiles/BBL/filament/Bambu ASA @BBL P2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ASA @BBL P2S 0.6 nozzle.json @@ -26,6 +26,8 @@ "0.2258" ], "fan_min_speed": [ + "25", + "25", "25" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ASA @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu ASA @BBL P2S 0.8 nozzle.json index 284e476b53..1967867290 100644 --- a/resources/profiles/BBL/filament/Bambu ASA @BBL P2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ASA @BBL P2S 0.8 nozzle.json @@ -21,6 +21,7 @@ "-0.0092" ], "fan_min_speed": [ + "25", "25" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ASA @BBL X1 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu ASA @BBL X1 0.6 nozzle.json index 63e38090f7..01b0a9ec05 100644 --- a/resources/profiles/BBL/filament/Bambu ASA @BBL X1 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ASA @BBL X1 0.6 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSB01_04", "instantiation": "true", "fan_min_speed": [ + "25", "25" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/BBL/filament/Bambu ASA @BBL X1C.json b/resources/profiles/BBL/filament/Bambu ASA @BBL X1C.json index ab1677285e..a4b301716c 100644 --- a/resources/profiles/BBL/filament/Bambu ASA @BBL X1C.json +++ b/resources/profiles/BBL/filament/Bambu ASA @BBL X1C.json @@ -6,6 +6,7 @@ "setting_id": "GFSB01_00", "instantiation": "true", "fan_min_speed": [ + "25", "25" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/BBL/filament/Bambu ASA @BBL X1E.json b/resources/profiles/BBL/filament/Bambu ASA @BBL X1E.json index 9336323c61..7bf1d303ca 100644 --- a/resources/profiles/BBL/filament/Bambu ASA @BBL X1E.json +++ b/resources/profiles/BBL/filament/Bambu ASA @BBL X1E.json @@ -6,6 +6,7 @@ "setting_id": "GFSB01_05", "instantiation": "true", "fan_min_speed": [ + "25", "25" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/BBL/filament/Bambu ASA @BBL X2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu ASA @BBL X2D 0.8 nozzle.json index 4bf30b3fce..45a24500f6 100644 --- a/resources/profiles/BBL/filament/Bambu ASA @BBL X2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu ASA @BBL X2D 0.8 nozzle.json @@ -21,6 +21,9 @@ "-0.0092" ], "fan_min_speed": [ + "25", + "25", + "25", "25" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu ASA @BBL X2D.json b/resources/profiles/BBL/filament/Bambu ASA @BBL X2D.json index 38c96bff13..9f702c0058 100644 --- a/resources/profiles/BBL/filament/Bambu ASA @BBL X2D.json +++ b/resources/profiles/BBL/filament/Bambu ASA @BBL X2D.json @@ -21,6 +21,11 @@ "-0.0092" ], "fan_min_speed": [ + "25", + "25", + "25", + "25", + "25", "25" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PA-CF @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PA-CF @BBL H2S.json index b1a9ff5a1b..dce0062f7a 100644 --- a/resources/profiles/BBL/filament/Bambu PA-CF @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PA-CF @BBL H2S.json @@ -12,9 +12,13 @@ "5" ], "fan_max_speed": [ + "30", + "30", "30" ], "fan_min_speed": [ + "10", + "10", "10" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PA-CF @BBL P2S.json b/resources/profiles/BBL/filament/Bambu PA-CF @BBL P2S.json index 2d6934a50f..70d1a9841c 100644 --- a/resources/profiles/BBL/filament/Bambu PA-CF @BBL P2S.json +++ b/resources/profiles/BBL/filament/Bambu PA-CF @BBL P2S.json @@ -17,9 +17,13 @@ "5" ], "fan_max_speed": [ + "30", + "30", "30" ], "fan_min_speed": [ + "10", + "10", "10" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PA-CF @BBL X1C.json b/resources/profiles/BBL/filament/Bambu PA-CF @BBL X1C.json index 52af58d8ba..a6b2b9f83a 100644 --- a/resources/profiles/BBL/filament/Bambu PA-CF @BBL X1C.json +++ b/resources/profiles/BBL/filament/Bambu PA-CF @BBL X1C.json @@ -9,9 +9,11 @@ "5" ], "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "10", "10" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PA-CF @BBL X1E.json b/resources/profiles/BBL/filament/Bambu PA-CF @BBL X1E.json index b12f9f014e..38e1aa5902 100644 --- a/resources/profiles/BBL/filament/Bambu PA-CF @BBL X1E.json +++ b/resources/profiles/BBL/filament/Bambu PA-CF @BBL X1E.json @@ -12,9 +12,11 @@ "5" ], "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "10", "10" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PA-CF @BBL X2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PA-CF @BBL X2D 0.4 nozzle.json index 4cd5698e7f..c94de9de77 100644 --- a/resources/profiles/BBL/filament/Bambu PA-CF @BBL X2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PA-CF @BBL X2D 0.4 nozzle.json @@ -15,9 +15,19 @@ "5" ], "fan_max_speed": [ + "30", + "30", + "30", + "30", + "30", "30" ], "fan_min_speed": [ + "10", + "10", + "10", + "10", + "10", "10" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PA-CF @BBL X2D.json b/resources/profiles/BBL/filament/Bambu PA-CF @BBL X2D.json index 384d2e4f3b..d0b17e5af0 100644 --- a/resources/profiles/BBL/filament/Bambu PA-CF @BBL X2D.json +++ b/resources/profiles/BBL/filament/Bambu PA-CF @BBL X2D.json @@ -15,9 +15,19 @@ "5" ], "fan_max_speed": [ + "30", + "30", + "30", + "30", + "30", "30" ], "fan_min_speed": [ + "10", + "10", + "10", + "10", + "10", "10" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PA6-CF @BBL X2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PA6-CF @BBL X2D 0.4 nozzle.json index 2f8844aac0..34a181b789 100644 --- a/resources/profiles/BBL/filament/Bambu PA6-CF @BBL X2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PA6-CF @BBL X2D 0.4 nozzle.json @@ -21,9 +21,19 @@ "-0.038" ], "fan_max_speed": [ + "40", + "40", + "40", + "40", + "40", "40" ], "fan_min_speed": [ + "20", + "20", + "20", + "20", + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PAHT-CF @BBL X2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PAHT-CF @BBL X2D 0.4 nozzle.json index 0f02b6ea95..778e557421 100644 --- a/resources/profiles/BBL/filament/Bambu PAHT-CF @BBL X2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PAHT-CF @BBL X2D 0.4 nozzle.json @@ -21,9 +21,19 @@ "-0.055" ], "fan_max_speed": [ + "45", + "45", + "45", + "45", + "45", "45" ], "fan_min_speed": [ + "25", + "25", + "25", + "25", + "25", "25" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL H2C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL H2C 0.2 nozzle.json index a3ba75304a..5a84b1af84 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL H2C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL H2C 0.2 nozzle.json @@ -21,6 +21,7 @@ "0.0183" ], "fan_max_speed": [ + "40", "40" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL H2C 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL H2C 0.6 nozzle.json index b53e6eaf93..cd61e78fd6 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL H2C 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL H2C 0.6 nozzle.json @@ -21,6 +21,8 @@ "0.2208" ], "fan_max_speed": [ + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL H2C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL H2C 0.8 nozzle.json index f4f455a8b2..ac5133e81e 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL H2C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL H2C 0.8 nozzle.json @@ -21,6 +21,7 @@ "0.0183" ], "fan_max_speed": [ + "40", "40" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PC @BBL H2C.json index b2bb924ca0..90fb772a06 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL H2C.json @@ -21,6 +21,8 @@ "0.2208" ], "fan_max_speed": [ + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL H2D 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL H2D 0.2 nozzle.json index fea45a13bd..2830345d52 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL H2D 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL H2D 0.2 nozzle.json @@ -21,6 +21,7 @@ "0.0183" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL H2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL H2D 0.4 nozzle.json index 7877739470..b5fb71be51 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL H2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL H2D 0.4 nozzle.json @@ -21,6 +21,8 @@ "0.0183" ], "fan_max_speed": [ + "40", + "40", "40" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL H2D 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL H2D 0.6 nozzle.json index 50a080979a..5abbb98402 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL H2D 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL H2D 0.6 nozzle.json @@ -21,6 +21,8 @@ "0.0183" ], "fan_max_speed": [ + "40", + "40", "40" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL H2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL H2D 0.8 nozzle.json index 37aa3eb857..f287b4b916 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL H2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL H2D 0.8 nozzle.json @@ -21,6 +21,7 @@ "0.0183" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL H2DP 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL H2DP 0.2 nozzle.json index 55575cee0b..fdcc06fe39 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL H2DP 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL H2DP 0.2 nozzle.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL H2DP 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL H2DP 0.4 nozzle.json index 4f843feadd..1fd18e3279 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL H2DP 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL H2DP 0.4 nozzle.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL H2DP 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL H2DP 0.6 nozzle.json index 1417c3c5a0..31025856e9 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL H2DP 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL H2DP 0.6 nozzle.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL H2DP 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL H2DP 0.8 nozzle.json index 01f3c1431f..159942cfe2 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL H2DP 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL H2DP 0.8 nozzle.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL H2S 0.2 nozzle.json index 8b6b883bb6..6887890740 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL H2S 0.2 nozzle.json @@ -18,6 +18,7 @@ "0.2208" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL H2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL H2S 0.6 nozzle.json index 21a6b513d5..7cffb5f3d7 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL H2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL H2S 0.6 nozzle.json @@ -21,6 +21,8 @@ "0.2208" ], "fan_max_speed": [ + "40", + "40", "40" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL H2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL H2S 0.8 nozzle.json index 6564a05f22..1d18c887cd 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL H2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL H2S 0.8 nozzle.json @@ -21,6 +21,7 @@ "0.2208" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PC @BBL H2S.json index 0a6ae94079..8cdcb1f9b5 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL H2S.json @@ -21,6 +21,8 @@ "0.2208" ], "fan_max_speed": [ + "40", + "40", "40" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL P2S 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL P2S 0.4 nozzle.json index 37390058bf..c7b0d65ca6 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL P2S 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL P2S 0.4 nozzle.json @@ -27,6 +27,8 @@ "100" ], "fan_max_speed": [ + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL P2S 0.6 nozzle.json index 0e8d81de92..fcad5a9ccf 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL P2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL P2S 0.6 nozzle.json @@ -27,6 +27,8 @@ "100" ], "fan_max_speed": [ + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL P2S 0.8 nozzle.json index 27a717ab89..d22d8550ef 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL P2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL P2S 0.8 nozzle.json @@ -27,6 +27,7 @@ "100" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL X1C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL X1C 0.2 nozzle.json index b2227e037e..dd9ab06970 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL X1C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL X1C 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSC00_02", "instantiation": "true", "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL X1C 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL X1C 0.6 nozzle.json index e8b0cf29d0..47dbdb48ac 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL X1C 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL X1C 0.6 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSC00_01", "instantiation": "true", "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL X1C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL X1C 0.8 nozzle.json index 7b18784bc6..4c2820a6f7 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL X1C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL X1C 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSC00_00", "instantiation": "true", "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL X1C.json b/resources/profiles/BBL/filament/Bambu PC @BBL X1C.json index 2462d8ccb9..ce801cb6c2 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL X1C.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL X1C.json @@ -6,6 +6,7 @@ "setting_id": "GFSC00", "instantiation": "true", "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL X1E 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL X1E 0.2 nozzle.json index 8cab064d11..4bfeb106eb 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL X1E 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL X1E 0.2 nozzle.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL X1E 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL X1E 0.6 nozzle.json index 6166c3653e..265364e4fd 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL X1E 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL X1E 0.6 nozzle.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL X1E 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL X1E 0.8 nozzle.json index 299637f67d..1fe2bd26c4 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL X1E 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL X1E 0.8 nozzle.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL X1E.json b/resources/profiles/BBL/filament/Bambu PC @BBL X1E.json index a0f287e1e5..d452548dfc 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL X1E.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL X1E.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL X2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL X2D 0.4 nozzle.json index ef08e157a6..8c844e320e 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL X2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL X2D 0.4 nozzle.json @@ -21,9 +21,19 @@ "0.0183" ], "fan_max_speed": [ + "55", + "55", + "55", + "55", + "55", "55" ], "fan_min_speed": [ + "25", + "25", + "25", + "25", + "25", "25" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL X2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PC @BBL X2D 0.8 nozzle.json index 45a270ee3a..0fe7536db8 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL X2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL X2D 0.8 nozzle.json @@ -21,6 +21,9 @@ "0.0183" ], "fan_max_speed": [ + "40", + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC @BBL X2D.json b/resources/profiles/BBL/filament/Bambu PC @BBL X2D.json index dfd7ca334f..94e8ba6fc5 100644 --- a/resources/profiles/BBL/filament/Bambu PC @BBL X2D.json +++ b/resources/profiles/BBL/filament/Bambu PC @BBL X2D.json @@ -21,6 +21,11 @@ "0.0183" ], "fan_max_speed": [ + "40", + "40", + "40", + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2C 0.2 nozzle.json index ffde022ea3..40e596e5a9 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2C 0.2 nozzle.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2C 0.4 nozzle.json index fcf1a079ce..460057a736 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2C 0.4 nozzle.json @@ -9,6 +9,8 @@ "60" ], "fan_max_speed": [ + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2C.json index 957304bc2a..5446841b6e 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2C.json @@ -9,6 +9,8 @@ "60" ], "fan_max_speed": [ + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2D 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2D 0.2 nozzle.json index e768297ad6..02269c8464 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2D 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2D 0.2 nozzle.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2D 0.4 nozzle.json index 75dffbd64b..a3c4f6355a 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2D 0.4 nozzle.json @@ -9,6 +9,8 @@ "60" ], "fan_max_speed": [ + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2D 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2D 0.6 nozzle.json index e30efad3f5..3b16cfe4ce 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2D 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2D 0.6 nozzle.json @@ -9,6 +9,8 @@ "60" ], "fan_max_speed": [ + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2D 0.8 nozzle.json index 9806d53088..27a87f6ce9 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2D 0.8 nozzle.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2DP 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2DP 0.2 nozzle.json index d61c1da460..a68b8ebf75 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2DP 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2DP 0.2 nozzle.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2DP 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2DP 0.4 nozzle.json index 8902b0bd27..42bbef7019 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2DP 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2DP 0.4 nozzle.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2DP 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2DP 0.6 nozzle.json index 0ba2dc14ed..99e6d107ab 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2DP 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2DP 0.6 nozzle.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2DP 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2DP 0.8 nozzle.json index 1b1768ef51..4d8873fd12 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2DP 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2DP 0.8 nozzle.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2S 0.2 nozzle.json index ebcb2129dd..d773154c7d 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2S 0.2 nozzle.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2S.json index cf9963a435..aaf725a515 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL H2S.json @@ -9,6 +9,8 @@ "60" ], "fan_max_speed": [ + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL P1P 0.2 nozzle.json index b9e028b071..8fbd9582bc 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL P1P 0.2 nozzle.json @@ -12,6 +12,7 @@ "100" ], "fan_max_speed": [ + "20", "20" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL P1P.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL P1P.json index 50c66f2864..c532964815 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL P1P.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL P1P.json @@ -12,6 +12,7 @@ "100" ], "fan_max_speed": [ + "20", "20" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL P1S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL P1S 0.2 nozzle.json index 472acab285..53d7d9f9f0 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL P1S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL P1S 0.2 nozzle.json @@ -12,6 +12,7 @@ "100" ], "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL P1S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL P1S 0.6 nozzle.json index 1089e6e4d7..1d12b0f53f 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL P1S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL P1S 0.6 nozzle.json @@ -12,6 +12,7 @@ "100" ], "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL P1S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL P1S 0.8 nozzle.json index a8d3cea654..f7a86aaf4c 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL P1S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL P1S 0.8 nozzle.json @@ -12,6 +12,7 @@ "100" ], "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL P1S.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL P1S.json index 0150515d70..93a219b4dd 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL P1S.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL P1S.json @@ -12,6 +12,7 @@ "100" ], "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL P2S 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL P2S 0.4 nozzle.json index ea21badf01..12cc5c1f31 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL P2S 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL P2S 0.4 nozzle.json @@ -15,6 +15,8 @@ "100" ], "fan_max_speed": [ + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL P2S 0.6 nozzle.json index ae0a394959..ad42035768 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL P2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL P2S 0.6 nozzle.json @@ -15,6 +15,8 @@ "100" ], "fan_max_speed": [ + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL P2S 0.8 nozzle.json index 6698de66a4..e1b6bcbb96 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL P2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL P2S 0.8 nozzle.json @@ -15,6 +15,7 @@ "100" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL X1C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL X1C 0.2 nozzle.json index a58b3a34fc..4b764c32bf 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL X1C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL X1C 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSC01_03", "instantiation": "true", "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL X1C 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL X1C 0.6 nozzle.json index 5f9fc6b9f8..41825cbce6 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL X1C 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL X1C 0.6 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSC01_01", "instantiation": "true", "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL X1C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL X1C 0.8 nozzle.json index bf7f605858..f7704e22bf 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL X1C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL X1C 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSC01_02", "instantiation": "true", "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL X1C.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL X1C.json index c30bef59b3..b3b011aefe 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL X1C.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL X1C.json @@ -6,6 +6,7 @@ "setting_id": "GFSC01_00", "instantiation": "true", "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL X1E 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL X1E 0.2 nozzle.json index d786e82f33..01ea062c32 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL X1E 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL X1E 0.2 nozzle.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL X1E 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL X1E 0.6 nozzle.json index f5545427f1..d323b5ff9b 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL X1E 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL X1E 0.6 nozzle.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL X1E 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL X1E 0.8 nozzle.json index 050855f467..9f7506e488 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL X1E 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL X1E 0.8 nozzle.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL X1E.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL X1E.json index 5314f3101a..5c062b2d0f 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL X1E.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL X1E.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL X2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL X2D 0.4 nozzle.json index ccdeb880a0..fcf2b10b28 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL X2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL X2D 0.4 nozzle.json @@ -9,6 +9,11 @@ "60" ], "fan_max_speed": [ + "40", + "40", + "40", + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL X2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL X2D 0.8 nozzle.json index 5c58c59432..8661cafc38 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL X2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL X2D 0.8 nozzle.json @@ -9,6 +9,9 @@ "60" ], "fan_max_speed": [ + "40", + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PC FR @BBL X2D.json b/resources/profiles/BBL/filament/Bambu PC FR @BBL X2D.json index 9dd52a836f..032233bbfa 100644 --- a/resources/profiles/BBL/filament/Bambu PC FR @BBL X2D.json +++ b/resources/profiles/BBL/filament/Bambu PC FR @BBL X2D.json @@ -9,6 +9,11 @@ "60" ], "fan_max_speed": [ + "40", + "40", + "40", + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PET-CF @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PET-CF @BBL H2C 0.4 nozzle.json index 4633db0021..08692a967c 100644 --- a/resources/profiles/BBL/filament/Bambu PET-CF @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PET-CF @BBL H2C 0.4 nozzle.json @@ -30,9 +30,13 @@ "80" ], "fan_max_speed": [ + "80", + "80", "80" ], "fan_min_speed": [ + "1", + "1", "1" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PET-CF @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PET-CF @BBL H2C.json index cee2b21e4d..8fc8fa046b 100644 --- a/resources/profiles/BBL/filament/Bambu PET-CF @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PET-CF @BBL H2C.json @@ -30,9 +30,13 @@ "80" ], "fan_max_speed": [ + "80", + "80", "80" ], "fan_min_speed": [ + "1", + "1", "1" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PET-CF @BBL H2D.json b/resources/profiles/BBL/filament/Bambu PET-CF @BBL H2D.json index 204fcc2b98..b6824b4c0b 100644 --- a/resources/profiles/BBL/filament/Bambu PET-CF @BBL H2D.json +++ b/resources/profiles/BBL/filament/Bambu PET-CF @BBL H2D.json @@ -30,9 +30,13 @@ "80" ], "fan_max_speed": [ + "80", + "80", "80" ], "fan_min_speed": [ + "1", + "1", "1" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PET-CF @BBL H2DP.json b/resources/profiles/BBL/filament/Bambu PET-CF @BBL H2DP.json index d6a563e31f..d60c7cecff 100644 --- a/resources/profiles/BBL/filament/Bambu PET-CF @BBL H2DP.json +++ b/resources/profiles/BBL/filament/Bambu PET-CF @BBL H2DP.json @@ -24,9 +24,11 @@ "80" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "1", "1" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PET-CF @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PET-CF @BBL H2S.json index 9eb410e996..d1726e1a55 100644 --- a/resources/profiles/BBL/filament/Bambu PET-CF @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PET-CF @BBL H2S.json @@ -30,9 +30,13 @@ "80" ], "fan_max_speed": [ + "80", + "80", "80" ], "fan_min_speed": [ + "1", + "1", "1" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PET-CF @BBL P2S.json b/resources/profiles/BBL/filament/Bambu PET-CF @BBL P2S.json index 8d5f30f117..5331a62893 100644 --- a/resources/profiles/BBL/filament/Bambu PET-CF @BBL P2S.json +++ b/resources/profiles/BBL/filament/Bambu PET-CF @BBL P2S.json @@ -30,9 +30,13 @@ "80" ], "fan_max_speed": [ + "80", + "80", "80" ], "fan_min_speed": [ + "1", + "1", "1" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PET-CF @BBL X1C.json b/resources/profiles/BBL/filament/Bambu PET-CF @BBL X1C.json index e119b37000..5c48a06982 100644 --- a/resources/profiles/BBL/filament/Bambu PET-CF @BBL X1C.json +++ b/resources/profiles/BBL/filament/Bambu PET-CF @BBL X1C.json @@ -9,9 +9,11 @@ "80" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "1", "1" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PET-CF @BBL X1E.json b/resources/profiles/BBL/filament/Bambu PET-CF @BBL X1E.json index 2e750018cf..1ce4d8ac33 100644 --- a/resources/profiles/BBL/filament/Bambu PET-CF @BBL X1E.json +++ b/resources/profiles/BBL/filament/Bambu PET-CF @BBL X1E.json @@ -12,9 +12,11 @@ "80" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "1", "1" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PET-CF @BBL X2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PET-CF @BBL X2D 0.4 nozzle.json index 55a854c514..42156c4444 100644 --- a/resources/profiles/BBL/filament/Bambu PET-CF @BBL X2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PET-CF @BBL X2D 0.4 nozzle.json @@ -30,9 +30,19 @@ "80" ], "fan_max_speed": [ + "80", + "80", + "80", + "80", + "80", "80" ], "fan_min_speed": [ + "1", + "1", + "1", + "1", + "1", "1" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PET-CF @BBL X2D.json b/resources/profiles/BBL/filament/Bambu PET-CF @BBL X2D.json index 277b655bf1..f902e4b8c7 100644 --- a/resources/profiles/BBL/filament/Bambu PET-CF @BBL X2D.json +++ b/resources/profiles/BBL/filament/Bambu PET-CF @BBL X2D.json @@ -30,9 +30,19 @@ "80" ], "fan_max_speed": [ + "80", + "80", + "80", + "80", + "80", "80" ], "fan_min_speed": [ + "1", + "1", + "1", + "1", + "1", "1" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2C 0.2 nozzle.json index 1767dc81c5..5b3ffab072 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2C 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSG00_24", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2C 0.4 nozzle.json index 9611d1f270..cd0d3bc27d 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2C 0.4 nozzle.json @@ -6,9 +6,13 @@ "setting_id": "GFSG00_23", "instantiation": "true", "fan_max_speed": [ + "30", + "30", "30" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2C 0.8 nozzle.json index f84645846a..b37b8dffc7 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2C 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSG00_44", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2C.json index 1b09bc58f6..fc20f3b466 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSG00_25", "instantiation": "true", "fan_min_speed": [ + "20", + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2D 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2D 0.2 nozzle.json index bc8b057d8a..754241c419 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2D 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2D 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSG00_15", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2D 0.4 nozzle.json index a2fd083bc2..08293df379 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2D 0.4 nozzle.json @@ -6,9 +6,13 @@ "setting_id": "GFSG00_09", "instantiation": "true", "fan_max_speed": [ + "30", + "30", "30" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2D 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2D 0.6 nozzle.json index 22ad00b017..bb30864d70 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2D 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2D 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSG00_16", "instantiation": "true", "fan_min_speed": [ + "20", + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2D 0.8 nozzle.json index 122c8a04cc..e3e9d11068 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2D 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSG00_45", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2DP 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2DP 0.2 nozzle.json index 36bb96bd39..f3ace78797 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2DP 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2DP 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSG00_20", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2DP 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2DP 0.4 nozzle.json index c2a14583c4..7e6b7ce770 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2DP 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2DP 0.4 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "GFSG00_19", "instantiation": "true", "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2DP 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2DP 0.6 nozzle.json index 53a228357b..bd9579de8a 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2DP 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2DP 0.6 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSG00_21", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2DP 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2DP 0.8 nozzle.json index 50663c56b2..82534872ba 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2DP 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2DP 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSG00_46", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2S 0.2 nozzle.json index 7c16a6a6c9..a513dff4d9 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSG00_11", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2S 0.6 nozzle.json index 48fdf41ed8..50b170db33 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2S 0.6 nozzle.json @@ -6,9 +6,13 @@ "setting_id": "GFSG00_32", "instantiation": "true", "fan_max_speed": [ + "60", + "60", "60" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2S 0.8 nozzle.json index 3887491880..5c04eb6cce 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2S 0.8 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "GFSG00_47", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2S.json index a640e8927f..11db629a3b 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL H2S.json @@ -6,9 +6,13 @@ "setting_id": "GFSG00_10", "instantiation": "true", "fan_max_speed": [ + "60", + "60", "60" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL P2S 0.2 nozzle.json index c718d3d5a3..16e0ee2324 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL P2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL P2S 0.2 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "GFSG00_17", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL P2S 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL P2S 0.4 nozzle.json index 8c24685000..cc6375362e 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL P2S 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL P2S 0.4 nozzle.json @@ -6,9 +6,13 @@ "setting_id": "GFSG00_14", "instantiation": "true", "fan_max_speed": [ + "60", + "60", "60" ], "fan_min_speed": [ + "30", + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL P2S 0.6 nozzle.json index 1b5a5fb77e..1a01f48967 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL P2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL P2S 0.6 nozzle.json @@ -6,9 +6,13 @@ "setting_id": "GFSG00_18", "instantiation": "true", "fan_max_speed": [ + "60", + "60", "60" ], "fan_min_speed": [ + "30", + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL P2S 0.8 nozzle.json index 0bcf1a6a65..375ecfbb25 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL P2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL P2S 0.8 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "GFSG00_48", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X1C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X1C 0.2 nozzle.json index 9d1f36e9f3..de6792eb32 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X1C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X1C 0.2 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "GFSG00_00", "instantiation": "true", "fan_max_speed": [ + "50", "50" ], "fan_min_speed": [ + "20", "20" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X1C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X1C 0.8 nozzle.json index b7a18afcd6..31a23708dd 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X1C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X1C 0.8 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "GFSG00_02", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "20", "20" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X1C.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X1C.json index a43717b952..96139668a3 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X1C.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X1C.json @@ -6,6 +6,7 @@ "setting_id": "GFSG00_01", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X2D 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X2D 0.2 nozzle.json index 69852348e4..8f22458ba7 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X2D 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X2D 0.2 nozzle.json @@ -6,9 +6,15 @@ "setting_id": "GFSG00_13", "instantiation": "true", "fan_max_speed": [ + "60", + "60", + "60", "60" ], "fan_min_speed": [ + "30", + "30", + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X2D 0.4 nozzle.json index c6cba824fd..99741b8e2b 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X2D 0.4 nozzle.json @@ -6,9 +6,19 @@ "setting_id": "GFSG00_12", "instantiation": "true", "fan_max_speed": [ + "60", + "60", + "60", + "60", + "60", "60" ], "fan_min_speed": [ + "30", + "30", + "30", + "30", + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X2D 0.8 nozzle.json index bf242cb1c1..4523b64c71 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X2D 0.8 nozzle.json @@ -6,9 +6,15 @@ "setting_id": "GFSG00_49", "instantiation": "true", "fan_max_speed": [ + "60", + "60", + "60", "60" ], "fan_min_speed": [ + "30", + "30", + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X2D.json b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X2D.json index 829448ab91..6f26175cfd 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X2D.json +++ b/resources/profiles/BBL/filament/Bambu PETG Basic @BBL X2D.json @@ -6,9 +6,19 @@ "setting_id": "GFSG00_22", "instantiation": "true", "fan_max_speed": [ + "60", + "60", + "60", + "60", + "60", "60" ], "fan_min_speed": [ + "30", + "30", + "30", + "30", + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2C 0.2 nozzle.json index 0fbf4068cb..10cf829118 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2C 0.2 nozzle.json @@ -21,6 +21,7 @@ "20" ], "fan_min_speed": [ + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2C 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2C 0.6 nozzle.json index 71292a2121..c41df16615 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2C 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2C 0.6 nozzle.json @@ -21,6 +21,8 @@ "20" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2C 0.8 nozzle.json index 6be40ba26d..bd71516546 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2C 0.8 nozzle.json @@ -21,6 +21,7 @@ "20" ], "fan_min_speed": [ + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2C.json index 77226518e4..7298848d07 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2C.json @@ -21,6 +21,8 @@ "20" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2D 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2D 0.2 nozzle.json index d7e22db57d..6e36e81da8 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2D 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2D 0.2 nozzle.json @@ -21,6 +21,7 @@ "20" ], "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2D 0.4 nozzle.json index e53509cfa5..f290a019f6 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2D 0.4 nozzle.json @@ -21,9 +21,13 @@ "20" ], "fan_max_speed": [ + "30", + "30", "30" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2D 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2D 0.6 nozzle.json index 4ebbc2da27..e7e359704c 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2D 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2D 0.6 nozzle.json @@ -21,6 +21,8 @@ "20" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2D 0.8 nozzle.json index 20402f5724..3303ea7265 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2D 0.8 nozzle.json @@ -21,6 +21,7 @@ "20" ], "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2DP 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2DP 0.2 nozzle.json index e0eaa5b1f4..63e2a461f8 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2DP 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2DP 0.2 nozzle.json @@ -9,6 +9,7 @@ "20" ], "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2DP 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2DP 0.4 nozzle.json index 7fec884516..71189a22a9 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2DP 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2DP 0.4 nozzle.json @@ -18,6 +18,7 @@ "20" ], "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2DP 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2DP 0.6 nozzle.json index 9a75d0fb5c..612992bf14 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2DP 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2DP 0.6 nozzle.json @@ -9,6 +9,7 @@ "20" ], "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2DP 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2DP 0.8 nozzle.json index d4f215c1f2..3d2fd0c48c 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2DP 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2DP 0.8 nozzle.json @@ -9,6 +9,7 @@ "20" ], "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2S 0.2 nozzle.json index ecfadc5c2f..98b79fb3ad 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2S 0.2 nozzle.json @@ -21,9 +21,11 @@ "20" ], "fan_max_speed": [ + "50", "50" ], "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2S 0.6 nozzle.json index a06b5d9f2a..28d6e8e8aa 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2S 0.6 nozzle.json @@ -21,9 +21,13 @@ "20" ], "fan_max_speed": [ + "50", + "50", "50" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2S 0.8 nozzle.json index 2c38bb2fd7..9f165d9c76 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2S 0.8 nozzle.json @@ -21,9 +21,11 @@ "20" ], "fan_max_speed": [ + "50", "50" ], "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2S.json index 1fc3f03964..43787a6732 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL H2S.json @@ -21,9 +21,13 @@ "20" ], "fan_max_speed": [ + "50", + "50", "50" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL P1P 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL P1P 0.4 nozzle.json index c50087e617..67cb2723cf 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL P1P 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL P1P 0.4 nozzle.json @@ -9,6 +9,7 @@ "20" ], "fan_min_speed": [ + "20", "20" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL P1S 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL P1S 0.4 nozzle.json index 782a19a045..00fdfaf9d2 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL P1S 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL P1S 0.4 nozzle.json @@ -9,6 +9,7 @@ "20" ], "fan_min_speed": [ + "20", "20" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL P2S 0.2 nozzle.json index 7160dd0f0d..eef6358ed2 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL P2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL P2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSG02_17", "instantiation": "true", "additional_cooling_fan_speed": [ + "40", "40" ], "counter_coef_2": [ @@ -24,9 +25,11 @@ "20" ], "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL P2S 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL P2S 0.4 nozzle.json index 4ff218f195..2fa9117a78 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL P2S 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL P2S 0.4 nozzle.json @@ -21,9 +21,13 @@ "20" ], "fan_max_speed": [ + "60", + "60", "60" ], "fan_min_speed": [ + "30", + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL P2S 0.6 nozzle.json index 10b8b449d7..ad0d25672c 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL P2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL P2S 0.6 nozzle.json @@ -21,9 +21,13 @@ "20" ], "fan_max_speed": [ + "60", + "60", "60" ], "fan_min_speed": [ + "30", + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL P2S 0.8 nozzle.json index f214212555..6a519754da 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL P2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL P2S 0.8 nozzle.json @@ -21,9 +21,11 @@ "20" ], "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL X1C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL X1C 0.2 nozzle.json index 52cf7e42be..11baeedb6e 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL X1C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL X1C 0.2 nozzle.json @@ -9,6 +9,7 @@ "20" ], "fan_min_speed": [ + "20", "20" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL X1C 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL X1C 0.6 nozzle.json index 5461db84fe..d9f680dd17 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL X1C 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL X1C 0.6 nozzle.json @@ -9,6 +9,7 @@ "20" ], "fan_min_speed": [ + "20", "20" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL X1C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL X1C 0.8 nozzle.json index e0df40dfcc..29c4ed85e3 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL X1C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL X1C 0.8 nozzle.json @@ -9,6 +9,7 @@ "20" ], "fan_min_speed": [ + "20", "20" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL X1C.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL X1C.json index 38d2d0a11b..cf3d77288c 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL X1C.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL X1C.json @@ -9,6 +9,7 @@ "20" ], "fan_min_speed": [ + "20", "20" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL X2D 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL X2D 0.2 nozzle.json index 20e69622e7..9666dae7d0 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL X2D 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL X2D 0.2 nozzle.json @@ -21,9 +21,15 @@ "20" ], "fan_max_speed": [ + "60", + "60", + "60", "60" ], "fan_min_speed": [ + "30", + "30", + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL X2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL X2D 0.4 nozzle.json index 81ffc9bf01..2b5576971e 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL X2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL X2D 0.4 nozzle.json @@ -21,9 +21,19 @@ "20" ], "fan_max_speed": [ + "60", + "60", + "60", + "60", + "60", "60" ], "fan_min_speed": [ + "30", + "30", + "30", + "30", + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL X2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL X2D 0.8 nozzle.json index 6db5064f92..af398088dc 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL X2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL X2D 0.8 nozzle.json @@ -21,9 +21,15 @@ "20" ], "fan_max_speed": [ + "60", + "60", + "60", "60" ], "fan_min_speed": [ + "30", + "30", + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG HF @BBL X2D.json b/resources/profiles/BBL/filament/Bambu PETG HF @BBL X2D.json index 84f2ede658..0b5bcd8fa4 100644 --- a/resources/profiles/BBL/filament/Bambu PETG HF @BBL X2D.json +++ b/resources/profiles/BBL/filament/Bambu PETG HF @BBL X2D.json @@ -21,9 +21,19 @@ "20" ], "fan_max_speed": [ + "60", + "60", + "60", + "60", + "60", "60" ], "fan_min_speed": [ + "30", + "30", + "30", + "30", + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2C 0.2 nozzle.json index 11e340a26f..a9159c0e51 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2C 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSG03_10", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2C 0.4 nozzle.json index 1feb3bc3d1..33fffd64a6 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2C 0.4 nozzle.json @@ -6,9 +6,13 @@ "setting_id": "GFSG03_11", "instantiation": "true", "fan_max_speed": [ + "30", + "30", "30" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2C 0.8 nozzle.json index 57ada86690..ed24e8c0f2 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2C 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSG03_12", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2C.json index c848448b3b..e4987f9444 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSG03_13", "instantiation": "true", "fan_min_speed": [ + "20", + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2D 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2D 0.2 nozzle.json index a50efe69d3..48de52a8ee 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2D 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2D 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSG03_14", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2D 0.4 nozzle.json index 037dbb39fc..2c637e48aa 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2D 0.4 nozzle.json @@ -6,9 +6,13 @@ "setting_id": "GFSG03_15", "instantiation": "true", "fan_max_speed": [ + "30", + "30", "30" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2D 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2D 0.6 nozzle.json index a60362042d..0339fb0505 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2D 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2D 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSG03_16", "instantiation": "true", "fan_min_speed": [ + "20", + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2D 0.8 nozzle.json index 003c19bb28..2fea9bb488 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2D 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSG03_17", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2DP 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2DP 0.2 nozzle.json index 8ba7e1fd4e..f28b6e63da 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2DP 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2DP 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSG03_18", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2DP 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2DP 0.4 nozzle.json index 201b082b3f..19dee4f9c7 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2DP 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2DP 0.4 nozzle.json @@ -6,9 +6,13 @@ "setting_id": "GFSG03_19", "instantiation": "true", "fan_max_speed": [ + "30", + "30", "30" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2DP 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2DP 0.6 nozzle.json index 292a5d108e..264ba6b25b 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2DP 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2DP 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSG03_20", "instantiation": "true", "fan_min_speed": [ + "20", + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2DP 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2DP 0.8 nozzle.json index 085e159b28..7779e219c1 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2DP 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2DP 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSG03_21", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2S 0.2 nozzle.json index ca2e8219fa..99d5279359 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSG03_22", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2S 0.6 nozzle.json index 052d7c8068..a5ab2326bc 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2S 0.6 nozzle.json @@ -6,9 +6,13 @@ "setting_id": "GFSG03_23", "instantiation": "true", "fan_max_speed": [ + "60", + "60", "60" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2S 0.8 nozzle.json index 3346abab97..8bf7065e8c 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2S 0.8 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "GFSG03_24", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2S.json index 0f95f15edd..c3716d514d 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL H2S.json @@ -6,9 +6,13 @@ "setting_id": "GFSG03_25", "instantiation": "true", "fan_max_speed": [ + "60", + "60", "60" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P1P 0.2 nozzle.json index 4f6e6b260a..2d35e4fb91 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P1P 0.2 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "GFSG03_49", "instantiation": "true", "fan_max_speed": [ + "50", "50" ], "fan_min_speed": [ + "20", "20" ], "filament_density": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P1P 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P1P 0.4 nozzle.json index 793997445f..7d20fb5b1f 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P1P 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P1P 0.4 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSG03_50", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_density": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P1P 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P1P 0.6 nozzle.json index 6c379dd43d..abf497bc62 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P1P 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P1P 0.6 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "GFSG03_51", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "20", "20" ], "filament_density": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P2S 0.2 nozzle.json index 8f17112e84..a743638555 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P2S 0.2 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "GFSG03_38", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P2S 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P2S 0.4 nozzle.json index 252bc6f136..03eae4d6d3 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P2S 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P2S 0.4 nozzle.json @@ -6,9 +6,13 @@ "setting_id": "GFSG03_39", "instantiation": "true", "fan_max_speed": [ + "60", + "60", "60" ], "fan_min_speed": [ + "30", + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P2S 0.6 nozzle.json index 4297e9278c..913a646355 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P2S 0.6 nozzle.json @@ -6,9 +6,13 @@ "setting_id": "GFSG03_40", "instantiation": "true", "fan_max_speed": [ + "60", + "60", "60" ], "fan_min_speed": [ + "30", + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P2S 0.8 nozzle.json index df51c24fd2..1d3e65ee6f 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL P2S 0.8 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "GFSG03_41", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X1C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X1C 0.2 nozzle.json index 31a5b61e21..b878af8540 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X1C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X1C 0.2 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "GFSG03_42", "instantiation": "true", "fan_max_speed": [ + "50", "50" ], "fan_min_speed": [ + "20", "20" ], "filament_density": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X1C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X1C 0.8 nozzle.json index 23db5e43c6..42bda23e2f 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X1C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X1C 0.8 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "GFSG03_43", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "20", "20" ], "filament_density": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X1C.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X1C.json index e56d030427..7d4ede9cae 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X1C.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X1C.json @@ -6,6 +6,7 @@ "setting_id": "GFSG03_44", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_density": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X2D 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X2D 0.2 nozzle.json index 0e56e63a37..3cb97fc7e5 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X2D 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X2D 0.2 nozzle.json @@ -6,9 +6,15 @@ "setting_id": "GFSG03_45", "instantiation": "true", "fan_max_speed": [ + "60", + "60", + "60", "60" ], "fan_min_speed": [ + "30", + "30", + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X2D 0.4 nozzle.json index a95be25e66..083cf77ded 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X2D 0.4 nozzle.json @@ -6,9 +6,19 @@ "setting_id": "GFSG03_46", "instantiation": "true", "fan_max_speed": [ + "60", + "60", + "60", + "60", + "60", "60" ], "fan_min_speed": [ + "30", + "30", + "30", + "30", + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X2D 0.8 nozzle.json index 9b9fa384c8..d9f44afcf5 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X2D 0.8 nozzle.json @@ -6,9 +6,15 @@ "setting_id": "GFSG03_47", "instantiation": "true", "fan_max_speed": [ + "60", + "60", + "60", "60" ], "fan_min_speed": [ + "30", + "30", + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X2D.json b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X2D.json index 8e90a43b96..9343f1265f 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X2D.json +++ b/resources/profiles/BBL/filament/Bambu PETG Matte @BBL X2D.json @@ -6,9 +6,19 @@ "setting_id": "GFSG03_48", "instantiation": "true", "fan_max_speed": [ + "60", + "60", + "60", + "60", + "60", "60" ], "fan_min_speed": [ + "30", + "30", + "30", + "30", + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2C 0.8 nozzle.json index 75ac270250..1caa272343 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2C 0.8 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "GFSG01_40", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2C.json index 73e0247e95..7942bf46f5 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2C.json @@ -6,9 +6,13 @@ "setting_id": "GFSG01_23", "instantiation": "true", "fan_max_speed": [ + "60", + "60", "60" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2D 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2D 0.6 nozzle.json index 0c2c4df008..adadc5b30d 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2D 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2D 0.6 nozzle.json @@ -6,9 +6,13 @@ "setting_id": "GFSG01_11", "instantiation": "true", "fan_max_speed": [ + "60", + "60", "60" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2D 0.8 nozzle.json index 66e2bbd006..b7532312a2 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2D 0.8 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "GFSG01_41", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2DP 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2DP 0.6 nozzle.json index 236296a6ce..d31ac20557 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2DP 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2DP 0.6 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "GFSG01_17", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2DP 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2DP 0.8 nozzle.json index fc0bf555ea..5f9c5dc247 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2DP 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL H2DP 0.8 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "GFSG01_42", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL P2S 0.2 nozzle.json index af938e3c2b..7415dbb6f7 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL P2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL P2S 0.2 nozzle.json @@ -6,12 +6,15 @@ "setting_id": "GFSG01_15", "instantiation": "true", "additional_cooling_fan_speed": [ + "20", "20" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL P2S 0.6 nozzle.json index 27b5f3a1fd..81620208c5 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL P2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL P2S 0.6 nozzle.json @@ -6,9 +6,13 @@ "setting_id": "GFSG01_14", "instantiation": "true", "fan_max_speed": [ + "60", + "60", "60" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL P2S 0.8 nozzle.json index cfbc2d52a7..eb44d5da56 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL P2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL P2S 0.8 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "GFSG01_43", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL X1C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL X1C 0.8 nozzle.json index 3ec40f01c5..68bf87dc3a 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL X1C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL X1C 0.8 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "GFSG01_01", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "20", "20" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL X2D 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL X2D 0.2 nozzle.json index 5d5cb6d173..47b3c851e3 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL X2D 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL X2D 0.2 nozzle.json @@ -6,12 +6,21 @@ "setting_id": "GFSG01_21", "instantiation": "true", "additional_cooling_fan_speed": [ + "20", + "20", + "20", "20" ], "fan_max_speed": [ + "40", + "40", + "40", "40" ], "fan_min_speed": [ + "20", + "20", + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL X2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL X2D 0.4 nozzle.json index 974b3528d6..212b9f5036 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL X2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL X2D 0.4 nozzle.json @@ -6,12 +6,27 @@ "setting_id": "GFSG01_19", "instantiation": "true", "additional_cooling_fan_speed": [ + "10", + "10", + "10", + "10", + "10", "10" ], "fan_max_speed": [ + "40", + "40", + "40", + "40", + "40", "40" ], "fan_min_speed": [ + "20", + "20", + "20", + "20", + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL X2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL X2D 0.8 nozzle.json index 4281c834f7..3f67d59a15 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL X2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL X2D 0.8 nozzle.json @@ -6,9 +6,15 @@ "setting_id": "GFSG01_29", "instantiation": "true", "fan_max_speed": [ + "60", + "60", + "60", "60" ], "fan_min_speed": [ + "20", + "20", + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL X2D.json b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL X2D.json index aca3950f6e..eda8949e91 100644 --- a/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL X2D.json +++ b/resources/profiles/BBL/filament/Bambu PETG Translucent @BBL X2D.json @@ -6,9 +6,19 @@ "setting_id": "GFSG01_20", "instantiation": "true", "fan_max_speed": [ + "60", + "60", + "60", + "60", + "60", "60" ], "fan_min_speed": [ + "20", + "20", + "20", + "20", + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2C 0.4 nozzle.json index 4de1312338..72a1b3bacc 100644 --- a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2C 0.4 nozzle.json @@ -21,9 +21,13 @@ "30" ], "fan_max_speed": [ + "40", + "40", "40" ], "fan_min_speed": [ + "5", + "5", "5" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2C 0.8 nozzle.json index f327384244..c85b6dc200 100644 --- a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2C 0.8 nozzle.json @@ -21,9 +21,11 @@ "30" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "5", "5" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2C.json index fa1cab99a7..5c0f7497a8 100644 --- a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2C.json @@ -21,9 +21,13 @@ "30" ], "fan_max_speed": [ + "40", + "40", "40" ], "fan_min_speed": [ + "5", + "5", "5" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2D 0.4 nozzle.json index cf5ed3198f..161f672a53 100644 --- a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2D 0.4 nozzle.json @@ -21,9 +21,13 @@ "30" ], "fan_max_speed": [ + "40", + "40", "40" ], "fan_min_speed": [ + "5", + "5", "5" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2D 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2D 0.6 nozzle.json index 347a56d4bf..54feeaf911 100644 --- a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2D 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2D 0.6 nozzle.json @@ -21,9 +21,13 @@ "30" ], "fan_max_speed": [ + "40", + "40", "40" ], "fan_min_speed": [ + "5", + "5", "5" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2D 0.8 nozzle.json index 4ae2558fb2..a976aa1b55 100644 --- a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2D 0.8 nozzle.json @@ -9,9 +9,11 @@ "30" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "5", "5" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2DP 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2DP 0.4 nozzle.json index 93bb7ca67d..47056bd31a 100644 --- a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2DP 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2DP 0.4 nozzle.json @@ -18,9 +18,11 @@ "30" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "5", "5" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2DP 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2DP 0.6 nozzle.json index 62bc309cfa..90dd4f26cf 100644 --- a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2DP 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2DP 0.6 nozzle.json @@ -9,9 +9,11 @@ "30" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "5", "5" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2DP 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2DP 0.8 nozzle.json index 62b7d54979..4b0f63c90c 100644 --- a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2DP 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2DP 0.8 nozzle.json @@ -9,9 +9,11 @@ "30" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "5", "5" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2S 0.6 nozzle.json index 236fa4d02d..ed7f2ee8c7 100644 --- a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2S 0.6 nozzle.json @@ -21,9 +21,13 @@ "30" ], "fan_max_speed": [ + "40", + "40", "40" ], "fan_min_speed": [ + "5", + "5", "5" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2S 0.8 nozzle.json index c9ce999176..cd56208ee1 100644 --- a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2S 0.8 nozzle.json @@ -21,9 +21,11 @@ "30" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "5", "5" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2S.json index f85e5e820c..5e6d734d53 100644 --- a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL H2S.json @@ -21,9 +21,13 @@ "30" ], "fan_max_speed": [ + "40", + "40", "40" ], "fan_min_speed": [ + "5", + "5", "5" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL P2S 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL P2S 0.4 nozzle.json index f7987b06fe..731a8539c2 100644 --- a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL P2S 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL P2S 0.4 nozzle.json @@ -21,9 +21,13 @@ "30" ], "fan_max_speed": [ + "40", + "40", "40" ], "fan_min_speed": [ + "5", + "5", "5" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL P2S 0.6 nozzle.json index 3958df13ad..b6d7bcd845 100644 --- a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL P2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL P2S 0.6 nozzle.json @@ -21,9 +21,13 @@ "30" ], "fan_max_speed": [ + "40", + "40", "40" ], "fan_min_speed": [ + "5", + "5", "5" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL P2S 0.8 nozzle.json index 7239839b3d..80bd8b59d6 100644 --- a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL P2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL P2S 0.8 nozzle.json @@ -21,9 +21,11 @@ "30" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "5", "5" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL X1C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL X1C 0.4 nozzle.json index c136712f57..0d405a8ea8 100644 --- a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL X1C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL X1C 0.4 nozzle.json @@ -9,9 +9,11 @@ "30" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "5", "5" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL X1C.json b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL X1C.json index e953110fe0..6e926af484 100644 --- a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL X1C.json +++ b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL X1C.json @@ -9,9 +9,11 @@ "30" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "5", "5" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL X2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL X2D 0.4 nozzle.json index 24b3775fd5..59de3f7aeb 100644 --- a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL X2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL X2D 0.4 nozzle.json @@ -21,9 +21,19 @@ "30" ], "fan_max_speed": [ + "40", + "40", + "40", + "40", + "40", "40" ], "fan_min_speed": [ + "5", + "5", + "5", + "5", + "5", "5" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL X2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL X2D 0.8 nozzle.json index 378ad209c9..fe7a25ebe5 100644 --- a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL X2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL X2D 0.8 nozzle.json @@ -21,9 +21,15 @@ "30" ], "fan_max_speed": [ + "40", + "40", + "40", "40" ], "fan_min_speed": [ + "5", + "5", + "5", "5" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL X2D.json b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL X2D.json index f6e03276e0..b9bcb45fb4 100644 --- a/resources/profiles/BBL/filament/Bambu PETG-CF @BBL X2D.json +++ b/resources/profiles/BBL/filament/Bambu PETG-CF @BBL X2D.json @@ -21,9 +21,19 @@ "30" ], "fan_max_speed": [ + "40", + "40", + "40", + "40", + "40", "40" ], "fan_min_speed": [ + "5", + "5", + "5", + "5", + "5", "5" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Aero @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Aero @BBL H2C 0.4 nozzle.json index ed720d6732..ad7ecb17d9 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Aero @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Aero @BBL H2C 0.4 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA11_11", "instantiation": "true", "additional_cooling_fan_speed": [ + "0", + "0", "0" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Aero @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PLA Aero @BBL H2C.json index 0d5865af10..f89fdd8bcb 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Aero @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PLA Aero @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSA11_12", "instantiation": "true", "additional_cooling_fan_speed": [ + "0", + "0", "0" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Aero @BBL H2D.json b/resources/profiles/BBL/filament/Bambu PLA Aero @BBL H2D.json index 43a47a4722..5b6a9e0f5d 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Aero @BBL H2D.json +++ b/resources/profiles/BBL/filament/Bambu PLA Aero @BBL H2D.json @@ -6,6 +6,8 @@ "setting_id": "GFSA11_05", "instantiation": "true", "additional_cooling_fan_speed": [ + "0", + "0", "0" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Aero @BBL H2DP.json b/resources/profiles/BBL/filament/Bambu PLA Aero @BBL H2DP.json index 4f30395743..71c62a47b2 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Aero @BBL H2DP.json +++ b/resources/profiles/BBL/filament/Bambu PLA Aero @BBL H2DP.json @@ -6,6 +6,7 @@ "setting_id": "GFSA11_08", "instantiation": "true", "additional_cooling_fan_speed": [ + "0", "0" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Aero @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PLA Aero @BBL H2S.json index 3a651c0105..d77572787e 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Aero @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Aero @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSA11_07", "instantiation": "true", "additional_cooling_fan_speed": [ + "0", + "0", "0" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Aero @BBL P2S.json b/resources/profiles/BBL/filament/Bambu PLA Aero @BBL P2S.json index f2ea01ad72..56ac92382a 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Aero @BBL P2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Aero @BBL P2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSA11_06", "instantiation": "true", "additional_cooling_fan_speed": [ + "0", + "0", "0" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Aero @BBL X1.json b/resources/profiles/BBL/filament/Bambu PLA Aero @BBL X1.json index 9e5ac03bf3..9a42cebace 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Aero @BBL X1.json +++ b/resources/profiles/BBL/filament/Bambu PLA Aero @BBL X1.json @@ -6,6 +6,7 @@ "setting_id": "GFSA11_00", "instantiation": "true", "additional_cooling_fan_speed": [ + "0", "0" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Aero @BBL X1C.json b/resources/profiles/BBL/filament/Bambu PLA Aero @BBL X1C.json index 9fe7a958e1..9e7e06ad66 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Aero @BBL X1C.json +++ b/resources/profiles/BBL/filament/Bambu PLA Aero @BBL X1C.json @@ -6,6 +6,7 @@ "setting_id": "GFSA11_01", "instantiation": "true", "additional_cooling_fan_speed": [ + "0", "0" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Aero @BBL X2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Aero @BBL X2D 0.4 nozzle.json index 40f718ed86..dbd40cffc8 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Aero @BBL X2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Aero @BBL X2D 0.4 nozzle.json @@ -6,6 +6,11 @@ "setting_id": "GFSA11_09", "instantiation": "true", "additional_cooling_fan_speed": [ + "0", + "0", + "0", + "0", + "0", "0" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Aero @BBL X2D.json b/resources/profiles/BBL/filament/Bambu PLA Aero @BBL X2D.json index d3472e210f..4c08f1619f 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Aero @BBL X2D.json +++ b/resources/profiles/BBL/filament/Bambu PLA Aero @BBL X2D.json @@ -6,6 +6,11 @@ "setting_id": "GFSA11_10", "instantiation": "true", "additional_cooling_fan_speed": [ + "0", + "0", + "0", + "0", + "0", "0" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2C 0.2 nozzle.json index 07ee98f7e6..1466508a02 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2C 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA00_23", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -27,9 +28,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2C 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2C 0.6 nozzle.json index 63a6a766b0..1905ccf9c0 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2C 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2C 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA00_31", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "counter_coef_2": [ @@ -27,9 +29,13 @@ "55" ], "fan_max_speed": [ + "80", + "80", "80" ], "fan_min_speed": [ + "60", + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2C 0.8 nozzle.json index a8f668860c..ea9d7d781f 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2C 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA00_32", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -27,9 +28,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2C.json index fb7e781c55..025c7658c3 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSA00_22", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "counter_coef_2": [ @@ -27,9 +29,13 @@ "55" ], "fan_max_speed": [ + "80", + "80", "80" ], "fan_min_speed": [ + "60", + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2D 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2D 0.2 nozzle.json index 53dfefb6b8..97bdac45bb 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2D 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2D 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA00_13", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -27,9 +28,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2D 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2D 0.6 nozzle.json index b201c04ae8..a39249eeb1 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2D 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2D 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA00_27", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "counter_coef_2": [ @@ -27,9 +29,13 @@ "55" ], "fan_max_speed": [ + "80", + "80", "80" ], "fan_min_speed": [ + "60", + "60", "60" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2D 0.8 nozzle.json index 155afbbc68..bcd8e2994d 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2D 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA00_14", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -27,9 +28,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2D.json b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2D.json index 0cb3530b9c..9f3286c359 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2D.json +++ b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2D.json @@ -6,6 +6,8 @@ "setting_id": "GFSA00_12", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "counter_coef_2": [ @@ -27,9 +29,13 @@ "55" ], "fan_max_speed": [ + "80", + "80", "80" ], "fan_min_speed": [ + "60", + "60", "60" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2DP 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2DP 0.2 nozzle.json index 9c49c6d367..07baeac758 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2DP 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2DP 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA00_18", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ @@ -15,9 +16,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2DP 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2DP 0.6 nozzle.json index 2efcf62609..8328a49d6f 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2DP 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2DP 0.6 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA00_28", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -24,9 +25,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2DP 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2DP 0.8 nozzle.json index ded3b4e3d9..b44ce50908 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2DP 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2DP 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA00_19", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ @@ -15,9 +16,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2DP.json b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2DP.json index 6df1b590b0..364a6dc643 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2DP.json +++ b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2DP.json @@ -6,6 +6,7 @@ "setting_id": "GFSA00_17", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -24,9 +25,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2S 0.2 nozzle.json index a26fae8231..25ea7ee5db 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA00_07", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -27,6 +28,7 @@ "55" ], "fan_min_speed": [ + "80", "80" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2S 0.6 nozzle.json index 5e8717b0fc..253b994f0d 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2S 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA00_30", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "counter_coef_2": [ @@ -27,6 +29,8 @@ "55" ], "fan_min_speed": [ + "80", + "80", "80" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2S 0.8 nozzle.json index fcfed64523..c1f368c66a 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2S 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA00_29", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -27,6 +28,7 @@ "55" ], "fan_min_speed": [ + "80", "80" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2S.json index 2b26b5a33b..fd525c42ca 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Basic @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSA00_05", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "counter_coef_2": [ @@ -27,6 +29,8 @@ "55" ], "fan_min_speed": [ + "80", + "80", "80" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL H2C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL H2C 0.2 nozzle.json index 31708faa3f..8936eee190 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL H2C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL H2C 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA13_25", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL H2C 0.4 nozzle.json index 203111f93d..e688214030 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL H2C 0.4 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA13_23", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL H2C.json index d6ae035131..42a99c95bb 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSA13_24", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL H2S 0.2 nozzle.json index 2e4df09afb..58caf94a5d 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA13_12", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ @@ -15,6 +16,7 @@ "55" ], "fan_min_speed": [ + "80", "80" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL H2S.json index 6bd6a008d5..7dc39f15c6 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSA13_11", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ @@ -15,6 +17,8 @@ "55" ], "fan_min_speed": [ + "80", + "80", "80" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL P1P 0.2 nozzle.json index 92b19310cb..5e8b2724c3 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL P1P 0.2 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL P1P 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL P1P 0.6 nozzle.json index 6f4d0fdbdc..a09807cbf2 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL P1P 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL P1P 0.6 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL P1P.json b/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL P1P.json index 8dd853022d..3cbae45547 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL P1P.json +++ b/resources/profiles/BBL/filament/Bambu PLA Dynamic @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL H2C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL H2C 0.2 nozzle.json index d7d9738881..90a2d75d36 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL H2C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL H2C 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA15_25", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL H2C 0.4 nozzle.json index 5fdb9b941f..98fe16b9e3 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL H2C 0.4 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA15_23", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL H2C.json index 3c4a3181fd..8152a27d3f 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSA15_24", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL H2S 0.2 nozzle.json index c9beecd89c..d8ed6ca17b 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA15_12", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ @@ -15,6 +16,7 @@ "55" ], "fan_min_speed": [ + "80", "80" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL H2S.json index 91d24f8332..eceb331c7f 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSA15_11", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ @@ -15,6 +17,8 @@ "55" ], "fan_min_speed": [ + "80", + "80", "80" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL P1P 0.2 nozzle.json index 7aa84eccd3..8967431e24 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL P1P 0.2 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL P1P 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL P1P 0.6 nozzle.json index b5d4190792..4f33e1c9ba 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL P1P 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL P1P 0.6 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL P1P.json b/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL P1P.json index 2c0bd7a5fe..949d6bdc40 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL P1P.json +++ b/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL P2S 0.2 nozzle.json index 049e3ad804..d1773e3953 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL P2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL P2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA15_15", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL P2S.json b/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL P2S.json index 52fe7324be..5a5f0e4688 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL P2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Galaxy @BBL P2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSA15_14", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Glow @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Glow @BBL H2C 0.4 nozzle.json index f0601b5374..018bcd48fe 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Glow @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Glow @BBL H2C 0.4 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA12_23", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Glow @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PLA Glow @BBL H2C.json index 7130e8e127..63cf2055ac 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Glow @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PLA Glow @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSA12_24", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Glow @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PLA Glow @BBL H2S.json index 4530262350..4b9feb7ea3 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Glow @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Glow @BBL H2S.json @@ -7,6 +7,8 @@ "setting_id": "GFSA12_12", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ @@ -16,6 +18,8 @@ "55" ], "fan_min_speed": [ + "80", + "80", "80" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Glow @BBL P1P 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Glow @BBL P1P 0.6 nozzle.json index 930f3a2160..0776e594b0 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Glow @BBL P1P 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Glow @BBL P1P 0.6 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Glow @BBL P1P.json b/resources/profiles/BBL/filament/Bambu PLA Glow @BBL P1P.json index 0d37c339c1..cd60db4f0a 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Glow @BBL P1P.json +++ b/resources/profiles/BBL/filament/Bambu PLA Glow @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Glow @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Glow @BBL P2S 0.2 nozzle.json index 1ac8c17a57..264f9ac893 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Glow @BBL P2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Glow @BBL P2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA12_15", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Glow @BBL P2S.json b/resources/profiles/BBL/filament/Bambu PLA Glow @BBL P2S.json index 43a636a896..d2cd72ccba 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Glow @BBL P2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Glow @BBL P2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSA12_14", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2C 0.2 nozzle.json index 938f70ef5f..f3c08d0276 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2C 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA18_17", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ @@ -15,9 +16,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2C 0.4 nozzle.json index a435869e4d..e47f6b0cfa 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2C 0.4 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA18_15", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "counter_coef_2": [ @@ -27,9 +29,13 @@ "55" ], "fan_max_speed": [ + "80", + "80", "80" ], "fan_min_speed": [ + "60", + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2C 0.8 nozzle.json index b10f90036d..c43ee9c55d 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2C 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA18_44", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -27,9 +28,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2C.json index a30e801edd..e0423ed4f4 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSA18_16", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "counter_coef_2": [ @@ -27,9 +29,13 @@ "55" ], "fan_max_speed": [ + "80", + "80", "80" ], "fan_min_speed": [ + "60", + "60", "60" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2D 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2D 0.2 nozzle.json index fcd3c66c1c..04084d28de 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2D 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2D 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA18_11", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ @@ -15,9 +16,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2D 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2D 0.6 nozzle.json index fbb2fb9994..5ab62e7c67 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2D 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2D 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA18_45", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "counter_coef_2": [ @@ -27,9 +29,13 @@ "55" ], "fan_max_speed": [ + "80", + "80", "80" ], "fan_min_speed": [ + "60", + "60", "60" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2D 0.8 nozzle.json index fb7b165418..b64c4716c8 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2D 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA18_10", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -27,9 +28,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2D.json b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2D.json index f7bcb1ad8f..fa4dd81219 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2D.json +++ b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2D.json @@ -6,6 +6,8 @@ "setting_id": "GFSA18_09", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "counter_coef_2": [ @@ -27,9 +29,13 @@ "55" ], "fan_max_speed": [ + "80", + "80", "80" ], "fan_min_speed": [ + "60", + "60", "60" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2DP 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2DP 0.2 nozzle.json index 3844c578ee..d35ffc498e 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2DP 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2DP 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA18_30", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ @@ -15,9 +16,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2DP 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2DP 0.4 nozzle.json index e2aea610ee..90df178b13 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2DP 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2DP 0.4 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA18_27", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -27,9 +28,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2DP 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2DP 0.6 nozzle.json index 758129964a..b896b19979 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2DP 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2DP 0.6 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA18_28", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -27,9 +28,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2DP 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2DP 0.8 nozzle.json index 924d3416d2..58bb62979a 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2DP 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2DP 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA18_29", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ @@ -15,9 +16,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2S 0.2 nozzle.json index c28b9c1e67..29f52784ee 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA18_19", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2S 0.6 nozzle.json index e1580f9a56..0a63f0b344 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2S 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA18_46", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2S 0.8 nozzle.json index a1435c6c6f..553ed9ea7e 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2S 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA18_47", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2S.json index da3587b191..834a9456f6 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSA18_18", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL P1P 0.2 nozzle.json index 897af5a38f..6c11a968f3 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL P1P 0.2 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL P1P 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL P1P 0.6 nozzle.json index a270dea0ab..a85ef0d269 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL P1P 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL P1P 0.6 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL P1P.json b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL P1P.json index e7a9400504..d1eaa7e721 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Lite @BBL P1P.json +++ b/resources/profiles/BBL/filament/Bambu PLA Lite @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Marble @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Marble @BBL H2C 0.4 nozzle.json index 1cdc32a534..3d1e35961b 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Marble @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Marble @BBL H2C 0.4 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA07_11", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Marble @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PLA Marble @BBL H2C.json index 6840297c89..b20ddba103 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Marble @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PLA Marble @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSA07_12", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Marble @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PLA Marble @BBL H2S.json index 077bbab752..bb34a9a0b7 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Marble @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Marble @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSA07_06", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ @@ -15,6 +17,8 @@ "55" ], "fan_min_speed": [ + "80", + "80", "80" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2C 0.2 nozzle.json index 8b970d07f2..52ba7791f1 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2C 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA01_23", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -27,6 +28,7 @@ "55" ], "fan_min_speed": [ + "60", "60" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2C 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2C 0.6 nozzle.json index 7ff6555cec..861d01de6c 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2C 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2C 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA01_31", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "counter_coef_2": [ @@ -27,6 +29,8 @@ "55" ], "fan_min_speed": [ + "60", + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2C 0.8 nozzle.json index 9bcd43edb4..0b325ba2e6 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2C 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA01_32", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -27,6 +28,7 @@ "55" ], "fan_min_speed": [ + "60", "60" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2C.json index 06643057f8..ace8143221 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSA01_22", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "counter_coef_2": [ @@ -27,6 +29,8 @@ "55" ], "fan_min_speed": [ + "60", + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2D 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2D 0.2 nozzle.json index b190108aed..b432c42994 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2D 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2D 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA01_13", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -27,9 +28,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2D 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2D 0.6 nozzle.json index be51abe9d3..bed53a5d5f 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2D 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2D 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA01_27", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "counter_coef_2": [ @@ -27,9 +29,13 @@ "55" ], "fan_max_speed": [ + "80", + "80", "80" ], "fan_min_speed": [ + "60", + "60", "60" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2D 0.8 nozzle.json index 3ada24f209..07f10c4332 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2D 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA01_14", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -27,9 +28,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2D.json b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2D.json index 2a0b55932e..c78634c8a8 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2D.json +++ b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2D.json @@ -6,6 +6,8 @@ "setting_id": "GFSA01_05", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "counter_coef_2": [ @@ -27,9 +29,13 @@ "55" ], "fan_max_speed": [ + "80", + "80", "80" ], "fan_min_speed": [ + "60", + "60", "60" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2DP 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2DP 0.2 nozzle.json index f69cf85eca..41eb682f5a 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2DP 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2DP 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA01_18", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ @@ -15,9 +16,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2DP 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2DP 0.6 nozzle.json index 477bf3492a..e213adcb5b 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2DP 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2DP 0.6 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA01_28", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -27,9 +28,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2DP 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2DP 0.8 nozzle.json index 85a1fbcf4f..cec1466727 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2DP 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2DP 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA01_19", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ @@ -15,9 +16,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2DP.json b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2DP.json index 08a123c6c4..ba91002588 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2DP.json +++ b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2DP.json @@ -6,6 +6,7 @@ "setting_id": "GFSA01_17", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -27,9 +28,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2S 0.2 nozzle.json index eeea4e7a78..131148ccae 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA01_08", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2S 0.6 nozzle.json index 6c611715da..390537664d 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2S 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA01_30", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2S 0.8 nozzle.json index 0c38e3d228..309723835c 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2S 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA01_29", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2S.json index f794e7e0f5..fcfee24f76 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSA01_07", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL P2S 0.2 nozzle.json index 7af0bca0ae..e7eaf4e62f 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL P2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL P2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA01_15", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL P2S 0.6 nozzle.json index 8845412d00..62ddb12047 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL P2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL P2S 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA01_26", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL P2S 0.8 nozzle.json index 369c15e2cd..0a04e30c0f 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL P2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL P2S 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA01_16", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL P2S.json b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL P2S.json index 85051a2210..079b580168 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Matte @BBL P2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Matte @BBL P2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSA01_12", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Metal @BBL H2C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Metal @BBL H2C 0.2 nozzle.json index d500051bb1..ee08893a69 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Metal @BBL H2C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Metal @BBL H2C 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA02_19", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Metal @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Metal @BBL H2C 0.4 nozzle.json index 75b221dcd3..6d9f35a444 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Metal @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Metal @BBL H2C 0.4 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA02_20", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Metal @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PLA Metal @BBL H2C.json index 1ec6730b1a..a061873ce1 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Metal @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PLA Metal @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSA02_21", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Metal @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Metal @BBL H2S 0.2 nozzle.json index e437ccd9f9..535253427f 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Metal @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Metal @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA02_09", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Metal @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PLA Metal @BBL H2S.json index 9c5648425d..b09fb08051 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Metal @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Metal @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSA02_08", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Metal @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Metal @BBL P2S 0.2 nozzle.json index d178683ef1..7c878d42bc 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Metal @BBL P2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Metal @BBL P2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA02_12", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Metal @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Metal @BBL P2S 0.6 nozzle.json index 7c26b3e980..37e8e36af1 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Metal @BBL P2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Metal @BBL P2S 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA02_24", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Metal @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Metal @BBL P2S 0.8 nozzle.json index 8211ae0b54..e875278247 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Metal @BBL P2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Metal @BBL P2S 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA02_25", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2C 0.2 nozzle.json index 2fce1d0a33..5a3bbcbc90 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2C 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA19_09", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2C 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2C 0.6 nozzle.json index 54866db466..ff6d82be48 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2C 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2C 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA19_42", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2C 0.8 nozzle.json index 88ab702e3e..bfd283ae88 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2C 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA19_43", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2C.json index 8a01b018c3..cc7b2051af 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSA19_10", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2D 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2D 0.2 nozzle.json index e30e6203b4..0071b46427 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2D 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2D 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA19_11", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2D 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2D 0.6 nozzle.json index 8126977f4b..b6c9a22b07 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2D 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2D 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA19_44", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2D 0.8 nozzle.json index aa981c5177..7a0e25ccb4 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2D 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA19_12", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2D.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2D.json index 002589887f..9f7d65ea19 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2D.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2D.json @@ -6,6 +6,8 @@ "setting_id": "GFSA19_13", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2DP 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2DP 0.2 nozzle.json index 27c1be9d5f..c78968507b 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2DP 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2DP 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA19_14", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2DP 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2DP 0.6 nozzle.json index 66a86f93ae..88074952fe 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2DP 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2DP 0.6 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA19_45", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2DP 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2DP 0.8 nozzle.json index dc62f9cecc..f3cf3e9800 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2DP 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2DP 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA19_15", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2DP.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2DP.json index 516cb30a90..d019971078 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2DP.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2DP.json @@ -6,6 +6,7 @@ "setting_id": "GFSA19_16", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2S 0.2 nozzle.json index 1206f03c01..25266ec00f 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA19_17", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2S 0.6 nozzle.json index 9a14fb210c..8f69445623 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2S 0.6 nozzle.json @@ -7,6 +7,8 @@ "instantiation": "true", "description": "The generic presets are conservatively tuned for compatibility with a wider range of filaments. For higher printing quality and speeds, please use Bambu filaments with Bambu presets.", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2S 0.8 nozzle.json index a780734dc8..23d1564a08 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2S 0.8 nozzle.json @@ -7,6 +7,7 @@ "instantiation": "true", "description": "The generic presets are conservatively tuned for compatibility with a wider range of filaments. For higher printing quality and speeds, please use Bambu filaments with Bambu presets.", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2S.json index a3ce5df6ea..71d49c9453 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL H2S.json @@ -7,6 +7,8 @@ "instantiation": "true", "description": "The generic presets are conservatively tuned for compatibility with a wider range of filaments. For higher printing quality and speeds, please use Bambu filaments with Bambu presets.", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P1P 0.2 nozzle.json index 9c77055180..eae13723b6 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P1P 0.2 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "60", "60" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P1P 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P1P 0.8 nozzle.json index 18d8fd4a8a..b2bc0cb4f6 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P1P 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P1P 0.8 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "60", "60" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P1P.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P1P.json index ff2ff52185..f4147158b6 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P1P.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "60", "60" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P2S 0.2 nozzle.json index 053c0cc82a..f81a615252 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA19_26", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P2S 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P2S 0.4 nozzle.json index cf2c8c5584..37529fde6f 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P2S 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P2S 0.4 nozzle.json @@ -7,6 +7,8 @@ "instantiation": "true", "description": "The generic presets are conservatively tuned for compatibility with a wider range of filaments. For higher printing quality and speeds, please use Bambu filaments with Bambu presets.", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P2S 0.6 nozzle.json index 081ae17a81..0c7542dc06 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P2S 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA19_28", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P2S 0.8 nozzle.json index 51b2957e47..d727ad3f43 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL P2S 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA19_29", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL X2D 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL X2D 0.2 nozzle.json index 32120fd0c8..b945c2ed24 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL X2D 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL X2D 0.2 nozzle.json @@ -6,6 +6,9 @@ "setting_id": "GFSA19_33", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL X2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL X2D 0.4 nozzle.json index b6079ea5d2..b37d3bbdd3 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL X2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL X2D 0.4 nozzle.json @@ -7,6 +7,11 @@ "instantiation": "true", "description": "The generic presets are conservatively tuned for compatibility with a wider range of filaments. For higher printing quality and speeds, please use Bambu filaments with Bambu presets.", "additional_cooling_fan_speed": [ + "75", + "75", + "75", + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL X2D 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL X2D 0.6 nozzle.json index 358b0ff624..dc8a1569ab 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL X2D 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL X2D 0.6 nozzle.json @@ -6,6 +6,11 @@ "setting_id": "GFSA19_35", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", + "75", + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL X2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL X2D 0.8 nozzle.json index 73a363cfab..5eb09d1b12 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Pure @BBL X2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Pure @BBL X2D 0.8 nozzle.json @@ -6,6 +6,9 @@ "setting_id": "GFSA19_36", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2C 0.2 nozzle.json index 5f6ac11da9..082074f8be 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2C 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA05_21", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2C 0.4 nozzle.json index d738fa5afd..2ea73c7c6e 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2C 0.4 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA05_22", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2C.json index e690d6b168..81ba2dbba1 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSA05_23", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2D 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2D 0.2 nozzle.json index c14f4c2f2b..b33bf9d81e 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2D 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2D 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA05_07", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2D.json b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2D.json index 991a51de13..7fc75dc921 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2D.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2D.json @@ -6,6 +6,8 @@ "setting_id": "GFSA05_16", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2DP 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2DP 0.2 nozzle.json index 7a619beb01..2642126fd7 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2DP 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2DP 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA05_18", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2DP.json b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2DP.json index 3a26b9d69e..b0795534f7 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2DP.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2DP.json @@ -6,6 +6,7 @@ "setting_id": "GFSA05_19", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2S 0.2 nozzle.json index ad5e9778c7..6f32b716f0 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA05_09", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2S.json index 6221528724..3b3737d286 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSA05_08", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL P2S 0.2 nozzle.json index 3600a41bbb..caafdde46c 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL P2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL P2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA05_15", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL P2S 0.6 nozzle.json index 71b9f5522b..207d52231d 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL P2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL P2S 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA05_26", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL P2S 0.8 nozzle.json index 95374a8d59..3ab3db3646 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL P2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL P2S 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA05_27", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL P2S.json b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL P2S.json index a650da3329..bcd959725c 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk @BBL P2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk @BBL P2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSA05_17", "instantiation": "true", "additional_cooling_fan_speed": [ + "80", + "80", "80" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2C 0.2 nozzle.json index 6d0f939e60..36f37b26ef 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2C 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA06_20", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2C 0.4 nozzle.json index 40b37f32b9..ee9d9b4098 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2C 0.4 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA06_18", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2C 0.8 nozzle.json index 4d460cde49..f51d644ad9 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2C 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA06_40", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2C.json index e3a787b51c..8da4bf0b6b 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSA06_19", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2D 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2D 0.2 nozzle.json index 80791f03c7..45d7f7b13c 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2D 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2D 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA06_10", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2D 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2D 0.6 nozzle.json index 8c97969bf2..b6deafebea 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2D 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2D 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA06_41", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2D 0.8 nozzle.json index f8508cd0f9..e9a49ec749 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2D 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA06_42", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2D.json b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2D.json index a1e9d5a6a9..25af3f5fa5 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2D.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2D.json @@ -6,6 +6,8 @@ "setting_id": "GFSA06_09", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2DP 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2DP 0.2 nozzle.json index 6ef42bcf5f..fa1a9ebcdc 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2DP 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2DP 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA06_14", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2DP 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2DP 0.6 nozzle.json index acb22ba23a..fe2e4f047a 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2DP 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2DP 0.6 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA06_43", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2DP 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2DP 0.8 nozzle.json index a4b9e799a4..ff86fe004f 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2DP 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2DP 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA06_44", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2DP.json b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2DP.json index cba3b52ae0..a56401ff23 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2DP.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2DP.json @@ -6,6 +6,7 @@ "setting_id": "GFSA06_13", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2S 0.2 nozzle.json index b1bf42971c..756829d4df 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA06_22", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2S 0.6 nozzle.json index 8868530a19..abdd991834 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2S 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA06_45", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2S 0.8 nozzle.json index 7e3270c32a..af15bde51b 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2S 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA06_46", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2S.json index 7d143d2360..9543ecc828 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSA06_21", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL P2S 0.2 nozzle.json index 05f3286f11..fee682b69f 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL P2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL P2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA06_12", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL P2S.json b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL P2S.json index 027b694a64..4bc7d8b4e6 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL P2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Silk+ @BBL P2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSA06_11", "instantiation": "true", "additional_cooling_fan_speed": [ + "80", + "80", "80" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Sparkle @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Sparkle @BBL H2C 0.4 nozzle.json index d635be71a7..85141b91ff 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Sparkle @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Sparkle @BBL H2C 0.4 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA08_11", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Sparkle @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PLA Sparkle @BBL H2C.json index 59f9880889..5223c79445 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Sparkle @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PLA Sparkle @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSA08_12", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Sparkle @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PLA Sparkle @BBL H2S.json index 53ce3da06c..01582d9f28 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Sparkle @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Sparkle @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSA08_06", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ @@ -15,6 +17,8 @@ "55" ], "fan_min_speed": [ + "80", + "80", "80" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Sparkle @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Sparkle @BBL P2S 0.6 nozzle.json index 8368e4c558..3696f420aa 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Sparkle @BBL P2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Sparkle @BBL P2S 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA08_14", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Tough @BBL H2C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Tough @BBL H2C 0.2 nozzle.json index afc18f86e9..3d77b54d7f 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Tough @BBL H2C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Tough @BBL H2C 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA09_22", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Tough @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Tough @BBL H2C 0.4 nozzle.json index 395e917d6d..1d4ad13d8b 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Tough @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Tough @BBL H2C 0.4 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA09_20", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Tough @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PLA Tough @BBL H2C.json index 687135999b..20c96446c3 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Tough @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PLA Tough @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSA09_21", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "counter_coef_2": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Tough @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Tough @BBL H2S 0.2 nozzle.json index 935d868854..0a3f3ac5a4 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Tough @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Tough @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA09_12", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ @@ -15,6 +16,7 @@ "55" ], "fan_min_speed": [ + "80", "80" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Tough @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PLA Tough @BBL H2S.json index 0f202955c9..3fb79ac36b 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Tough @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Tough @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSA09_09", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ @@ -15,6 +17,8 @@ "55" ], "fan_min_speed": [ + "80", + "80", "80" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Tough+ @BBL X1.json b/resources/profiles/BBL/filament/Bambu PLA Tough+ @BBL X1.json index 80e7689736..ed66381b35 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Tough+ @BBL X1.json +++ b/resources/profiles/BBL/filament/Bambu PLA Tough+ @BBL X1.json @@ -6,12 +6,14 @@ "setting_id": "GFSA10_29", "instantiation": "true", "additional_cooling_fan_speed": [ + "70", "70" ], "fan_cooling_layer_time": [ "80" ], "fan_min_speed": [ + "90", "90" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2C 0.2 nozzle.json index 743a5d09aa..160791f8fe 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2C 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA17_30", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ @@ -65,6 +66,7 @@ "fdm_filament_template_direct_dual" ], "nozzle_temperature_range_low": [ + "200", "200" ], "nozzle_temperature": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2C 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2C 0.6 nozzle.json index bf8d33de15..55f6e27d6a 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2C 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2C 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA17_43", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ @@ -115,6 +117,8 @@ "220" ], "nozzle_temperature_range_low": [ + "200", + "200", "200" ], "pre_start_fan_time": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2C 0.8 nozzle.json index 7c19dbe1a7..406cabd416 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2C 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA17_44", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ @@ -101,6 +102,7 @@ "220" ], "nozzle_temperature_range_low": [ + "200", "200" ], "pre_start_fan_time": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2C.json index 6a059d251b..d2de2598db 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSA17_29", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ @@ -103,6 +105,8 @@ "220" ], "nozzle_temperature_range_low": [ + "200", + "200", "200" ], "pre_start_fan_time": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2D 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2D 0.6 nozzle.json index 595316f8b8..d9be5d6c45 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2D 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2D 0.6 nozzle.json @@ -82,6 +82,8 @@ "220" ], "nozzle_temperature_range_low": [ + "200", + "200", "200" ], "slow_down_min_speed": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2DP 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2DP 0.6 nozzle.json index d4e2e52372..23c3a5de4a 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2DP 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2DP 0.6 nozzle.json @@ -73,6 +73,7 @@ "220" ], "nozzle_temperature_range_low": [ + "200", "200" ], "slow_down_min_speed": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2S 0.2 nozzle.json index 8ee80929b7..1bbe592a2d 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA17_16", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ @@ -48,6 +49,7 @@ "0" ], "nozzle_temperature_range_low": [ + "200", "200" ], "nozzle_temperature": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2S 0.6 nozzle.json index 264712fbc9..7d82407ece 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2S 0.6 nozzle.json @@ -7,6 +7,8 @@ "instantiation": "true", "description": "The generic presets are conservatively tuned for compatibility with a wider range of filaments. For higher printing quality and speeds, please use Bambu filaments with Bambu presets.", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ @@ -86,6 +88,8 @@ "220" ], "nozzle_temperature_range_low": [ + "200", + "200", "200" ], "pre_start_fan_time": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2S 0.8 nozzle.json index 398d122abe..3091014ddf 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2S 0.8 nozzle.json @@ -7,6 +7,7 @@ "instantiation": "true", "description": "The generic presets are conservatively tuned for compatibility with a wider range of filaments. For higher printing quality and speeds, please use Bambu filaments with Bambu presets.", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ @@ -75,6 +76,7 @@ "220" ], "nozzle_temperature_range_low": [ + "200", "200" ], "pre_start_fan_time": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2S.json index 5a1f621995..f041cc80c5 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL H2S.json @@ -7,6 +7,8 @@ "instantiation": "true", "description": "The generic presets are conservatively tuned for compatibility with a wider range of filaments. For higher printing quality and speeds, please use Bambu filaments with Bambu presets.", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ @@ -80,6 +82,8 @@ "220" ], "nozzle_temperature_range_low": [ + "200", + "200", "200" ], "pre_start_fan_time": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P1P 0.2 nozzle.json index 00bc9dac11..72d39255e4 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P1P 0.2 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ @@ -41,6 +42,7 @@ "0" ], "nozzle_temperature_range_low": [ + "200", "200" ], "nozzle_temperature": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P1P 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P1P 0.8 nozzle.json index 670d60514e..a79b0e1316 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P1P 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P1P 0.8 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ @@ -41,6 +42,7 @@ "0" ], "nozzle_temperature_range_low": [ + "200", "200" ], "nozzle_temperature": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P1P.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P1P.json index 7ae191d7af..a6017bd39d 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P1P.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ @@ -41,6 +42,7 @@ "0" ], "nozzle_temperature_range_low": [ + "200", "200" ], "nozzle_temperature": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P2S 0.2 nozzle.json index f4d864f29e..5fee15f5e0 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSA17_24", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ @@ -66,6 +67,7 @@ "240" ], "nozzle_temperature_range_low": [ + "200", "200" ], "retraction_distances_when_ec": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P2S 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P2S 0.4 nozzle.json index c09cce8eaf..a5902e29e6 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P2S 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P2S 0.4 nozzle.json @@ -7,6 +7,8 @@ "instantiation": "true", "description": "The generic presets are conservatively tuned for compatibility with a wider range of filaments. For higher printing quality and speeds, please use Bambu filaments with Bambu presets.", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ @@ -59,6 +61,8 @@ "220" ], "nozzle_temperature_range_low": [ + "200", + "200", "200" ], "retraction_distances_when_ec": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P2S 0.6 nozzle.json index 2725f38791..d49dceff5e 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P2S 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA17_22", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ @@ -68,6 +70,8 @@ "220" ], "nozzle_temperature_range_low": [ + "200", + "200", "200" ], "retraction_distances_when_ec": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P2S 0.8 nozzle.json index fda701016e..0099579353 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL P2S 0.8 nozzle.json @@ -51,6 +51,7 @@ "220" ], "nozzle_temperature_range_low": [ + "200", "200" ], "retraction_distances_when_ec": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X1C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X1C 0.2 nozzle.json index 56c2cb1916..749eb63e59 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X1C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X1C 0.2 nozzle.json @@ -29,6 +29,7 @@ "0" ], "nozzle_temperature_range_low": [ + "200", "200" ], "nozzle_temperature": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X1C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X1C 0.8 nozzle.json index 41f54caeb0..bcb3002712 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X1C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X1C 0.8 nozzle.json @@ -29,6 +29,7 @@ "0" ], "nozzle_temperature_range_low": [ + "200", "200" ], "nozzle_temperature": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X1C.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X1C.json index cfe114c228..cb52e670ff 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X1C.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X1C.json @@ -30,6 +30,7 @@ "0" ], "nozzle_temperature_range_low": [ + "200", "200" ], "nozzle_temperature": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X2D 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X2D 0.2 nozzle.json index 9ff9f3c7c9..f6b9163be8 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X2D 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X2D 0.2 nozzle.json @@ -108,6 +108,9 @@ "220" ], "nozzle_temperature_range_low": [ + "200", + "200", + "200", "200" ], "retraction_distances_when_ec": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X2D 0.4 nozzle.json index 8fbd944596..828f755d23 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X2D 0.4 nozzle.json @@ -115,6 +115,11 @@ "220" ], "nozzle_temperature_range_low": [ + "200", + "200", + "200", + "200", + "200", "200" ], "retraction_distances_when_ec": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X2D 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X2D 0.6 nozzle.json index 2efa9652d0..6219086298 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X2D 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X2D 0.6 nozzle.json @@ -114,6 +114,11 @@ "220" ], "nozzle_temperature_range_low": [ + "200", + "200", + "200", + "200", + "200", "200" ], "retraction_distances_when_ec": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X2D 0.8 nozzle.json index e8bba0ada4..27f1d0ada2 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Translucent @BBL X2D 0.8 nozzle.json @@ -90,6 +90,9 @@ "220" ], "nozzle_temperature_range_low": [ + "200", + "200", + "200", "200" ], "retraction_distances_when_ec": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Wood @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Wood @BBL H2C 0.4 nozzle.json index 2165790d3a..88e688db95 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Wood @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Wood @BBL H2C 0.4 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSA16_14", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Wood @BBL H2C.json b/resources/profiles/BBL/filament/Bambu PLA Wood @BBL H2C.json index 1d718aa068..715c72353f 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Wood @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu PLA Wood @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSA16_15", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Wood @BBL H2S.json b/resources/profiles/BBL/filament/Bambu PLA Wood @BBL H2S.json index 8d45019fdd..746c38b10d 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Wood @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu PLA Wood @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSA16_16", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ @@ -15,6 +17,8 @@ "55" ], "fan_min_speed": [ + "80", + "80", "80" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Wood @BBL P1P 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu PLA Wood @BBL P1P 0.6 nozzle.json index abb60dfc31..8dcd364d90 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Wood @BBL P1P 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PLA Wood @BBL P1P 0.6 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/Bambu PLA Wood @BBL P1P.json b/resources/profiles/BBL/filament/Bambu PLA Wood @BBL P1P.json index 16208882d0..4d8785a24c 100644 --- a/resources/profiles/BBL/filament/Bambu PLA Wood @BBL P1P.json +++ b/resources/profiles/BBL/filament/Bambu PLA Wood @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/Bambu PPA-CF @BBL P2S.json b/resources/profiles/BBL/filament/Bambu PPA-CF @BBL P2S.json index 01846e0988..3a8b52097d 100644 --- a/resources/profiles/BBL/filament/Bambu PPA-CF @BBL P2S.json +++ b/resources/profiles/BBL/filament/Bambu PPA-CF @BBL P2S.json @@ -26,9 +26,13 @@ "-0.0075" ], "fan_max_speed": [ + "40", + "40", "40" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu PVA @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu PVA @BBL P1P 0.2 nozzle.json index 597e2d83ba..76ebb00b63 100644 --- a/resources/profiles/BBL/filament/Bambu PVA @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu PVA @BBL P1P 0.2 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu PVA @BBL P1P.json b/resources/profiles/BBL/filament/Bambu PVA @BBL P1P.json index 4c98d516d1..1167c54593 100644 --- a/resources/profiles/BBL/filament/Bambu PVA @BBL P1P.json +++ b/resources/profiles/BBL/filament/Bambu PVA @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2C 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2C 0.2 nozzle.json index 50022ade4b..afa2910113 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2C 0.2 nozzle.json @@ -12,9 +12,11 @@ "55" ], "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2C 0.4 nozzle.json index c8b6e555c3..46e55e63ba 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2C 0.4 nozzle.json @@ -12,9 +12,13 @@ "55" ], "fan_max_speed": [ + "30", + "30", "30" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2C 0.8 nozzle.json index 6186fbbfb1..7e9e769bec 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2C 0.8 nozzle.json @@ -12,9 +12,11 @@ "55" ], "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2C.json b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2C.json index ccdcb88f8b..0300745302 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2C.json @@ -12,9 +12,13 @@ "55" ], "fan_max_speed": [ + "30", + "30", "30" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2D 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2D 0.2 nozzle.json index 9ffdfdde10..2701b25584 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2D 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2D 0.2 nozzle.json @@ -12,9 +12,11 @@ "55" ], "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2D 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2D 0.6 nozzle.json index 0157469f7b..9a66fa6176 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2D 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2D 0.6 nozzle.json @@ -12,9 +12,13 @@ "55" ], "fan_max_speed": [ + "30", + "30", "30" ], "fan_min_speed": [ + "20", + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2D 0.8 nozzle.json index 18f993668b..4eda30ff3e 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2D 0.8 nozzle.json @@ -12,9 +12,11 @@ "55" ], "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2D.json b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2D.json index 224744fbe2..6a0624836e 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2D.json +++ b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2D.json @@ -12,9 +12,13 @@ "55" ], "fan_max_speed": [ + "15", + "15", "15" ], "fan_min_speed": [ + "10", + "10", "10" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2DP 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2DP 0.2 nozzle.json index 6d3d64f4e4..5970e18cdf 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2DP 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2DP 0.2 nozzle.json @@ -12,9 +12,11 @@ "55" ], "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2DP 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2DP 0.6 nozzle.json index 96927d4949..d24ff505b5 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2DP 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2DP 0.6 nozzle.json @@ -12,9 +12,11 @@ "55" ], "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2DP 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2DP 0.8 nozzle.json index 1790f3dc7e..5584bbb12d 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2DP 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2DP 0.8 nozzle.json @@ -12,9 +12,11 @@ "55" ], "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2DP.json b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2DP.json index ddf96b9502..6c61a92acc 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2DP.json +++ b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2DP.json @@ -12,9 +12,11 @@ "55" ], "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2S 0.2 nozzle.json index c3bc4b262f..112de8a48e 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSS02_09", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ @@ -15,9 +16,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2S 0.6 nozzle.json index e1a5cabe18..f6f0016403 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2S 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSS02_39", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ @@ -15,9 +17,13 @@ "55" ], "fan_max_speed": [ + "80", + "80", "80" ], "fan_min_speed": [ + "60", + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2S 0.8 nozzle.json index 3c6c49d850..71e743880c 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2S 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSS02_40", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ @@ -15,9 +16,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2S.json b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2S.json index 9a65ce122d..9ff211ad6f 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSS02_08", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ @@ -15,9 +17,13 @@ "55" ], "fan_max_speed": [ + "80", + "80", "80" ], "fan_min_speed": [ + "60", + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL P2S 0.2 nozzle.json index 67d1b7513c..2b7667a143 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL P2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL P2S 0.2 nozzle.json @@ -12,9 +12,11 @@ "55" ], "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL X2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL X2D 0.4 nozzle.json index f6241c25d3..3dddf75456 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PLA @BBL X2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu Support For PLA @BBL X2D 0.4 nozzle.json @@ -12,9 +12,19 @@ "55" ], "fan_max_speed": [ + "30", + "30", + "30", + "30", + "30", "30" ], "fan_min_speed": [ + "20", + "20", + "20", + "20", + "20", "20" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA-PETG @BBL H2D.json b/resources/profiles/BBL/filament/Bambu Support For PLA-PETG @BBL H2D.json index 658e24aee0..2af3930c39 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PLA-PETG @BBL H2D.json +++ b/resources/profiles/BBL/filament/Bambu Support For PLA-PETG @BBL H2D.json @@ -12,9 +12,13 @@ "60" ], "fan_max_speed": [ + "15", + "15", "15" ], "fan_min_speed": [ + "10", + "10", "10" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu Support W @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Bambu Support W @BBL H2S 0.2 nozzle.json index 0c9ad96894..312df41753 100644 --- a/resources/profiles/BBL/filament/Bambu Support W @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu Support W @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSS00_08", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ @@ -15,9 +16,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu Support W @BBL H2S.json b/resources/profiles/BBL/filament/Bambu Support W @BBL H2S.json index e1bb915584..2a9ca56c04 100644 --- a/resources/profiles/BBL/filament/Bambu Support W @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu Support W @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSS00_07", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ @@ -15,9 +17,13 @@ "55" ], "fan_max_speed": [ + "80", + "80", "80" ], "fan_min_speed": [ + "60", + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2C.json b/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2C.json index 50da178634..ec735c5deb 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSU04_05", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ @@ -115,6 +117,8 @@ "fdm_filament_template_direct_dual_e3d" ], "nozzle_temperature_range_high": [ + "250", + "250", "250" ], "nozzle_temperature": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2D.json b/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2D.json index a1c364a9ec..b0f686e2b3 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2D.json +++ b/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2D.json @@ -6,6 +6,9 @@ "setting_id": "GFSU04_02", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", + "100", "100" ], "eng_plate_temp": [ @@ -252,6 +255,9 @@ "225" ], "nozzle_temperature_range_high": [ + "250", + "250", + "250", "250" ], "override_process_overhang_speed": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2DP.json b/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2DP.json index 5afdd55df6..de3bccb27b 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2DP.json +++ b/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2DP.json @@ -6,6 +6,8 @@ "setting_id": "GFSU04_03", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ @@ -214,6 +216,8 @@ "235" ], "nozzle_temperature_range_high": [ + "250", + "250", "250" ], "override_process_overhang_speed": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2S 0.6 nozzle.json index 493fb2004d..c3766b23b3 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2S 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSU04_8", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2S 0.8 nozzle.json index b39cbdfe34..9585ab1101 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2S 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSU04_9", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2S.json b/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2S.json index 32401c7021..403197ee60 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu TPU 85A @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSU04_07", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2C 0.4 nozzle.json index 9a41e06a29..9e63a28406 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2C 0.4 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSU03_08", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ @@ -112,6 +114,8 @@ "fdm_filament_template_direct_dual_e3d" ], "nozzle_temperature_range_high": [ + "250", + "250", "250" ], "nozzle_temperature": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2C.json b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2C.json index 28f6778d20..2191881728 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSU03_09", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ @@ -112,6 +114,8 @@ "fdm_filament_template_direct_dual_e3d" ], "nozzle_temperature_range_high": [ + "250", + "250", "250" ], "nozzle_temperature": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2D 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2D 0.6 nozzle.json index feb9db8a13..dacefeec95 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2D 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2D 0.6 nozzle.json @@ -6,6 +6,9 @@ "setting_id": "GFSU03_24", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", + "100", "100" ], "eng_plate_temp": [ @@ -240,6 +243,9 @@ "1" ], "nozzle_temperature_range_high": [ + "250", + "250", + "250", "250" ], "nozzle_temperature": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2D 0.8 nozzle.json index b1a45e6d9a..6615572218 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2D 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSU03_26", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ @@ -72,6 +73,7 @@ "220" ], "nozzle_temperature_range_high": [ + "250", "250" ], "retraction_distances_when_ec": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2D.json b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2D.json index f2d39bc05c..24ca1c2dca 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2D.json +++ b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2D.json @@ -6,6 +6,9 @@ "setting_id": "GFSU03_02", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", + "100", "100" ], "eng_plate_temp": [ @@ -252,6 +255,9 @@ "225" ], "nozzle_temperature_range_high": [ + "250", + "250", + "250", "250" ], "override_process_overhang_speed": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2DP 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2DP 0.6 nozzle.json index cb24fbdbdd..261a5ccf41 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2DP 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2DP 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSU03_25", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ @@ -214,6 +216,8 @@ "220" ], "nozzle_temperature_range_high": [ + "250", + "250", "250" ], "override_process_overhang_speed": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2DP 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2DP 0.8 nozzle.json index 6f8fc61c4e..a131ceacbe 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2DP 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2DP 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSU03_27", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ @@ -72,6 +73,7 @@ "220" ], "nozzle_temperature_range_high": [ + "250", "250" ], "retraction_distances_when_ec": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2DP.json b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2DP.json index a77efbc6d7..02871a3337 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2DP.json +++ b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2DP.json @@ -6,6 +6,8 @@ "setting_id": "GFSU03_03", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ @@ -214,6 +216,8 @@ "225" ], "nozzle_temperature_range_high": [ + "250", + "250", "250" ], "override_process_overhang_speed": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2S 0.6 nozzle.json index c56c11ff05..b365a9a185 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2S 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSU03_13", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2S 0.8 nozzle.json index d6358536a6..7dc1555cb3 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2S 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSU03_14", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2S.json b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2S.json index 0cae43b7ab..132086eef3 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSU03_11", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL P2S 0.6 nozzle.json index b294fbacc4..6a2f08a138 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL P2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL P2S 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSU03_16", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL P2S.json b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL P2S.json index 1942641481..89648b3efe 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 90A @BBL P2S.json +++ b/resources/profiles/BBL/filament/Bambu TPU 90A @BBL P2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSU03_10", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 95A @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU 95A @BBL H2C 0.4 nozzle.json index bbd0dbc809..8c480a2ff2 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 95A @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU 95A @BBL H2C 0.4 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSU01_08", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 95A @BBL H2C.json b/resources/profiles/BBL/filament/Bambu TPU 95A @BBL H2C.json index 20331454b3..54023d7e7d 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 95A @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu TPU 95A @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSU01_09", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 95A @BBL H2D.json b/resources/profiles/BBL/filament/Bambu TPU 95A @BBL H2D.json index fa3cb556e2..88c16df8f3 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 95A @BBL H2D.json +++ b/resources/profiles/BBL/filament/Bambu TPU 95A @BBL H2D.json @@ -6,6 +6,9 @@ "setting_id": "GFSU01_03", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 95A @BBL H2DP.json b/resources/profiles/BBL/filament/Bambu TPU 95A @BBL H2DP.json index b2c79b2b94..487b31709b 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 95A @BBL H2DP.json +++ b/resources/profiles/BBL/filament/Bambu TPU 95A @BBL H2DP.json @@ -6,6 +6,8 @@ "setting_id": "GFSU01_06", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 95A @BBL H2S.json b/resources/profiles/BBL/filament/Bambu TPU 95A @BBL H2S.json index af483d6b70..6ae911cb74 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 95A @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu TPU 95A @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSU01_04", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2C 0.4 nozzle.json index e482736c0e..e37518a172 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2C 0.4 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSU00_13", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2C 0.8 nozzle.json index d2368268d3..081cfb90a3 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2C 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSU00_26", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2C.json b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2C.json index 52fdb8633b..1ec5931109 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSU00_14", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2D 0.8 nozzle.json index 04662c4e58..3ce09e805e 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2D 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSU00_27", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2D.json b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2D.json index e4333a1576..08f1fda6f3 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2D.json +++ b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2D.json @@ -6,6 +6,9 @@ "setting_id": "GFSU00_08", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2DP 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2DP 0.8 nozzle.json index 68762079c0..a3eab351a5 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2DP 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2DP 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSU00_28", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2DP.json b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2DP.json index d0b0b6191f..a3eb1be1c1 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2DP.json +++ b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2DP.json @@ -6,6 +6,8 @@ "setting_id": "GFSU00_11", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2S 0.6 nozzle.json index c9afffaaf8..6f26afb875 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2S 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSU00_29", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2S 0.8 nozzle.json index bbe70e2225..e1f6f3ceab 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2S 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSU00_30", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2S.json b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2S.json index 82964bc87a..0f81463bfc 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSU00_09", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL P2S 0.6 nozzle.json index 949d9c0906..d7a54596c3 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL P2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL P2S 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSU00_31", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL P2S 0.8 nozzle.json index a2665f6707..a55b3fbcdb 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL P2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL P2S 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSU00_32", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL P2S.json b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL P2S.json index ab6761cea9..40799c1dd6 100644 --- a/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL P2S.json +++ b/resources/profiles/BBL/filament/Bambu TPU 95A HF @BBL P2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSU00_10", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2C 0.4 nozzle.json index 73f4e42208..f8a39ec2e4 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2C 0.4 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSU02_09", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ @@ -15,6 +17,8 @@ "35" ], "fan_max_speed": [ + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2C 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2C 0.8 nozzle.json index 0e54a2e1d5..77446cdc1a 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2C 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSU02_25", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ @@ -15,6 +16,7 @@ "35" ], "fan_max_speed": [ + "40", "40" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2C.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2C.json index 74253e1452..c3f47d4ffd 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2C.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSU02_10", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ @@ -15,6 +17,8 @@ "35" ], "fan_max_speed": [ + "40", + "40", "40" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2D 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2D 0.6 nozzle.json index 24405af162..4136556b4f 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2D 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2D 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSU02_26", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ @@ -15,6 +17,8 @@ "35" ], "fan_max_speed": [ + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2D 0.8 nozzle.json index cc9f65412d..0fa63dbfce 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2D 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSU02_27", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ @@ -15,6 +16,7 @@ "35" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2D.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2D.json index 22d29a0b08..29e9cbbf73 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2D.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2D.json @@ -6,6 +6,8 @@ "setting_id": "GFSU02_04", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ @@ -15,6 +17,8 @@ "35" ], "fan_max_speed": [ + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2DP 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2DP 0.6 nozzle.json index 728ec60720..66a4403a4d 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2DP 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2DP 0.6 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSU02_28", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ @@ -15,6 +16,7 @@ "35" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2DP 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2DP 0.8 nozzle.json index 405b96caa6..bc564266ef 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2DP 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2DP 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSU02_29", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ @@ -15,6 +16,7 @@ "35" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2DP.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2DP.json index 641c6bd8b2..14173055e1 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2DP.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2DP.json @@ -6,6 +6,7 @@ "setting_id": "GFSU02_06", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ @@ -15,6 +16,7 @@ "35" ], "fan_max_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2S 0.6 nozzle.json index 3791a37dfb..5d958d75df 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2S 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSU02_24", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ @@ -15,6 +17,8 @@ "35" ], "fan_max_speed": [ + "40", + "40", "40" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2S 0.8 nozzle.json index 785f6512f1..22d4b92b7a 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2S 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSU02_23", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ @@ -15,6 +16,7 @@ "35" ], "fan_max_speed": [ + "40", "40" ], "filament_change_length": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2S.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2S.json index 195742bdb0..525f771285 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2S.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSU02_11", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ @@ -15,6 +17,8 @@ "35" ], "fan_max_speed": [ + "60", + "60", "60" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL P1P 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL P1P 0.6 nozzle.json index bf9b7554f8..dde9ae028a 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL P1P 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL P1P 0.6 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSU02_16", "instantiation": "true", "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL P1P.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL P1P.json index 80d40378a7..fa0005a6f0 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL P1P.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL P1P.json @@ -6,6 +6,7 @@ "setting_id": "GFSU02_01", "instantiation": "true", "fan_max_speed": [ + "40", "40" ], "filament_retraction_length": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL P2S 0.6 nozzle.json index 4f2c364ef4..0e11fa482f 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL P2S 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL P2S 0.6 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSU02_30", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ @@ -15,9 +17,13 @@ "35" ], "fan_max_speed": [ + "60", + "60", "60" ], "fan_min_speed": [ + "30", + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL P2S 0.8 nozzle.json index fe6a92f21c..d5249e0707 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL P2S 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL P2S 0.8 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSU02_31", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ @@ -15,9 +16,11 @@ "35" ], "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL P2S.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL P2S.json index bf1249ebcb..437fd35001 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL P2S.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL P2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSU02_05", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ @@ -15,9 +17,13 @@ "35" ], "fan_max_speed": [ + "60", + "60", "60" ], "fan_min_speed": [ + "30", + "30", "30" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL X1C 0.6 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL X1C 0.6 nozzle.json index f7e164fe81..caef461097 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL X1C 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL X1C 0.6 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSU02_13", "instantiation": "true", "fan_max_speed": [ + "40", "40" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL X1C.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL X1C.json index 45b784e741..42271bba10 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL X1C.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL X1C.json @@ -6,6 +6,7 @@ "setting_id": "GFSU02_00", "instantiation": "true", "fan_max_speed": [ + "40", "40" ], "filament_retraction_length": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL X2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL X2D 0.4 nozzle.json index 696a79e84a..99183ac320 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL X2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL X2D 0.4 nozzle.json @@ -12,6 +12,11 @@ "35" ], "fan_max_speed": [ + "40", + "40", + "40", + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL X2D 0.8 nozzle.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL X2D 0.8 nozzle.json index 116476ceeb..100cfb14bc 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL X2D 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL X2D 0.8 nozzle.json @@ -12,6 +12,9 @@ "35" ], "fan_max_speed": [ + "40", + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL X2D.json b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL X2D.json index ee378baeff..02d5532acd 100644 --- a/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL X2D.json +++ b/resources/profiles/BBL/filament/Bambu TPU for AMS @BBL X2D.json @@ -12,6 +12,11 @@ "35" ], "fan_max_speed": [ + "40", + "40", + "40", + "40", + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic ABS @BBL H2C 0.2 nozzle.json b/resources/profiles/BBL/filament/Generic ABS @BBL H2C 0.2 nozzle.json index a95f8b7000..8d1a12f356 100644 --- a/resources/profiles/BBL/filament/Generic ABS @BBL H2C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Generic ABS @BBL H2C 0.2 nozzle.json @@ -9,9 +9,11 @@ "60" ], "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic ABS @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Generic ABS @BBL H2S 0.2 nozzle.json index e412d22e80..b2d460207f 100644 --- a/resources/profiles/BBL/filament/Generic ABS @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Generic ABS @BBL H2S 0.2 nozzle.json @@ -9,9 +9,11 @@ "60" ], "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "40", "40" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic PETG-CF @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Generic PETG-CF @BBL H2C 0.4 nozzle.json index e2dc6c43da..609200aaf7 100644 --- a/resources/profiles/BBL/filament/Generic PETG-CF @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Generic PETG-CF @BBL H2C 0.4 nozzle.json @@ -9,9 +9,13 @@ "30" ], "fan_max_speed": [ + "40", + "40", "40" ], "fan_min_speed": [ + "5", + "5", "5" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic PETG-CF @BBL H2C.json b/resources/profiles/BBL/filament/Generic PETG-CF @BBL H2C.json index fffb6a6e49..a8d158bf12 100644 --- a/resources/profiles/BBL/filament/Generic PETG-CF @BBL H2C.json +++ b/resources/profiles/BBL/filament/Generic PETG-CF @BBL H2C.json @@ -9,9 +9,13 @@ "30" ], "fan_max_speed": [ + "40", + "40", "40" ], "fan_min_speed": [ + "5", + "5", "5" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic PETG-CF @BBL H2D.json b/resources/profiles/BBL/filament/Generic PETG-CF @BBL H2D.json index 063d30a279..627dcf1ea7 100644 --- a/resources/profiles/BBL/filament/Generic PETG-CF @BBL H2D.json +++ b/resources/profiles/BBL/filament/Generic PETG-CF @BBL H2D.json @@ -9,9 +9,13 @@ "30" ], "fan_max_speed": [ + "40", + "40", "40" ], "fan_min_speed": [ + "5", + "5", "5" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic PETG-CF @BBL H2DP.json b/resources/profiles/BBL/filament/Generic PETG-CF @BBL H2DP.json index d0e4ff8752..4e15261404 100644 --- a/resources/profiles/BBL/filament/Generic PETG-CF @BBL H2DP.json +++ b/resources/profiles/BBL/filament/Generic PETG-CF @BBL H2DP.json @@ -9,9 +9,11 @@ "30" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "5", "5" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic PETG-CF @BBL H2S.json b/resources/profiles/BBL/filament/Generic PETG-CF @BBL H2S.json index 585f656ab1..123c23d5d7 100644 --- a/resources/profiles/BBL/filament/Generic PETG-CF @BBL H2S.json +++ b/resources/profiles/BBL/filament/Generic PETG-CF @BBL H2S.json @@ -9,9 +9,13 @@ "30" ], "fan_max_speed": [ + "40", + "40", "40" ], "fan_min_speed": [ + "5", + "5", "5" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic PETG-CF @BBL P2S.json b/resources/profiles/BBL/filament/Generic PETG-CF @BBL P2S.json index e1944cca5d..eaf44c29a4 100644 --- a/resources/profiles/BBL/filament/Generic PETG-CF @BBL P2S.json +++ b/resources/profiles/BBL/filament/Generic PETG-CF @BBL P2S.json @@ -9,9 +9,13 @@ "30" ], "fan_max_speed": [ + "40", + "40", "40" ], "fan_min_speed": [ + "5", + "5", "5" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic PETG-CF @BBL X1C.json b/resources/profiles/BBL/filament/Generic PETG-CF @BBL X1C.json index 46dda5c26d..fe45f29947 100644 --- a/resources/profiles/BBL/filament/Generic PETG-CF @BBL X1C.json +++ b/resources/profiles/BBL/filament/Generic PETG-CF @BBL X1C.json @@ -9,9 +9,11 @@ "30" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "5", "5" ], "filament_max_volumetric_speed": [ diff --git a/resources/profiles/BBL/filament/Generic PETG-CF @BBL X2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Generic PETG-CF @BBL X2D 0.4 nozzle.json index 73db99b2ce..f21d59a9ca 100644 --- a/resources/profiles/BBL/filament/Generic PETG-CF @BBL X2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Generic PETG-CF @BBL X2D 0.4 nozzle.json @@ -9,9 +9,19 @@ "30" ], "fan_max_speed": [ + "40", + "40", + "40", + "40", + "40", "40" ], "fan_min_speed": [ + "5", + "5", + "5", + "5", + "5", "5" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic PETG-CF @BBL X2D.json b/resources/profiles/BBL/filament/Generic PETG-CF @BBL X2D.json index 2309a91389..59f4b1c1f1 100644 --- a/resources/profiles/BBL/filament/Generic PETG-CF @BBL X2D.json +++ b/resources/profiles/BBL/filament/Generic PETG-CF @BBL X2D.json @@ -9,9 +9,19 @@ "30" ], "fan_max_speed": [ + "40", + "40", + "40", + "40", + "40", "40" ], "fan_min_speed": [ + "5", + "5", + "5", + "5", + "5", "5" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic PLA @BBL H2C 0.2 nozzle.json b/resources/profiles/BBL/filament/Generic PLA @BBL H2C 0.2 nozzle.json index 634dc781c4..0109d97d23 100644 --- a/resources/profiles/BBL/filament/Generic PLA @BBL H2C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Generic PLA @BBL H2C 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSL99_22", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Generic PLA @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Generic PLA @BBL H2C 0.4 nozzle.json index 9489ef729a..6d00ddb005 100644 --- a/resources/profiles/BBL/filament/Generic PLA @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Generic PLA @BBL H2C 0.4 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSL99_20", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Generic PLA @BBL H2C.json b/resources/profiles/BBL/filament/Generic PLA @BBL H2C.json index 334c18e36c..18fbaaa915 100644 --- a/resources/profiles/BBL/filament/Generic PLA @BBL H2C.json +++ b/resources/profiles/BBL/filament/Generic PLA @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSL99_21", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Generic PLA @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Generic PLA @BBL H2S 0.2 nozzle.json index c04a5a7f1d..b5ab0c7b0e 100644 --- a/resources/profiles/BBL/filament/Generic PLA @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Generic PLA @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSL99_09", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Generic PLA @BBL H2S.json b/resources/profiles/BBL/filament/Generic PLA @BBL H2S.json index d43e487d30..b02c02007f 100644 --- a/resources/profiles/BBL/filament/Generic PLA @BBL H2S.json +++ b/resources/profiles/BBL/filament/Generic PLA @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSL99_08", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Generic PLA High Speed @BBL H2C 0.2 nozzle.json b/resources/profiles/BBL/filament/Generic PLA High Speed @BBL H2C 0.2 nozzle.json index 736f9d5b73..58b1f98c68 100644 --- a/resources/profiles/BBL/filament/Generic PLA High Speed @BBL H2C 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Generic PLA High Speed @BBL H2C 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSL95_21", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Generic PLA High Speed @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Generic PLA High Speed @BBL H2C 0.4 nozzle.json index 1978b96cf3..491d03de56 100644 --- a/resources/profiles/BBL/filament/Generic PLA High Speed @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Generic PLA High Speed @BBL H2C 0.4 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSL95_19", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Generic PLA High Speed @BBL H2C.json b/resources/profiles/BBL/filament/Generic PLA High Speed @BBL H2C.json index e7083107ef..762249175f 100644 --- a/resources/profiles/BBL/filament/Generic PLA High Speed @BBL H2C.json +++ b/resources/profiles/BBL/filament/Generic PLA High Speed @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSL95_20", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Generic PLA High Speed @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Generic PLA High Speed @BBL H2S 0.2 nozzle.json index 23633f4ce4..ae178912f4 100644 --- a/resources/profiles/BBL/filament/Generic PLA High Speed @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Generic PLA High Speed @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSL95_11", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Generic PLA High Speed @BBL H2S.json b/resources/profiles/BBL/filament/Generic PLA High Speed @BBL H2S.json index 97706a18ab..42372243fc 100644 --- a/resources/profiles/BBL/filament/Generic PLA High Speed @BBL H2S.json +++ b/resources/profiles/BBL/filament/Generic PLA High Speed @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSL95_10", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Generic PLA High Speed @BBL P1P.json b/resources/profiles/BBL/filament/Generic PLA High Speed @BBL P1P.json index 9c36600779..7d40128b9b 100644 --- a/resources/profiles/BBL/filament/Generic PLA High Speed @BBL P1P.json +++ b/resources/profiles/BBL/filament/Generic PLA High Speed @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Generic PLA Silk @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Generic PLA Silk @BBL H2C 0.4 nozzle.json index c48a46337c..e4c891915d 100644 --- a/resources/profiles/BBL/filament/Generic PLA Silk @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Generic PLA Silk @BBL H2C 0.4 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSL96_08", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Generic PLA Silk @BBL H2C.json b/resources/profiles/BBL/filament/Generic PLA Silk @BBL H2C.json index 61201273f0..f3a82bdd05 100644 --- a/resources/profiles/BBL/filament/Generic PLA Silk @BBL H2C.json +++ b/resources/profiles/BBL/filament/Generic PLA Silk @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSL96_09", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Generic PLA Silk @BBL H2S.json b/resources/profiles/BBL/filament/Generic PLA Silk @BBL H2S.json index 100e03b3c9..dae0e857d4 100644 --- a/resources/profiles/BBL/filament/Generic PLA Silk @BBL H2S.json +++ b/resources/profiles/BBL/filament/Generic PLA Silk @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSL96_04", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Generic PPA-CF @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Generic PPA-CF @BBL H2C 0.4 nozzle.json index 6307ce8fd1..4d1f2c5c06 100644 --- a/resources/profiles/BBL/filament/Generic PPA-CF @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Generic PPA-CF @BBL H2C 0.4 nozzle.json @@ -9,6 +9,8 @@ "60" ], "fan_max_speed": [ + "35", + "35", "35" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic PPA-CF @BBL H2C.json b/resources/profiles/BBL/filament/Generic PPA-CF @BBL H2C.json index 99fbafe181..7fefc47632 100644 --- a/resources/profiles/BBL/filament/Generic PPA-CF @BBL H2C.json +++ b/resources/profiles/BBL/filament/Generic PPA-CF @BBL H2C.json @@ -9,6 +9,8 @@ "60" ], "fan_max_speed": [ + "35", + "35", "35" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic PPA-CF @BBL H2D.json b/resources/profiles/BBL/filament/Generic PPA-CF @BBL H2D.json index 5798bede95..0e0b947fa0 100644 --- a/resources/profiles/BBL/filament/Generic PPA-CF @BBL H2D.json +++ b/resources/profiles/BBL/filament/Generic PPA-CF @BBL H2D.json @@ -9,6 +9,8 @@ "60" ], "fan_max_speed": [ + "35", + "35", "35" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic PPA-CF @BBL H2DP.json b/resources/profiles/BBL/filament/Generic PPA-CF @BBL H2DP.json index ab2c95d6ec..3a6d182a8b 100644 --- a/resources/profiles/BBL/filament/Generic PPA-CF @BBL H2DP.json +++ b/resources/profiles/BBL/filament/Generic PPA-CF @BBL H2DP.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "35", "35" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic PPA-CF @BBL H2S 0.4 nozzle.json b/resources/profiles/BBL/filament/Generic PPA-CF @BBL H2S 0.4 nozzle.json index 1424641376..8f134d6350 100644 --- a/resources/profiles/BBL/filament/Generic PPA-CF @BBL H2S 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Generic PPA-CF @BBL H2S 0.4 nozzle.json @@ -9,6 +9,8 @@ "60" ], "fan_max_speed": [ + "35", + "35", "35" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic PPA-CF @BBL P2S.json b/resources/profiles/BBL/filament/Generic PPA-CF @BBL P2S.json index 77b17fb0d8..1ac2b972ff 100644 --- a/resources/profiles/BBL/filament/Generic PPA-CF @BBL P2S.json +++ b/resources/profiles/BBL/filament/Generic PPA-CF @BBL P2S.json @@ -9,6 +9,8 @@ "60" ], "fan_max_speed": [ + "35", + "35", "35" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic PPA-CF @BBL X1C.json b/resources/profiles/BBL/filament/Generic PPA-CF @BBL X1C.json index 6b543ecd14..235dc251c6 100644 --- a/resources/profiles/BBL/filament/Generic PPA-CF @BBL X1C.json +++ b/resources/profiles/BBL/filament/Generic PPA-CF @BBL X1C.json @@ -6,6 +6,7 @@ "setting_id": "GFSN97_00", "instantiation": "true", "fan_max_speed": [ + "35", "35" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Generic PPA-CF @BBL X1E.json b/resources/profiles/BBL/filament/Generic PPA-CF @BBL X1E.json index 636204070f..b455fbb083 100644 --- a/resources/profiles/BBL/filament/Generic PPA-CF @BBL X1E.json +++ b/resources/profiles/BBL/filament/Generic PPA-CF @BBL X1E.json @@ -9,6 +9,7 @@ "60" ], "fan_max_speed": [ + "35", "35" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Generic PPA-CF @BBL X2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Generic PPA-CF @BBL X2D 0.4 nozzle.json index c171b03fe9..9963bf7c53 100644 --- a/resources/profiles/BBL/filament/Generic PPA-CF @BBL X2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Generic PPA-CF @BBL X2D 0.4 nozzle.json @@ -9,6 +9,11 @@ "60" ], "fan_max_speed": [ + "35", + "35", + "35", + "35", + "35", "35" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic PPA-CF @BBL X2D.json b/resources/profiles/BBL/filament/Generic PPA-CF @BBL X2D.json index 97b164c7a4..f3f53400d6 100644 --- a/resources/profiles/BBL/filament/Generic PPA-CF @BBL X2D.json +++ b/resources/profiles/BBL/filament/Generic PPA-CF @BBL X2D.json @@ -9,6 +9,11 @@ "60" ], "fan_max_speed": [ + "35", + "35", + "35", + "35", + "35", "35" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic TPU @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Generic TPU @BBL H2C 0.4 nozzle.json index 9dda2e9f85..fe62720d18 100644 --- a/resources/profiles/BBL/filament/Generic TPU @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Generic TPU @BBL H2C 0.4 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSU99_08", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Generic TPU @BBL H2C.json b/resources/profiles/BBL/filament/Generic TPU @BBL H2C.json index 063f446f6d..818944cfa2 100644 --- a/resources/profiles/BBL/filament/Generic TPU @BBL H2C.json +++ b/resources/profiles/BBL/filament/Generic TPU @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSU99_09", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Generic TPU @BBL H2D.json b/resources/profiles/BBL/filament/Generic TPU @BBL H2D.json index 1d2e1a447d..b5d28a37e9 100644 --- a/resources/profiles/BBL/filament/Generic TPU @BBL H2D.json +++ b/resources/profiles/BBL/filament/Generic TPU @BBL H2D.json @@ -6,6 +6,9 @@ "setting_id": "GFSU99_02", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Generic TPU @BBL H2DP.json b/resources/profiles/BBL/filament/Generic TPU @BBL H2DP.json index 456941431d..2fd9bf50e7 100644 --- a/resources/profiles/BBL/filament/Generic TPU @BBL H2DP.json +++ b/resources/profiles/BBL/filament/Generic TPU @BBL H2DP.json @@ -6,6 +6,8 @@ "setting_id": "GFSU99_06", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Generic TPU @BBL H2S.json b/resources/profiles/BBL/filament/Generic TPU @BBL H2S.json index be7f778869..aa6a537525 100644 --- a/resources/profiles/BBL/filament/Generic TPU @BBL H2S.json +++ b/resources/profiles/BBL/filament/Generic TPU @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSU99_04", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Generic TPU for AMS @BBL H2C 0.4 nozzle.json b/resources/profiles/BBL/filament/Generic TPU for AMS @BBL H2C 0.4 nozzle.json index 7232187281..58cf7270b5 100644 --- a/resources/profiles/BBL/filament/Generic TPU for AMS @BBL H2C 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Generic TPU for AMS @BBL H2C 0.4 nozzle.json @@ -6,6 +6,8 @@ "setting_id": "GFSU98_10", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ @@ -18,9 +20,13 @@ "40" ], "fan_max_speed": [ + "50", + "50", "50" ], "fan_min_speed": [ + "10", + "10", "10" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic TPU for AMS @BBL H2C.json b/resources/profiles/BBL/filament/Generic TPU for AMS @BBL H2C.json index 4980e672f8..700c90e567 100644 --- a/resources/profiles/BBL/filament/Generic TPU for AMS @BBL H2C.json +++ b/resources/profiles/BBL/filament/Generic TPU for AMS @BBL H2C.json @@ -6,6 +6,8 @@ "setting_id": "GFSU98_11", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ @@ -18,9 +20,13 @@ "40" ], "fan_max_speed": [ + "50", + "50", "50" ], "fan_min_speed": [ + "10", + "10", "10" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic TPU for AMS @BBL H2D.json b/resources/profiles/BBL/filament/Generic TPU for AMS @BBL H2D.json index 37f4f2ffa4..9eec32f435 100644 --- a/resources/profiles/BBL/filament/Generic TPU for AMS @BBL H2D.json +++ b/resources/profiles/BBL/filament/Generic TPU for AMS @BBL H2D.json @@ -6,6 +6,8 @@ "setting_id": "GFSU98_04", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ @@ -18,9 +20,13 @@ "40" ], "fan_max_speed": [ + "50", + "50", "50" ], "fan_min_speed": [ + "10", + "10", "10" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic TPU for AMS @BBL H2DP.json b/resources/profiles/BBL/filament/Generic TPU for AMS @BBL H2DP.json index 42f65f4680..1e3b9ac042 100644 --- a/resources/profiles/BBL/filament/Generic TPU for AMS @BBL H2DP.json +++ b/resources/profiles/BBL/filament/Generic TPU for AMS @BBL H2DP.json @@ -6,6 +6,7 @@ "setting_id": "GFSU98_08", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ @@ -18,9 +19,11 @@ "40" ], "fan_max_speed": [ + "50", "50" ], "fan_min_speed": [ + "10", "10" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic TPU for AMS @BBL H2S.json b/resources/profiles/BBL/filament/Generic TPU for AMS @BBL H2S.json index 7931394b1f..6cd4066909 100644 --- a/resources/profiles/BBL/filament/Generic TPU for AMS @BBL H2S.json +++ b/resources/profiles/BBL/filament/Generic TPU for AMS @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSU98_06", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", + "100", "100" ], "eng_plate_temp": [ @@ -18,9 +20,13 @@ "40" ], "fan_max_speed": [ + "50", + "50", "50" ], "fan_min_speed": [ + "10", + "10", "10" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic TPU for AMS @BBL P1P.json b/resources/profiles/BBL/filament/Generic TPU for AMS @BBL P1P.json index 437aadfa54..e93e38f3dd 100644 --- a/resources/profiles/BBL/filament/Generic TPU for AMS @BBL P1P.json +++ b/resources/profiles/BBL/filament/Generic TPU for AMS @BBL P1P.json @@ -9,9 +9,11 @@ "40" ], "fan_max_speed": [ + "50", "50" ], "fan_min_speed": [ + "10", "10" ], "filament_max_volumetric_speed": [ diff --git a/resources/profiles/BBL/filament/Generic TPU for AMS @BBL P2S.json b/resources/profiles/BBL/filament/Generic TPU for AMS @BBL P2S.json index 24c8b552fb..cde0a1e7f7 100644 --- a/resources/profiles/BBL/filament/Generic TPU for AMS @BBL P2S.json +++ b/resources/profiles/BBL/filament/Generic TPU for AMS @BBL P2S.json @@ -15,9 +15,13 @@ "40" ], "fan_max_speed": [ + "50", + "50", "50" ], "fan_min_speed": [ + "10", + "10", "10" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic TPU for AMS @BBL X1C.json b/resources/profiles/BBL/filament/Generic TPU for AMS @BBL X1C.json index 85b053a0cd..189175d6c1 100644 --- a/resources/profiles/BBL/filament/Generic TPU for AMS @BBL X1C.json +++ b/resources/profiles/BBL/filament/Generic TPU for AMS @BBL X1C.json @@ -9,9 +9,11 @@ "40" ], "fan_max_speed": [ + "50", "50" ], "fan_min_speed": [ + "10", "10" ], "filament_max_volumetric_speed": [ diff --git a/resources/profiles/BBL/filament/Generic TPU for AMS @BBL X2D 0.4 nozzle.json b/resources/profiles/BBL/filament/Generic TPU for AMS @BBL X2D 0.4 nozzle.json index 446cb91fbb..0e71a2ab70 100644 --- a/resources/profiles/BBL/filament/Generic TPU for AMS @BBL X2D 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/Generic TPU for AMS @BBL X2D 0.4 nozzle.json @@ -15,9 +15,19 @@ "40" ], "fan_max_speed": [ + "50", + "50", + "50", + "50", + "50", "50" ], "fan_min_speed": [ + "10", + "10", + "10", + "10", + "10", "10" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Generic TPU for AMS @BBL X2D.json b/resources/profiles/BBL/filament/Generic TPU for AMS @BBL X2D.json index 3223336a76..6b7ccfcb34 100644 --- a/resources/profiles/BBL/filament/Generic TPU for AMS @BBL X2D.json +++ b/resources/profiles/BBL/filament/Generic TPU for AMS @BBL X2D.json @@ -15,9 +15,19 @@ "40" ], "fan_max_speed": [ + "50", + "50", + "50", + "50", + "50", "50" ], "fan_min_speed": [ + "10", + "10", + "10", + "10", + "10", "10" ], "filament_cooling_before_tower": [ diff --git a/resources/profiles/BBL/filament/Overture Matte PLA @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Overture Matte PLA @BBL H2S 0.2 nozzle.json index 5c258fbd47..843ad953e4 100644 --- a/resources/profiles/BBL/filament/Overture Matte PLA @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Overture Matte PLA @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSL05_12", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Overture Matte PLA @BBL H2S.json b/resources/profiles/BBL/filament/Overture Matte PLA @BBL H2S.json index eb84bfac73..c2f8591551 100644 --- a/resources/profiles/BBL/filament/Overture Matte PLA @BBL H2S.json +++ b/resources/profiles/BBL/filament/Overture Matte PLA @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSL05_11", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Overture Matte PLA @BBL P1P.json b/resources/profiles/BBL/filament/Overture Matte PLA @BBL P1P.json index b6747d4d27..0b4c7295e5 100644 --- a/resources/profiles/BBL/filament/Overture Matte PLA @BBL P1P.json +++ b/resources/profiles/BBL/filament/Overture Matte PLA @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/Overture PLA @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/Overture PLA @BBL H2S 0.2 nozzle.json index 015bd25840..d810f76495 100644 --- a/resources/profiles/BBL/filament/Overture PLA @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/Overture PLA @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSL04_12", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Overture PLA @BBL H2S.json b/resources/profiles/BBL/filament/Overture PLA @BBL H2S.json index e66b7c9405..f65421b43c 100644 --- a/resources/profiles/BBL/filament/Overture PLA @BBL H2S.json +++ b/resources/profiles/BBL/filament/Overture PLA @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSL04_11", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/Overture PLA @BBL P1P.json b/resources/profiles/BBL/filament/Overture PLA @BBL P1P.json index c8ad574304..9c24fa069a 100644 --- a/resources/profiles/BBL/filament/Overture PLA @BBL P1P.json +++ b/resources/profiles/BBL/filament/Overture PLA @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu ABS @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/P1P/Bambu ABS @BBL P1P 0.2 nozzle.json index dcbd382ffe..d86054d733 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu ABS @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Bambu ABS @BBL P1P 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSB00_02", "instantiation": "true", "fan_max_speed": [ + "20", "20" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu ABS @BBL P1P 0.6 nozzle.json b/resources/profiles/BBL/filament/P1P/Bambu ABS @BBL P1P 0.6 nozzle.json index ea6060ba15..bd88551b01 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu ABS @BBL P1P 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Bambu ABS @BBL P1P 0.6 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSB00_41", "instantiation": "true", "fan_max_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu ABS @BBL P1P.json b/resources/profiles/BBL/filament/P1P/Bambu ABS @BBL P1P.json index c21004ca75..c667b7bb37 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu ABS @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/Bambu ABS @BBL P1P.json @@ -6,6 +6,7 @@ "setting_id": "GFSB00_03", "instantiation": "true", "fan_max_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PA-CF @BBL P1P.json b/resources/profiles/BBL/filament/P1P/Bambu PA-CF @BBL P1P.json index b7cfc61878..b4e7c13da7 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PA-CF @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PA-CF @BBL P1P.json @@ -9,9 +9,11 @@ "5" ], "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "10", "10" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PC @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/P1P/Bambu PC @BBL P1P 0.2 nozzle.json index d64fff5af9..d211d9acff 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PC @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PC @BBL P1P 0.2 nozzle.json @@ -12,6 +12,7 @@ "100" ], "fan_max_speed": [ + "20", "20" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PC @BBL P1P 0.6 nozzle.json b/resources/profiles/BBL/filament/P1P/Bambu PC @BBL P1P 0.6 nozzle.json index bc23bb1607..4167792881 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PC @BBL P1P 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PC @BBL P1P 0.6 nozzle.json @@ -12,6 +12,7 @@ "100" ], "fan_max_speed": [ + "20", "20" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PC @BBL P1P.json b/resources/profiles/BBL/filament/P1P/Bambu PC @BBL P1P.json index 0977c4fbb8..0d40c57d7f 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PC @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PC @BBL P1P.json @@ -12,6 +12,7 @@ "100" ], "fan_max_speed": [ + "20", "20" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PET-CF @BBL P1P.json b/resources/profiles/BBL/filament/P1P/Bambu PET-CF @BBL P1P.json index 5095859d67..f31d1108a6 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PET-CF @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PET-CF @BBL P1P.json @@ -9,9 +9,11 @@ "80" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "1", "1" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PETG-CF @BBL P1P 0.4 nozzle.json b/resources/profiles/BBL/filament/P1P/Bambu PETG-CF @BBL P1P 0.4 nozzle.json index 6f21d17509..1d0a40bbc1 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PETG-CF @BBL P1P 0.4 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PETG-CF @BBL P1P 0.4 nozzle.json @@ -9,9 +9,11 @@ "30" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "5", "5" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PETG-CF @BBL P1P.json b/resources/profiles/BBL/filament/P1P/Bambu PETG-CF @BBL P1P.json index bf140d0d31..514ff302b6 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PETG-CF @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PETG-CF @BBL P1P.json @@ -9,9 +9,11 @@ "30" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "5", "5" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PLA Aero @BBL P1P.json b/resources/profiles/BBL/filament/P1P/Bambu PLA Aero @BBL P1P.json index 01eb23cd11..eb6c25d774 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PLA Aero @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PLA Aero @BBL P1P.json @@ -6,6 +6,7 @@ "setting_id": "GFSA11_02", "instantiation": "true", "additional_cooling_fan_speed": [ + "0", "0" ], "fan_cooling_layer_time": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PLA Basic @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/P1P/Bambu PLA Basic @BBL P1P 0.2 nozzle.json index db4088ef94..14ffc1f3af 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PLA Basic @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PLA Basic @BBL P1P 0.2 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PLA Basic @BBL P1P 0.6 nozzle.json b/resources/profiles/BBL/filament/P1P/Bambu PLA Basic @BBL P1P 0.6 nozzle.json index f30ef29c1f..3afb4e04d7 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PLA Basic @BBL P1P 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PLA Basic @BBL P1P 0.6 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PLA Basic @BBL P1P.json b/resources/profiles/BBL/filament/P1P/Bambu PLA Basic @BBL P1P.json index eb897cf359..c8d5579415 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PLA Basic @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PLA Basic @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PLA Marble @BBL P1P 0.6 nozzle.json b/resources/profiles/BBL/filament/P1P/Bambu PLA Marble @BBL P1P 0.6 nozzle.json index 2cbd700b75..98d76749a1 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PLA Marble @BBL P1P 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PLA Marble @BBL P1P 0.6 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PLA Marble @BBL P1P.json b/resources/profiles/BBL/filament/P1P/Bambu PLA Marble @BBL P1P.json index 1cc0306b4b..dbdfba324d 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PLA Marble @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PLA Marble @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PLA Matte @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/P1P/Bambu PLA Matte @BBL P1P 0.2 nozzle.json index 6dbead6f4d..58d82e9947 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PLA Matte @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PLA Matte @BBL P1P 0.2 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PLA Matte @BBL P1P 0.6 nozzle.json b/resources/profiles/BBL/filament/P1P/Bambu PLA Matte @BBL P1P 0.6 nozzle.json index 6107e5ba72..444a896d66 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PLA Matte @BBL P1P 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PLA Matte @BBL P1P 0.6 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PLA Matte @BBL P1P.json b/resources/profiles/BBL/filament/P1P/Bambu PLA Matte @BBL P1P.json index a70c160320..54b746b622 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PLA Matte @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PLA Matte @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PLA Metal @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/P1P/Bambu PLA Metal @BBL P1P 0.2 nozzle.json index c33ce2a479..ddab76f59b 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PLA Metal @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PLA Metal @BBL P1P 0.2 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PLA Metal @BBL P1P 0.6 nozzle.json b/resources/profiles/BBL/filament/P1P/Bambu PLA Metal @BBL P1P 0.6 nozzle.json index f0fd865b66..2b4247d4df 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PLA Metal @BBL P1P 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PLA Metal @BBL P1P 0.6 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PLA Metal @BBL P1P.json b/resources/profiles/BBL/filament/P1P/Bambu PLA Metal @BBL P1P.json index 9c600caafe..58c8d0f1f6 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PLA Metal @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PLA Metal @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PLA Sparkle @BBL P1P 0.6 nozzle.json b/resources/profiles/BBL/filament/P1P/Bambu PLA Sparkle @BBL P1P 0.6 nozzle.json index eaa8896497..15251bcf61 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PLA Sparkle @BBL P1P 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PLA Sparkle @BBL P1P 0.6 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PLA Sparkle @BBL P1P.json b/resources/profiles/BBL/filament/P1P/Bambu PLA Sparkle @BBL P1P.json index 240da6d165..1f13647b2f 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PLA Sparkle @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PLA Sparkle @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PLA Tough @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/P1P/Bambu PLA Tough @BBL P1P 0.2 nozzle.json index f99ea5200f..ed776be75b 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PLA Tough @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PLA Tough @BBL P1P 0.2 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PLA Tough @BBL P1P.json b/resources/profiles/BBL/filament/P1P/Bambu PLA Tough @BBL P1P.json index 8a1a0fd274..c50562687f 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PLA Tough @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PLA Tough @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PLA Tough+ @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/P1P/Bambu PLA Tough+ @BBL P1P 0.2 nozzle.json index 1a0c12f0ff..f578485c6e 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PLA Tough+ @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PLA Tough+ @BBL P1P 0.2 nozzle.json @@ -6,12 +6,14 @@ "setting_id": "GFSA10_33", "instantiation": "true", "additional_cooling_fan_speed": [ + "70", "70" ], "fan_cooling_layer_time": [ "80" ], "fan_min_speed": [ + "90", "90" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PLA Tough+ @BBL P1P 0.6 nozzle.json b/resources/profiles/BBL/filament/P1P/Bambu PLA Tough+ @BBL P1P 0.6 nozzle.json index c090797121..09fd6e3614 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PLA Tough+ @BBL P1P 0.6 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PLA Tough+ @BBL P1P 0.6 nozzle.json @@ -6,12 +6,14 @@ "setting_id": "GFSA10_38", "instantiation": "true", "additional_cooling_fan_speed": [ + "70", "70" ], "fan_cooling_layer_time": [ "80" ], "fan_min_speed": [ + "90", "90" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PLA Tough+ @BBL P1P.json b/resources/profiles/BBL/filament/P1P/Bambu PLA Tough+ @BBL P1P.json index b4e25f845b..13c6ae6a58 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PLA Tough+ @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PLA Tough+ @BBL P1P.json @@ -6,12 +6,14 @@ "setting_id": "GFSA10_34", "instantiation": "true", "additional_cooling_fan_speed": [ + "70", "70" ], "fan_cooling_layer_time": [ "80" ], "fan_min_speed": [ + "90", "90" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PLA-CF @BBL P1P 0.8 nozzle.json b/resources/profiles/BBL/filament/P1P/Bambu PLA-CF @BBL P1P 0.8 nozzle.json index dcf38827ce..def3a2b834 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PLA-CF @BBL P1P 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PLA-CF @BBL P1P 0.8 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu PLA-CF @BBL P1P.json b/resources/profiles/BBL/filament/P1P/Bambu PLA-CF @BBL P1P.json index 82a84b4d1f..607ad46ade 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu PLA-CF @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/Bambu PLA-CF @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu Support For PLA @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/P1P/Bambu Support For PLA @BBL P1P 0.2 nozzle.json index 0a789a6941..3e06730971 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu Support For PLA @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Bambu Support For PLA @BBL P1P 0.2 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu Support For PLA @BBL P1P.json b/resources/profiles/BBL/filament/P1P/Bambu Support For PLA @BBL P1P.json index 3bbd78f489..bed7a8a83f 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu Support For PLA @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/Bambu Support For PLA @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu Support W @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/P1P/Bambu Support W @BBL P1P 0.2 nozzle.json index 43c99264b1..34d24468ed 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu Support W @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Bambu Support W @BBL P1P 0.2 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Bambu Support W @BBL P1P.json b/resources/profiles/BBL/filament/P1P/Bambu Support W @BBL P1P.json index ca08dc8ce3..1d0ddf542f 100644 --- a/resources/profiles/BBL/filament/P1P/Bambu Support W @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/Bambu Support W @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Generic ABS @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/P1P/Generic ABS @BBL P1P 0.2 nozzle.json index a11c8b406c..b8ba38cc18 100644 --- a/resources/profiles/BBL/filament/P1P/Generic ABS @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Generic ABS @BBL P1P 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSB99_02", "instantiation": "true", "fan_max_speed": [ + "20", "20" ], "filament_max_volumetric_speed": [ diff --git a/resources/profiles/BBL/filament/P1P/Generic ABS @BBL P1P.json b/resources/profiles/BBL/filament/P1P/Generic ABS @BBL P1P.json index 77f537261e..e2d57d2407 100644 --- a/resources/profiles/BBL/filament/P1P/Generic ABS @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/Generic ABS @BBL P1P.json @@ -6,6 +6,7 @@ "setting_id": "GFSB99_01", "instantiation": "true", "fan_max_speed": [ + "20", "20" ], "filament_max_volumetric_speed": [ diff --git a/resources/profiles/BBL/filament/P1P/Generic PC @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/P1P/Generic PC @BBL P1P 0.2 nozzle.json index 75837ececb..9fac30cbfc 100644 --- a/resources/profiles/BBL/filament/P1P/Generic PC @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Generic PC @BBL P1P 0.2 nozzle.json @@ -12,6 +12,7 @@ "100" ], "fan_max_speed": [ + "20", "20" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Generic PC @BBL P1P.json b/resources/profiles/BBL/filament/P1P/Generic PC @BBL P1P.json index 08086f47f6..df1ea38f65 100644 --- a/resources/profiles/BBL/filament/P1P/Generic PC @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/Generic PC @BBL P1P.json @@ -12,6 +12,7 @@ "100" ], "fan_max_speed": [ + "20", "20" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Generic PETG-CF @BBL P1P.json b/resources/profiles/BBL/filament/P1P/Generic PETG-CF @BBL P1P.json index fc0eb4132b..59049ae30c 100644 --- a/resources/profiles/BBL/filament/P1P/Generic PETG-CF @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/Generic PETG-CF @BBL P1P.json @@ -9,9 +9,11 @@ "30" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "5", "5" ], "filament_max_volumetric_speed": [ diff --git a/resources/profiles/BBL/filament/P1P/Generic PLA @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/P1P/Generic PLA @BBL P1P 0.2 nozzle.json index fb08c2ca3c..b4f52cf6e2 100644 --- a/resources/profiles/BBL/filament/P1P/Generic PLA @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/Generic PLA @BBL P1P 0.2 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Generic PLA @BBL P1P.json b/resources/profiles/BBL/filament/P1P/Generic PLA @BBL P1P.json index 23782d1f63..fefc87018c 100644 --- a/resources/profiles/BBL/filament/P1P/Generic PLA @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/Generic PLA @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/Generic PVA @BBL P1P.json b/resources/profiles/BBL/filament/P1P/Generic PVA @BBL P1P.json index 55cbed4c0f..32406f8d79 100644 --- a/resources/profiles/BBL/filament/P1P/Generic PVA @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/Generic PVA @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_max_volumetric_speed": [ diff --git a/resources/profiles/BBL/filament/P1P/PolyLite PLA @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/P1P/PolyLite PLA @BBL P1P 0.2 nozzle.json index c7e57b79d2..d5d4db0ceb 100644 --- a/resources/profiles/BBL/filament/P1P/PolyLite PLA @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/PolyLite PLA @BBL P1P 0.2 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_max_volumetric_speed": [ diff --git a/resources/profiles/BBL/filament/P1P/PolyLite PLA @BBL P1P.json b/resources/profiles/BBL/filament/P1P/PolyLite PLA @BBL P1P.json index 9d5f349afb..1bd2bda10c 100644 --- a/resources/profiles/BBL/filament/P1P/PolyLite PLA @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/PolyLite PLA @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_max_volumetric_speed": [ diff --git a/resources/profiles/BBL/filament/P1P/PolyTerra PLA @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/P1P/PolyTerra PLA @BBL P1P 0.2 nozzle.json index c07ce9f912..608b2c4654 100644 --- a/resources/profiles/BBL/filament/P1P/PolyTerra PLA @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/PolyTerra PLA @BBL P1P 0.2 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_max_volumetric_speed": [ diff --git a/resources/profiles/BBL/filament/P1P/PolyTerra PLA @BBL P1P.json b/resources/profiles/BBL/filament/P1P/PolyTerra PLA @BBL P1P.json index 69b0921119..d9389ab744 100644 --- a/resources/profiles/BBL/filament/P1P/PolyTerra PLA @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/PolyTerra PLA @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/eSUN PLA+ @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/P1P/eSUN PLA+ @BBL P1P 0.2 nozzle.json index 07b2229f3c..1bab33f91d 100644 --- a/resources/profiles/BBL/filament/P1P/eSUN PLA+ @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/P1P/eSUN PLA+ @BBL P1P 0.2 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/P1P/eSUN PLA+ @BBL P1P.json b/resources/profiles/BBL/filament/P1P/eSUN PLA+ @BBL P1P.json index c035419a13..b4bfa2366f 100644 --- a/resources/profiles/BBL/filament/P1P/eSUN PLA+ @BBL P1P.json +++ b/resources/profiles/BBL/filament/P1P/eSUN PLA+ @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/PolyLite ABS @BBL P1P.json b/resources/profiles/BBL/filament/PolyLite ABS @BBL P1P.json index 57d32de9a7..32b64c1349 100644 --- a/resources/profiles/BBL/filament/PolyLite ABS @BBL P1P.json +++ b/resources/profiles/BBL/filament/PolyLite ABS @BBL P1P.json @@ -6,6 +6,7 @@ "setting_id": "GFSB60_04", "instantiation": "true", "fan_max_speed": [ + "20", "20" ], "filament_max_volumetric_speed": [ diff --git a/resources/profiles/BBL/filament/PolyLite PETG @BBL H2D.json b/resources/profiles/BBL/filament/PolyLite PETG @BBL H2D.json index 3e90dda823..ccc0b59c48 100644 --- a/resources/profiles/BBL/filament/PolyLite PETG @BBL H2D.json +++ b/resources/profiles/BBL/filament/PolyLite PETG @BBL H2D.json @@ -24,6 +24,8 @@ "fdm_filament_template_direct_dual_e3d" ], "nozzle_temperature_range_high": [ + "270", + "270", "270" ], "nozzle_temperature": [ diff --git a/resources/profiles/BBL/filament/PolyLite PETG @BBL H2DP.json b/resources/profiles/BBL/filament/PolyLite PETG @BBL H2DP.json index 24fcafab4f..f8179f7c73 100644 --- a/resources/profiles/BBL/filament/PolyLite PETG @BBL H2DP.json +++ b/resources/profiles/BBL/filament/PolyLite PETG @BBL H2DP.json @@ -29,6 +29,7 @@ "255" ], "nozzle_temperature_range_high": [ + "270", "270" ], "slow_down_min_speed": [ diff --git a/resources/profiles/BBL/filament/PolyLite PETG @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/PolyLite PETG @BBL H2S 0.2 nozzle.json index 0bdcad3735..d4e4978024 100644 --- a/resources/profiles/BBL/filament/PolyLite PETG @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/PolyLite PETG @BBL H2S 0.2 nozzle.json @@ -33,6 +33,7 @@ "0" ], "nozzle_temperature_range_high": [ + "270", "270" ], "nozzle_temperature": [ diff --git a/resources/profiles/BBL/filament/PolyLite PETG @BBL H2S.json b/resources/profiles/BBL/filament/PolyLite PETG @BBL H2S.json index 912cb9a3ed..58e42dba4e 100644 --- a/resources/profiles/BBL/filament/PolyLite PETG @BBL H2S.json +++ b/resources/profiles/BBL/filament/PolyLite PETG @BBL H2S.json @@ -34,6 +34,8 @@ "0" ], "nozzle_temperature_range_high": [ + "270", + "270", "270" ], "nozzle_temperature": [ diff --git a/resources/profiles/BBL/filament/PolyLite PETG @BBL P1P.json b/resources/profiles/BBL/filament/PolyLite PETG @BBL P1P.json index 9b0b409cb9..ec05e93824 100644 --- a/resources/profiles/BBL/filament/PolyLite PETG @BBL P1P.json +++ b/resources/profiles/BBL/filament/PolyLite PETG @BBL P1P.json @@ -27,6 +27,7 @@ "0" ], "nozzle_temperature_range_high": [ + "270", "270" ], "nozzle_temperature": [ diff --git a/resources/profiles/BBL/filament/PolyLite PETG @BBL X1C.json b/resources/profiles/BBL/filament/PolyLite PETG @BBL X1C.json index fb7f801850..a69f499126 100644 --- a/resources/profiles/BBL/filament/PolyLite PETG @BBL X1C.json +++ b/resources/profiles/BBL/filament/PolyLite PETG @BBL X1C.json @@ -21,6 +21,7 @@ "0" ], "nozzle_temperature_range_high": [ + "270", "270" ], "nozzle_temperature": [ diff --git a/resources/profiles/BBL/filament/PolyLite PLA @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/PolyLite PLA @BBL H2S 0.2 nozzle.json index bcd5226a9e..f623a98acb 100644 --- a/resources/profiles/BBL/filament/PolyLite PLA @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/PolyLite PLA @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSL00_07", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/PolyLite PLA @BBL H2S.json b/resources/profiles/BBL/filament/PolyLite PLA @BBL H2S.json index 8ff8f89d35..c4d8c7413f 100644 --- a/resources/profiles/BBL/filament/PolyLite PLA @BBL H2S.json +++ b/resources/profiles/BBL/filament/PolyLite PLA @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSL00_06", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/PolyTerra PLA @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/PolyTerra PLA @BBL H2S 0.2 nozzle.json index 38d5fec0ca..d966dce73c 100644 --- a/resources/profiles/BBL/filament/PolyTerra PLA @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/PolyTerra PLA @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSL01_07", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/PolyTerra PLA @BBL H2S.json b/resources/profiles/BBL/filament/PolyTerra PLA @BBL H2S.json index ee6b8e2e36..533c00c5f8 100644 --- a/resources/profiles/BBL/filament/PolyTerra PLA @BBL H2S.json +++ b/resources/profiles/BBL/filament/PolyTerra PLA @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSL01_06", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/SUNLU/SUNLU Marble PLA @BBL P1P.json b/resources/profiles/BBL/filament/SUNLU/SUNLU Marble PLA @BBL P1P.json index 0a103c995c..bc6e1338f0 100644 --- a/resources/profiles/BBL/filament/SUNLU/SUNLU Marble PLA @BBL P1P.json +++ b/resources/profiles/BBL/filament/SUNLU/SUNLU Marble PLA @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/SUNLU/SUNLU PETG @BBL X1C 0.8 nozzle.json b/resources/profiles/BBL/filament/SUNLU/SUNLU PETG @BBL X1C 0.8 nozzle.json index 99dc78c918..608f860fcc 100644 --- a/resources/profiles/BBL/filament/SUNLU/SUNLU PETG @BBL X1C 0.8 nozzle.json +++ b/resources/profiles/BBL/filament/SUNLU/SUNLU PETG @BBL X1C 0.8 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "GFSNLS08_01", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "fan_min_speed": [ + "20", "20" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/BBL/filament/SUNLU/SUNLU PLA Matte @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/SUNLU/SUNLU PLA Matte @BBL P1P 0.2 nozzle.json index 464e056c8c..7150b7078b 100644 --- a/resources/profiles/BBL/filament/SUNLU/SUNLU PLA Matte @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/SUNLU/SUNLU PLA Matte @BBL P1P 0.2 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/SUNLU/SUNLU PLA Matte @BBL P1P.json b/resources/profiles/BBL/filament/SUNLU/SUNLU PLA Matte @BBL P1P.json index 6054f5fd07..6fa8309b85 100644 --- a/resources/profiles/BBL/filament/SUNLU/SUNLU PLA Matte @BBL P1P.json +++ b/resources/profiles/BBL/filament/SUNLU/SUNLU PLA Matte @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/SUNLU/SUNLU PLA+ 2.0 @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/SUNLU/SUNLU PLA+ 2.0 @BBL P1P 0.2 nozzle.json index f8329334a4..5e6555da12 100644 --- a/resources/profiles/BBL/filament/SUNLU/SUNLU PLA+ 2.0 @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/SUNLU/SUNLU PLA+ 2.0 @BBL P1P 0.2 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/SUNLU/SUNLU PLA+ 2.0 @BBL P1P.json b/resources/profiles/BBL/filament/SUNLU/SUNLU PLA+ 2.0 @BBL P1P.json index b6bccf5151..ae8bcda763 100644 --- a/resources/profiles/BBL/filament/SUNLU/SUNLU PLA+ 2.0 @BBL P1P.json +++ b/resources/profiles/BBL/filament/SUNLU/SUNLU PLA+ 2.0 @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/SUNLU/SUNLU PLA+ @BBL P1P 0.2 nozzle.json b/resources/profiles/BBL/filament/SUNLU/SUNLU PLA+ @BBL P1P 0.2 nozzle.json index 23227067a2..5f46964241 100644 --- a/resources/profiles/BBL/filament/SUNLU/SUNLU PLA+ @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/SUNLU/SUNLU PLA+ @BBL P1P 0.2 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/SUNLU/SUNLU PLA+ @BBL P1P.json b/resources/profiles/BBL/filament/SUNLU/SUNLU PLA+ @BBL P1P.json index 092d213cca..c936fed91a 100644 --- a/resources/profiles/BBL/filament/SUNLU/SUNLU PLA+ @BBL P1P.json +++ b/resources/profiles/BBL/filament/SUNLU/SUNLU PLA+ @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/BBL/filament/SUNLU/SUNLU Wood PLA @BBL P1P.json b/resources/profiles/BBL/filament/SUNLU/SUNLU Wood PLA @BBL P1P.json index ff61f6d3ce..9b7811ef7b 100644 --- a/resources/profiles/BBL/filament/SUNLU/SUNLU Wood PLA @BBL P1P.json +++ b/resources/profiles/BBL/filament/SUNLU/SUNLU Wood PLA @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/BBL/filament/eSUN PLA+ @BBL H2S 0.2 nozzle.json b/resources/profiles/BBL/filament/eSUN PLA+ @BBL H2S 0.2 nozzle.json index eb6e63dd5c..c8d99496b4 100644 --- a/resources/profiles/BBL/filament/eSUN PLA+ @BBL H2S 0.2 nozzle.json +++ b/resources/profiles/BBL/filament/eSUN PLA+ @BBL H2S 0.2 nozzle.json @@ -6,6 +6,7 @@ "setting_id": "GFSL03_12", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/BBL/filament/eSUN PLA+ @BBL H2S.json b/resources/profiles/BBL/filament/eSUN PLA+ @BBL H2S.json index 0a60b92b11..770b3ae75f 100644 --- a/resources/profiles/BBL/filament/eSUN PLA+ @BBL H2S.json +++ b/resources/profiles/BBL/filament/eSUN PLA+ @BBL H2S.json @@ -6,6 +6,8 @@ "setting_id": "GFSL03_09", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/OrcaFilamentLibrary.json b/resources/profiles/OrcaFilamentLibrary.json index 4ef905b799..d5f972a222 100644 --- a/resources/profiles/OrcaFilamentLibrary.json +++ b/resources/profiles/OrcaFilamentLibrary.json @@ -1,6 +1,6 @@ { "name": "OrcaFilamentLibrary", - "version": "02.04.00.17", + "version": "02.04.00.18", "force_update": "0", "description": "Orca Filament Library", "filament_list": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA ABS @BBL H2D.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA ABS @BBL H2D.json index b02dd7539c..d656750212 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA ABS @BBL H2D.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA ABS @BBL H2D.json @@ -9,6 +9,7 @@ "65" ], "fan_max_speed": [ + "60", "60" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA ABS @BBL P1P.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA ABS @BBL P1P.json index 8210765185..f04ace20fe 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA ABS @BBL P1P.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA ABS @BBL P1P.json @@ -6,6 +6,7 @@ "setting_id": "MPYyfCKMsJZy8GaY", "instantiation": "true", "fan_max_speed": [ + "20", "20" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA ABS @BBL X1C.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA ABS @BBL X1C.json index 196053eb0a..c7d4e82ed6 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA ABS @BBL X1C.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA ABS @BBL X1C.json @@ -6,6 +6,7 @@ "setting_id": "vMA7EhgNDT4MwMRr", "instantiation": "true", "fan_max_speed": [ + "60", "60" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PEBA 90A @BBL H2D.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PEBA 90A @BBL H2D.json index a5b0a4e46b..a165447b05 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PEBA 90A @BBL H2D.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PEBA 90A @BBL H2D.json @@ -6,6 +6,7 @@ "setting_id": "91b8WTzvOky9wAVK", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ @@ -113,6 +114,7 @@ "1" ], "nozzle_temperature_range_high": [ + "250", "250" ], "nozzle_temperature": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG @BBL H2D 0.4 nozzle.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG @BBL H2D 0.4 nozzle.json index c508f9041c..057c9f8242 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG @BBL H2D 0.4 nozzle.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG @BBL H2D 0.4 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "KnKvUsD4rSpQU413", "instantiation": "true", "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG @BBL X1C.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG @BBL X1C.json index 5547610fcf..04710b1fa5 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG @BBL X1C.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG @BBL X1C.json @@ -6,6 +6,7 @@ "setting_id": "uM6wHdt0N5ftZRge", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Fluorescence @BBL H2D 0.4 nozzle.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Fluorescence @BBL H2D 0.4 nozzle.json index 19f8879c41..7e48c959e7 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Fluorescence @BBL H2D 0.4 nozzle.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Fluorescence @BBL H2D 0.4 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "JHg4y0pxAOtoJSuw", "instantiation": "true", "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Fluorescence @BBL X1C.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Fluorescence @BBL X1C.json index 353498bf5c..4912d88dab 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Fluorescence @BBL X1C.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Fluorescence @BBL X1C.json @@ -6,6 +6,7 @@ "setting_id": "pCetfFvyLLw3vS2j", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Glitter @BBL H2D 0.4 nozzle.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Glitter @BBL H2D 0.4 nozzle.json index 7d4b3bb5d5..fedcc166de 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Glitter @BBL H2D 0.4 nozzle.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Glitter @BBL H2D 0.4 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "ijW4zvNAQ3DIK984", "instantiation": "true", "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Glitter @BBL X1C.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Glitter @BBL X1C.json index 18c99695f4..c8a290d952 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Glitter @BBL X1C.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Glitter @BBL X1C.json @@ -6,6 +6,7 @@ "setting_id": "UI220qldPoLY8VZN", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Glow @BBL H2D 0.4 nozzle.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Glow @BBL H2D 0.4 nozzle.json index 9196aff31b..40643ae178 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Glow @BBL H2D 0.4 nozzle.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Glow @BBL H2D 0.4 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "3s9w3mH6M4OdbrUV", "instantiation": "true", "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Glow @BBL X1C.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Glow @BBL X1C.json index 42a4521212..789ea04781 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Glow @BBL X1C.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Glow @BBL X1C.json @@ -6,6 +6,7 @@ "setting_id": "wv5FRKdnsHaIFA7L", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Gradient @BBL H2D 0.4 nozzle.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Gradient @BBL H2D 0.4 nozzle.json index b468069cb7..794d273b55 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Gradient @BBL H2D 0.4 nozzle.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Gradient @BBL H2D 0.4 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "GqmCuPg6baUxeoYS", "instantiation": "true", "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Gradient @BBL X1C.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Gradient @BBL X1C.json index 48c3f30535..64edc74c5a 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Gradient @BBL X1C.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Gradient @BBL X1C.json @@ -6,6 +6,7 @@ "setting_id": "oYJjUiaCI4UBZ0ur", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG HF @BBL H2D 0.4 nozzle.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG HF @BBL H2D 0.4 nozzle.json index f8bc99dd97..1550a34be4 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG HF @BBL H2D 0.4 nozzle.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG HF @BBL H2D 0.4 nozzle.json @@ -18,6 +18,7 @@ "20" ], "fan_min_speed": [ + "20", "20" ], "filament_deretraction_speed": [ @@ -109,6 +110,7 @@ "0" ], "fan_max_speed": [ + "30", "30" ], "hole_coef_2": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG HF @BBL X1C.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG HF @BBL X1C.json index d67f8d619e..b58c82e3f2 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG HF @BBL X1C.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG HF @BBL X1C.json @@ -9,6 +9,7 @@ "20" ], "fan_min_speed": [ + "20", "20" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Heat Color Change @BBL H2D 0.4 nozzle.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Heat Color Change @BBL H2D 0.4 nozzle.json index 5f6706766a..1bae511a03 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Heat Color Change @BBL H2D 0.4 nozzle.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Heat Color Change @BBL H2D 0.4 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "DpWgpTiKdm70Xt1i", "instantiation": "true", "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Heat Color Change @BBL X1C.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Heat Color Change @BBL X1C.json index b0e0ee39ca..59bb2ed2b0 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Heat Color Change @BBL X1C.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Heat Color Change @BBL X1C.json @@ -6,6 +6,7 @@ "setting_id": "3lJe6ftSHQDkQZ1S", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Marble @BBL H2D 0.4 nozzle.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Marble @BBL H2D 0.4 nozzle.json index decfcb73a1..9597d6cf2c 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Marble @BBL H2D 0.4 nozzle.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Marble @BBL H2D 0.4 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "RxCTxdvym4iNOB5j", "instantiation": "true", "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Marble @BBL X1C.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Marble @BBL X1C.json index 49ac9206f0..92d80895bf 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Marble @BBL X1C.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Marble @BBL X1C.json @@ -6,6 +6,7 @@ "setting_id": "3BgsPAgilo1MgtOV", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Matte @BBL H2D 0.4 nozzle.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Matte @BBL H2D 0.4 nozzle.json index 3287a32971..f092a437d8 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Matte @BBL H2D 0.4 nozzle.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Matte @BBL H2D 0.4 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "sqz8Vluq7GMd3siv", "instantiation": "true", "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Matte @BBL X1C.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Matte @BBL X1C.json index d8f5548ba0..432feb1f44 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Matte @BBL X1C.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Matte @BBL X1C.json @@ -6,6 +6,7 @@ "setting_id": "NXgqYCqHeVCgFF3I", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Metallic @BBL H2D 0.4 nozzle.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Metallic @BBL H2D 0.4 nozzle.json index c04e4513b5..5493034671 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Metallic @BBL H2D 0.4 nozzle.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Metallic @BBL H2D 0.4 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "giq8BW6qcwz91e4C", "instantiation": "true", "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Metallic @BBL X1C.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Metallic @BBL X1C.json index eaa3a9dae2..bd40641cf8 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Metallic @BBL X1C.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG Metallic @BBL X1C.json @@ -6,6 +6,7 @@ "setting_id": "tZzoc2LdgWkpEiox", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG UV Color Change @BBL H2D 0.4 nozzle.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG UV Color Change @BBL H2D 0.4 nozzle.json index 03f0000a35..664acba408 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG UV Color Change @BBL H2D 0.4 nozzle.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG UV Color Change @BBL H2D 0.4 nozzle.json @@ -6,9 +6,11 @@ "setting_id": "Ic0NUfwLmR0NSh5q", "instantiation": "true", "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG UV Color Change @BBL X1C.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG UV Color Change @BBL X1C.json index 32eaa333cf..f379f7822c 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG UV Color Change @BBL X1C.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG UV Color Change @BBL X1C.json @@ -6,6 +6,7 @@ "setting_id": "BUKCpukEUmocfz3v", "instantiation": "true", "fan_min_speed": [ + "20", "20" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-CF @BBL H2D 0.4 nozzle.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-CF @BBL H2D 0.4 nozzle.json index 22af1e06ff..2baf05c347 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-CF @BBL H2D 0.4 nozzle.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-CF @BBL H2D 0.4 nozzle.json @@ -18,9 +18,11 @@ "30" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "5", "5" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-CF @BBL P1P 0.4 nozzle.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-CF @BBL P1P 0.4 nozzle.json index f1a7334810..818f5cfa7f 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-CF @BBL P1P 0.4 nozzle.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-CF @BBL P1P 0.4 nozzle.json @@ -9,9 +9,11 @@ "30" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "5", "5" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-CF @BBL X1C 0.4 nozzle.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-CF @BBL X1C 0.4 nozzle.json index 273260a31f..59a3c628e1 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-CF @BBL X1C 0.4 nozzle.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-CF @BBL X1C 0.4 nozzle.json @@ -9,9 +9,11 @@ "30" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "5", "5" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-GF @BBL H2D 0.4 nozzle.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-GF @BBL H2D 0.4 nozzle.json index 277d1915b1..7b819b21b9 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-GF @BBL H2D 0.4 nozzle.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-GF @BBL H2D 0.4 nozzle.json @@ -18,9 +18,11 @@ "30" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "5", "5" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-GF @BBL P1P 0.4 nozzle.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-GF @BBL P1P 0.4 nozzle.json index a94fc48630..ec1c4d0d6c 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-GF @BBL P1P 0.4 nozzle.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-GF @BBL P1P 0.4 nozzle.json @@ -9,9 +9,11 @@ "30" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "5", "5" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-GF @BBL X1C 0.4 nozzle.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-GF @BBL X1C 0.4 nozzle.json index db3ecc90d9..d17270dc91 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-GF @BBL X1C 0.4 nozzle.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PETG-GF @BBL X1C 0.4 nozzle.json @@ -9,9 +9,11 @@ "30" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "5", "5" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Basic @BBL H2D.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Basic @BBL H2D.json index 97e462a487..0ee70ddaa9 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Basic @BBL H2D.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Basic @BBL H2D.json @@ -6,6 +6,7 @@ "setting_id": "LZBgJ5xFtzrt3v02", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -24,9 +25,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Basic @BBL P1P.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Basic @BBL P1P.json index 99214e744d..2630b20b48 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Basic @BBL P1P.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Basic @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Chameleon @BBL P1P.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Chameleon @BBL P1P.json index ea66c51982..07a08de4ae 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Chameleon @BBL P1P.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Chameleon @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Fluorescence @BBL P1P.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Fluorescence @BBL P1P.json index a786a7aa36..d03578a040 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Fluorescence @BBL P1P.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Fluorescence @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Glitter @BBL P1P.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Glitter @BBL P1P.json index c7cb14bea3..c938541308 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Glitter @BBL P1P.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Glitter @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Glow @BBL P1P.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Glow @BBL P1P.json index f19705f369..7054e30d3c 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Glow @BBL P1P.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Glow @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Gradient @BBL P1P.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Gradient @BBL P1P.json index 15b6d22f5e..5797b492b0 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Gradient @BBL P1P.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Gradient @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Heat Color Change @BBL P1P.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Heat Color Change @BBL P1P.json index b789e28638..6ba21121f6 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Heat Color Change @BBL P1P.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Heat Color Change @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA High Speed @BBL P1P.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA High Speed @BBL P1P.json index b2f9e5ad9d..741ec66d99 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA High Speed @BBL P1P.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA High Speed @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA High Temp @BBL H2D.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA High Temp @BBL H2D.json index 4047850988..d47c4fddb6 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA High Temp @BBL H2D.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA High Temp @BBL H2D.json @@ -6,6 +6,7 @@ "setting_id": "5F1rml3AGNMD6F1g", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Marble @BBL P1P.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Marble @BBL P1P.json index a198637db6..eccd20aeeb 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Marble @BBL P1P.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Marble @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Matte @BBL H2D.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Matte @BBL H2D.json index bc595e11b5..cb47647899 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Matte @BBL H2D.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Matte @BBL H2D.json @@ -6,6 +6,7 @@ "setting_id": "Zw3CZVyBon930GCH", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -27,9 +28,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Matte @BBL P1P.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Matte @BBL P1P.json index 6331679d0c..aaee41348e 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Matte @BBL P1P.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Matte @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Metal @BBL P1P.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Metal @BBL P1P.json index 512647c8ac..bd9857150f 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Metal @BBL P1P.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Metal @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_max_volumetric_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Metallic @BBL P1P.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Metallic @BBL P1P.json index cdb7e0a72d..3438bd4c8c 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Metallic @BBL P1P.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Metallic @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_max_volumetric_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA PRO @BBL H2D.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA PRO @BBL H2D.json index 605e1e9a37..b82556d9db 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA PRO @BBL H2D.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA PRO @BBL H2D.json @@ -6,6 +6,7 @@ "setting_id": "Mth7IAdxkWf7pSSj", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -24,9 +25,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA PRO @BBL P1P.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA PRO @BBL P1P.json index 35d88a6d9f..8f85e07ab2 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA PRO @BBL P1P.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA PRO @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Silk @BBL H2D.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Silk @BBL H2D.json index 943bdfaf11..76525381a6 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Silk @BBL H2D.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Silk @BBL H2D.json @@ -6,6 +6,7 @@ "setting_id": "qXuac1ixZ4VdWMyA", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Silk+ @BBL H2D.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Silk+ @BBL H2D.json index 35d9da0d5c..da4b170101 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Silk+ @BBL H2D.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Silk+ @BBL H2D.json @@ -6,6 +6,7 @@ "setting_id": "4aj9YjpmJDQoTIv7", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "eng_plate_temp": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Transparent @BBL P1P.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Transparent @BBL P1P.json index 3ab7af716f..b009240a89 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Transparent @BBL P1P.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Transparent @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_retract_before_wipe": [ @@ -110,6 +111,7 @@ "0" ], "nozzle_temperature_range_low": [ + "200", "200" ], "nozzle_temperature": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Transparent @BBL X1C.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Transparent @BBL X1C.json index 0c564fe5d0..5d3cafd4c7 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Transparent @BBL X1C.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Transparent @BBL X1C.json @@ -99,6 +99,7 @@ "0" ], "nozzle_temperature_range_low": [ + "200", "200" ], "nozzle_temperature": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA UV Color Change @BBL P1P.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA UV Color Change @BBL P1P.json index 15309bbca5..fee733bc53 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA UV Color Change @BBL P1P.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA UV Color Change @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Wood @BBL P1P.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Wood @BBL P1P.json index 62d5a28a78..09fb339399 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Wood @BBL P1P.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Wood @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_flow_ratio": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Youth @BBL H2D.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Youth @BBL H2D.json index 4b1b4b9a50..cc5a192fda 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Youth @BBL H2D.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Youth @BBL H2D.json @@ -6,6 +6,7 @@ "setting_id": "ZtgqauY30xyhRIB3", "instantiation": "true", "additional_cooling_fan_speed": [ + "75", "75" ], "counter_coef_2": [ @@ -27,9 +28,11 @@ "55" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "60", "60" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Youth @BBL P1P.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Youth @BBL P1P.json index a40b68fbc3..14995c7334 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Youth @BBL P1P.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA Youth @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA-CF @BBL P1P.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA-CF @BBL P1P.json index d7e1cf9423..a5015a81c3 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA-CF @BBL P1P.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA PLA-CF @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA TPU 90A @BBL H2D.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA TPU 90A @BBL H2D.json index 8eb8e80392..d1533707ee 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA TPU 90A @BBL H2D.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA TPU 90A @BBL H2D.json @@ -6,6 +6,7 @@ "setting_id": "lm5PEdmOJODAz6KJ", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ @@ -113,6 +114,7 @@ "1" ], "nozzle_temperature_range_high": [ + "250", "250" ], "nozzle_temperature": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA TPU 95A @BBL H2D.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA TPU 95A @BBL H2D.json index 62cdeadad1..dad06c4758 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA TPU 95A @BBL H2D.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA TPU 95A @BBL H2D.json @@ -6,6 +6,7 @@ "setting_id": "k6clcTFJXTGVdmZg", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA TPU 98A @BBL H2D.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA TPU 98A @BBL H2D.json index ab3c0989cb..eb4cdc710e 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA TPU 98A @BBL H2D.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA TPU 98A @BBL H2D.json @@ -6,6 +6,7 @@ "setting_id": "p0qjTRymaVctbU11", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA TPU Matte @BBL H2D.json b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA TPU Matte @BBL H2D.json index a1571446ab..ccb5715f92 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA TPU Matte @BBL H2D.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/BETA/BBL/BETA TPU Matte @BBL H2D.json @@ -6,6 +6,7 @@ "setting_id": "PeJPI88l3Ui7jZnj", "instantiation": "true", "additional_cooling_fan_speed": [ + "100", "100" ], "eng_plate_temp": [ @@ -113,6 +114,7 @@ "1" ], "nozzle_temperature_range_high": [ + "250", "250" ], "nozzle_temperature": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu ABS @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu ABS @System.json index 9a66aa85db..6409e3aaa7 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu ABS @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu ABS @System.json @@ -6,6 +6,11 @@ "setting_id": "iXiD2dx3qXFVPHRi", "instantiation": "true", "fan_max_speed": [ + "60", + "60", + "60", + "60", + "60", "60" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu ASA @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu ASA @System.json index 0706ff5cc5..85d822e0e7 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu ASA @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu ASA @System.json @@ -6,6 +6,11 @@ "setting_id": "NEYuPnFvaHGhAdpD", "instantiation": "true", "fan_min_speed": [ + "25", + "25", + "25", + "25", + "25", "25" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PA-CF @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PA-CF @System.json index a37d362c30..8ff5ce30ef 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PA-CF @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PA-CF @System.json @@ -22,9 +22,19 @@ "290" ], "fan_max_speed": [ + "30", + "30", + "30", + "30", + "30", "30" ], "fan_min_speed": [ + "10", + "10", + "10", + "10", + "10", "10" ], "fan_cooling_layer_time": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PC @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PC @System.json index cf5cc74a10..f11e3cc8bb 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PC @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PC @System.json @@ -6,6 +6,11 @@ "setting_id": "piqS3ekvyUfN9dgG", "instantiation": "true", "fan_max_speed": [ + "40", + "40", + "40", + "40", + "40", "40" ], "slow_down_layer_time": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PC FR @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PC FR @System.json index a3abfb5bd5..3f650afda5 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PC FR @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PC FR @System.json @@ -6,6 +6,11 @@ "setting_id": "m7TaVFWJZUqzf0rP", "instantiation": "true", "fan_max_speed": [ + "40", + "40", + "40", + "40", + "40", "40" ], "slow_down_layer_time": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PETG HF @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PETG HF @System.json index 453ccc6400..97c8436453 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PETG HF @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PETG HF @System.json @@ -9,6 +9,11 @@ "20" ], "fan_min_speed": [ + "20", + "20", + "20", + "20", + "20", "20" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PETG-CF @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PETG-CF @System.json index 28ae43ae59..f5590b772d 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PETG-CF @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PETG-CF @System.json @@ -9,9 +9,19 @@ "30" ], "fan_max_speed": [ + "40", + "40", + "40", + "40", + "40", "40" ], "fan_min_speed": [ + "5", + "5", + "5", + "5", + "5", "5" ], "filament_long_retractions_when_cut": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PLA Aero @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PLA Aero @System.json index 511914839a..28ae999df2 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PLA Aero @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PLA Aero @System.json @@ -6,6 +6,11 @@ "setting_id": "R499nyjJA5r7khOd", "instantiation": "true", "additional_cooling_fan_speed": [ + "0", + "0", + "0", + "0", + "0", "0" ], "slow_down_layer_time": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Generic PA-CF @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Generic PA-CF @System.json index fdef334123..74036889c0 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Generic PA-CF @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Generic PA-CF @System.json @@ -19,6 +19,11 @@ "PA-CF" ], "filament_multitool_ramming_flow": [ + "8", + "8", + "8", + "8", + "8", "8" ], "compatible_printers": [] diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Generic PETG HF @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Generic PETG HF @System.json index e46645a5e1..3c787ca1d8 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Generic PETG HF @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Generic PETG HF @System.json @@ -14,6 +14,11 @@ "21" ], "filament_multitool_ramming_flow": [ + "20", + "20", + "20", + "20", + "20", "20" ], "compatible_printers": [] diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Generic PETG-CF @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Generic PETG-CF @System.json index 4127e51392..f7536223af 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Generic PETG-CF @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Generic PETG-CF @System.json @@ -9,9 +9,19 @@ "30" ], "fan_max_speed": [ + "40", + "40", + "40", + "40", + "40", "40" ], "fan_min_speed": [ + "5", + "5", + "5", + "5", + "5", "5" ], "filament_max_volumetric_speed": [ @@ -23,6 +33,11 @@ "11.5" ], "filament_multitool_ramming_flow": [ + "13", + "13", + "13", + "13", + "13", "13" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Generic PLA High Speed @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Generic PLA High Speed @System.json index 303ad6ac2f..90aee0b556 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Generic PLA High Speed @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Generic PLA High Speed @System.json @@ -31,6 +31,11 @@ "30" ], "filament_multitool_ramming_flow": [ + "25", + "25", + "25", + "25", + "25", "25" ], "slow_down_layer_time": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Generic PLA Matte @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Generic PLA Matte @System.json index a7dd869115..800f9f4e0a 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Generic PLA Matte @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Generic PLA Matte @System.json @@ -14,6 +14,11 @@ "25" ], "filament_multitool_ramming_flow": [ + "11", + "11", + "11", + "11", + "11", "11" ], "filament_retraction_length": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Generic PP-CF @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Generic PP-CF @System.json index 3e55ce0703..4c035469ed 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Generic PP-CF @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Generic PP-CF @System.json @@ -21,6 +21,11 @@ "6" ], "filament_multitool_ramming_flow": [ + "6", + "6", + "6", + "6", + "6", "6" ], "filament_type": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Generic PP-GF @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Generic PP-GF @System.json index 101c2be4e7..c24caddef0 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Generic PP-GF @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Generic PP-GF @System.json @@ -21,6 +21,11 @@ "6" ], "filament_multitool_ramming_flow": [ + "6", + "6", + "6", + "6", + "6", "6" ], "filament_type": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Generic PPA-CF @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Generic PPA-CF @System.json index 33c6747a68..a909c0bc38 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Generic PPA-CF @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Generic PPA-CF @System.json @@ -7,6 +7,11 @@ "filament_id": "OF8tPByX", "instantiation": "true", "fan_max_speed": [ + "35", + "35", + "35", + "35", + "35", "35" ], "filament_extruder_variant": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PA6-66 @BBL P2S.json b/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PA6-66 @BBL P2S.json index e421095237..cbd4beb609 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PA6-66 @BBL P2S.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PA6-66 @BBL P2S.json @@ -10,6 +10,7 @@ "1" ], "additional_cooling_fan_speed": [ + "70", "70" ], "close_fan_the_first_x_layers": [ @@ -42,9 +43,11 @@ "20" ], "fan_max_speed": [ + "100", "100" ], "fan_min_speed": [ + "60", "60" ], "filament_adaptive_volumetric_speed": [ @@ -137,6 +140,7 @@ "12" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_pre_cooling_temperature": [ @@ -222,9 +226,11 @@ "255" ], "nozzle_temperature_range_high": [ + "280", "280" ], "nozzle_temperature_range_low": [ + "250", "250" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PA6-66 @P1-X1.json b/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PA6-66 @P1-X1.json index 6a01ae3255..96a607e349 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PA6-66 @P1-X1.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PA6-66 @P1-X1.json @@ -66,6 +66,7 @@ "12" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_retraction_minimum_travel": [ @@ -119,9 +120,11 @@ "2" ], "fan_max_speed": [ + "100", "100" ], "fan_min_speed": [ + "60", "60" ], "slow_down_min_speed": [ @@ -200,6 +203,7 @@ "0" ], "additional_cooling_fan_speed": [ + "70", "70" ], "compatible_printers": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PA6-CF @BBL P2S.json b/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PA6-CF @BBL P2S.json index 2e2b7b82b5..3a8fad133d 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PA6-CF @BBL P2S.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PA6-CF @BBL P2S.json @@ -10,6 +10,7 @@ "1" ], "additional_cooling_fan_speed": [ + "70", "70" ], "close_fan_the_first_x_layers": [ @@ -42,9 +43,11 @@ "20" ], "fan_max_speed": [ + "100", "100" ], "fan_min_speed": [ + "60", "60" ], "filament_adaptive_volumetric_speed": [ @@ -137,6 +140,7 @@ "12" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_pre_cooling_temperature": [ @@ -222,9 +226,11 @@ "280" ], "nozzle_temperature_range_high": [ + "290", "290" ], "nozzle_temperature_range_low": [ + "270", "270" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PA6-CF @P1-X1.json b/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PA6-CF @P1-X1.json index b50fbd38e9..87c0461e53 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PA6-CF @P1-X1.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PA6-CF @P1-X1.json @@ -66,6 +66,7 @@ "12" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_retraction_minimum_travel": [ @@ -119,9 +120,11 @@ "2" ], "fan_max_speed": [ + "100", "100" ], "fan_min_speed": [ + "60", "60" ], "slow_down_min_speed": [ @@ -200,6 +203,7 @@ "0" ], "additional_cooling_fan_speed": [ + "70", "70" ], "compatible_printers": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PETG Pro @BBL P2S.json b/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PETG Pro @BBL P2S.json index 7522dfad87..5cfadde8ee 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PETG Pro @BBL P2S.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PETG Pro @BBL P2S.json @@ -21,9 +21,11 @@ "25" ], "fan_max_speed": [ + "100", "100" ], "fan_min_speed": [ + "60", "60" ], "filament_flow_ratio": [ @@ -41,6 +43,7 @@ "70" ], "nozzle_temperature_range_high": [ + "260", "260" ], "overhang_fan_speed": [ @@ -182,6 +185,7 @@ "0" ], "additional_cooling_fan_speed": [ + "70", "70" ], "close_fan_the_first_x_layers": [ @@ -262,6 +266,7 @@ "0" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_prime_volume": [ @@ -305,6 +310,7 @@ "10" ], "nozzle_temperature_range_low": [ + "240", "240" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PETG Pro @P1-X1.json b/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PETG Pro @P1-X1.json index 9269f53bc5..1e441cfcb9 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PETG Pro @P1-X1.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PETG Pro @P1-X1.json @@ -66,6 +66,7 @@ "12" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_retraction_minimum_travel": [ @@ -119,9 +120,11 @@ "2" ], "fan_max_speed": [ + "100", "100" ], "fan_min_speed": [ + "60", "60" ], "slow_down_min_speed": [ @@ -200,6 +203,7 @@ "0" ], "additional_cooling_fan_speed": [ + "70", "70" ], "compatible_printers": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PLA Pro @BBL P2S.json b/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PLA Pro @BBL P2S.json index b9a4e0b377..443fd76caa 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PLA Pro @BBL P2S.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PLA Pro @BBL P2S.json @@ -176,12 +176,15 @@ "55" ], "fan_min_speed": [ + "60", "60" ], "fan_max_speed": [ + "100", "100" ], "additional_cooling_fan_speed": [ + "70", "70" ], "compatible_printers": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PLA Pro @P1-X1.json b/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PLA Pro @P1-X1.json index 52cbbd3596..90ce6f5192 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PLA Pro @P1-X1.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/INSLOGIC/BBL/INSLOGIC PLA Pro @P1-X1.json @@ -66,6 +66,7 @@ "12" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_retraction_minimum_travel": [ @@ -119,9 +120,11 @@ "2" ], "fan_max_speed": [ + "100", "100" ], "fan_min_speed": [ + "60", "60" ], "slow_down_min_speed": [ @@ -200,6 +203,7 @@ "0" ], "additional_cooling_fan_speed": [ + "70", "70" ], "compatible_printers": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/JAYO/BBL/JAYO PETG @BBL P2S 0.4 nozzle.json b/resources/profiles/OrcaFilamentLibrary/filament/JAYO/BBL/JAYO PETG @BBL P2S 0.4 nozzle.json index fc70479c7b..afced56591 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/JAYO/BBL/JAYO PETG @BBL P2S 0.4 nozzle.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/JAYO/BBL/JAYO PETG @BBL P2S 0.4 nozzle.json @@ -48,6 +48,7 @@ "25" ], "additional_cooling_fan_speed": [ + "70", "70" ], "slow_down_layer_time": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Numakers/BBL/Numakers PLA+ @BBL P1P 0.2 nozzle.json b/resources/profiles/OrcaFilamentLibrary/filament/Numakers/BBL/Numakers PLA+ @BBL P1P 0.2 nozzle.json index f2c1c78883..e6242c5aa0 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Numakers/BBL/Numakers PLA+ @BBL P1P 0.2 nozzle.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Numakers/BBL/Numakers PLA+ @BBL P1P 0.2 nozzle.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_max_volumetric_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Numakers/BBL/Numakers PLA+ @BBL P1P.json b/resources/profiles/OrcaFilamentLibrary/filament/Numakers/BBL/Numakers PLA+ @BBL P1P.json index a9e5da69b6..988868d757 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Numakers/BBL/Numakers PLA+ @BBL P1P.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Numakers/BBL/Numakers PLA+ @BBL P1P.json @@ -9,6 +9,7 @@ "80" ], "fan_min_speed": [ + "50", "50" ], "filament_deretraction_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Polymaker/BBL/Fiberon PETG-ESD @BBL X1.json b/resources/profiles/OrcaFilamentLibrary/filament/Polymaker/BBL/Fiberon PETG-ESD @BBL X1.json index 99fcec0536..943ad93ee4 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Polymaker/BBL/Fiberon PETG-ESD @BBL X1.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Polymaker/BBL/Fiberon PETG-ESD @BBL X1.json @@ -19,6 +19,7 @@ "Cool Plate" ], "fan_max_speed": [ + "80", "80" ], "temperature_vitrification": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Polymaker/BBL/Fiberon PETG-rCF08 @BBL X1.json b/resources/profiles/OrcaFilamentLibrary/filament/Polymaker/BBL/Fiberon PETG-rCF08 @BBL X1.json index d564918a1e..00c9cbc3d3 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Polymaker/BBL/Fiberon PETG-rCF08 @BBL X1.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Polymaker/BBL/Fiberon PETG-rCF08 @BBL X1.json @@ -15,6 +15,7 @@ "nil" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_retraction_minimum_travel": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth ABS rABS.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth ABS rABS.json index b9ee6494c5..9c41c99100 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth ABS rABS.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth ABS rABS.json @@ -11,6 +11,7 @@ "1" ], "additional_cooling_fan_speed": [ + "0", "0" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "30" ], "fan_max_speed": [ + "25", "25" ], "fan_min_speed": [ + "0", "0" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "12" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.6 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "250" ], "nozzle_temperature_range_high": [ + "260", "260" ], "nozzle_temperature_range_low": [ + "230", "230" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA Adura FDA.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA Adura FDA.json index 8532e514a3..b9cfdd4fa7 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA Adura FDA.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA Adura FDA.json @@ -11,6 +11,7 @@ "1" ], "additional_cooling_fan_speed": [ + "0", "0" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "5" ], "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "10", "10" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "11" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.6 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "265" ], "nozzle_temperature_range_high": [ + "270", "270" ], "nozzle_temperature_range_low": [ + "245", "245" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA Adura.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA Adura.json index c7690e1178..7b5f99ee60 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA Adura.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA Adura.json @@ -11,6 +11,7 @@ "1" ], "additional_cooling_fan_speed": [ + "0", "0" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "5" ], "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "10", "10" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "11" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.6 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "265" ], "nozzle_temperature_range_high": [ + "270", "270" ], "nozzle_temperature_range_low": [ + "245", "245" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA-CF Adura X.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA-CF Adura X.json index 7dab608a6f..bb38a38852 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA-CF Adura X.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA-CF Adura X.json @@ -11,6 +11,7 @@ "1" ], "additional_cooling_fan_speed": [ + "0", "0" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "5" ], "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "10", "10" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "15" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.6 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "275" ], "nozzle_temperature_range_high": [ + "280", "280" ], "nozzle_temperature_range_low": [ + "260", "260" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA6 Addlantis.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA6 Addlantis.json index 6e068507f1..f8da458e21 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA6 Addlantis.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA6 Addlantis.json @@ -11,6 +11,7 @@ "1" ], "additional_cooling_fan_speed": [ + "0", "0" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "5" ], "fan_max_speed": [ + "30", "30" ], "fan_min_speed": [ + "10", "10" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "11" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.62 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "275" ], "nozzle_temperature_range_high": [ + "285", "285" ], "nozzle_temperature_range_low": [ + "265", "265" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PC BLend HT LCF.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PC BLend HT LCF.json index de13a67d83..a58028b69f 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PC BLend HT LCF.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PC BLend HT LCF.json @@ -11,6 +11,7 @@ "0" ], "additional_cooling_fan_speed": [ + "0", "0" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "30" ], "fan_max_speed": [ + "40", "40" ], "fan_min_speed": [ + "40", "40" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "15" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.6 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "275" ], "nozzle_temperature_range_high": [ + "290", "290" ], "nozzle_temperature_range_low": [ + "265", "265" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Base.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Base.json index b1120899d7..3fdf2fee27 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Base.json @@ -11,6 +11,7 @@ "0" ], "additional_cooling_fan_speed": [ + "0", "0" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "30" ], "fan_max_speed": [ + "90", "90" ], "fan_min_speed": [ + "40", "40" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "10" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.6 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "255" ], "nozzle_temperature_range_high": [ + "275", "275" ], "nozzle_temperature_range_low": [ + "240", "240" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG ESD.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG ESD.json index cc7012c75d..25e253f757 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG ESD.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG ESD.json @@ -11,6 +11,7 @@ "0" ], "additional_cooling_fan_speed": [ + "0", "0" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "30" ], "fan_max_speed": [ + "90", "90" ], "fan_min_speed": [ + "40", "40" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "20" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.6 high reliability - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "290" ], "nozzle_temperature_range_high": [ + "270", "270" ], "nozzle_temperature_range_low": [ + "255", "255" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Economy.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Economy.json index 48119c7b04..bd7e0764f7 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Economy.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Economy.json @@ -11,6 +11,7 @@ "0" ], "additional_cooling_fan_speed": [ + "0", "0" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "30" ], "fan_max_speed": [ + "90", "90" ], "fan_min_speed": [ + "40", "40" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "10" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.6 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "255" ], "nozzle_temperature_range_high": [ + "275", "275" ], "nozzle_temperature_range_low": [ + "240", "240" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Flame v0.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Flame v0.json index 0650202cea..cb94413ef6 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Flame v0.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Flame v0.json @@ -11,6 +11,7 @@ "0" ], "additional_cooling_fan_speed": [ + "0", "0" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "30" ], "fan_max_speed": [ + "90", "90" ], "fan_min_speed": [ + "40", "40" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "20" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.4 high reliability - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "290" ], "nozzle_temperature_range_high": [ + "270", "270" ], "nozzle_temperature_range_low": [ + "255", "255" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG PRO Matte.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG PRO Matte.json index 978db741a7..52da3757e3 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG PRO Matte.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG PRO Matte.json @@ -11,6 +11,7 @@ "0" ], "additional_cooling_fan_speed": [ + "0", "0" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "30" ], "fan_max_speed": [ + "90", "90" ], "fan_min_speed": [ + "40", "40" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "8" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.6 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "260" ], "nozzle_temperature_range_high": [ + "275", "275" ], "nozzle_temperature_range_low": [ + "235", "235" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG rPETG Matte.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG rPETG Matte.json index 8514e62315..159fcca4d7 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG rPETG Matte.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG rPETG Matte.json @@ -11,6 +11,7 @@ "0" ], "additional_cooling_fan_speed": [ + "0", "0" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "30" ], "fan_max_speed": [ + "90", "90" ], "fan_min_speed": [ + "40", "40" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "10" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.6 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "255" ], "nozzle_temperature_range_high": [ + "260", "260" ], "nozzle_temperature_range_low": [ + "225", "225" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG-CF Rigid X.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG-CF Rigid X.json index d2789e2d15..b62613ae92 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG-CF Rigid X.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG-CF Rigid X.json @@ -11,6 +11,7 @@ "0" ], "additional_cooling_fan_speed": [ + "100", "100" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "30" ], "fan_max_speed": [ + "100", "100" ], "fan_min_speed": [ + "5", "5" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "11" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.6 - V.Pack:18_05_2026 – Special thanks to Mats.Z (3DP24, Jonkoping University / Campus Varnamo) for profile improvements during internship autumn 2025.", @@ -299,9 +303,11 @@ "270" ], "nozzle_temperature_range_high": [ + "275", "275" ], "nozzle_temperature_range_low": [ + "245", "245" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA E-PLA.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA E-PLA.json index b92f8c27b5..1cf251f555 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA E-PLA.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA E-PLA.json @@ -11,6 +11,7 @@ "0" ], "additional_cooling_fan_speed": [ + "70", "70" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "100" ], "fan_max_speed": [ + "100", "100" ], "fan_min_speed": [ + "100", "100" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "12" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.7 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "235" ], "nozzle_temperature_range_high": [ + "240", "240" ], "nozzle_temperature_range_low": [ + "190", "190" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Economy.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Economy.json index de0cdb9f05..4271837beb 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Economy.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Economy.json @@ -11,6 +11,7 @@ "0" ], "additional_cooling_fan_speed": [ + "70", "70" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "100" ], "fan_max_speed": [ + "100", "100" ], "fan_min_speed": [ + "100", "100" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "12" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.6 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "235" ], "nozzle_temperature_range_high": [ + "240", "240" ], "nozzle_temperature_range_low": [ + "200", "200" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA HT-PLA PRO Matte.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA HT-PLA PRO Matte.json index e8e2fca28a..03f7f6ff13 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA HT-PLA PRO Matte.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA HT-PLA PRO Matte.json @@ -11,6 +11,7 @@ "0" ], "additional_cooling_fan_speed": [ + "70", "70" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "100" ], "fan_max_speed": [ + "100", "100" ], "fan_min_speed": [ + "100", "100" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "11" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.6 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "235" ], "nozzle_temperature_range_high": [ + "245", "245" ], "nozzle_temperature_range_low": [ + "210", "210" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Premium Silk.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Premium Silk.json index f3cb324a87..73c52acce6 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Premium Silk.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Premium Silk.json @@ -11,6 +11,7 @@ "0" ], "additional_cooling_fan_speed": [ + "50", "50" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "80" ], "fan_max_speed": [ + "90", "90" ], "fan_min_speed": [ + "70", "70" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "10" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.7 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "245" ], "nozzle_temperature_range_high": [ + "255", "255" ], "nozzle_temperature_range_low": [ + "235", "235" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Textura.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Textura.json index 9b49ebac26..bfe7676c25 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Textura.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Textura.json @@ -11,6 +11,7 @@ "0" ], "additional_cooling_fan_speed": [ + "70", "70" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "80" ], "fan_max_speed": [ + "100", "100" ], "fan_min_speed": [ + "100", "100" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "14" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.7 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "235" ], "nozzle_temperature_range_high": [ + "240", "240" ], "nozzle_temperature_range_low": [ + "190", "190" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Wood.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Wood.json index e29534a5ad..1be018e431 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Wood.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Wood.json @@ -11,6 +11,7 @@ "0" ], "additional_cooling_fan_speed": [ + "70", "70" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "100" ], "fan_max_speed": [ + "100", "100" ], "fan_min_speed": [ + "100", "100" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "8" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.7 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "230" ], "nozzle_temperature_range_high": [ + "230", "230" ], "nozzle_temperature_range_low": [ + "200", "200" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA X-PLA High Speed.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA X-PLA High Speed.json index 98a98c4536..cd8b897e5f 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA X-PLA High Speed.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA X-PLA High Speed.json @@ -11,6 +11,7 @@ "0" ], "additional_cooling_fan_speed": [ + "70", "70" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "100" ], "fan_max_speed": [ + "100", "100" ], "fan_min_speed": [ + "100", "100" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "12" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.7 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "235" ], "nozzle_temperature_range_high": [ + "240", "240" ], "nozzle_temperature_range_low": [ + "190", "190" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA X-PLA.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA X-PLA.json index def3487467..59aa588cd4 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA X-PLA.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA X-PLA.json @@ -11,6 +11,7 @@ "0" ], "additional_cooling_fan_speed": [ + "70", "70" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "100" ], "fan_max_speed": [ + "100", "100" ], "fan_min_speed": [ + "100", "100" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "14" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.7 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "230" ], "nozzle_temperature_range_high": [ + "240", "240" ], "nozzle_temperature_range_low": [ + "190", "190" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA rPLA RE-ADD.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA rPLA RE-ADD.json index ef614b39ae..0aa4ca73b7 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA rPLA RE-ADD.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA rPLA RE-ADD.json @@ -11,6 +11,7 @@ "0" ], "additional_cooling_fan_speed": [ + "70", "70" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "100" ], "fan_max_speed": [ + "100", "100" ], "fan_min_speed": [ + "100", "100" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "16" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.7 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "235" ], "nozzle_temperature_range_high": [ + "240", "240" ], "nozzle_temperature_range_low": [ + "190", "190" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PVDF Adamant S1.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PVDF Adamant S1.json index e0a99b8b05..96ba6ace88 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PVDF Adamant S1.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PVDF Adamant S1.json @@ -11,6 +11,7 @@ "1" ], "additional_cooling_fan_speed": [ + "0", "0" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "5" ], "fan_max_speed": [ + "80", "80" ], "fan_min_speed": [ + "25", "25" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "4" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.6 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "265" ], "nozzle_temperature_range_high": [ + "260", "260" ], "nozzle_temperature_range_low": [ + "240", "240" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU EasyFlex.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU EasyFlex.json index a1d9deab44..2b05356f4e 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU EasyFlex.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU EasyFlex.json @@ -11,6 +11,7 @@ "0" ], "additional_cooling_fan_speed": [ + "70", "70" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "100" ], "fan_max_speed": [ + "100", "100" ], "fan_min_speed": [ + "100", "100" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "5" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.7 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "235" ], "nozzle_temperature_range_high": [ + "250", "250" ], "nozzle_temperature_range_low": [ + "230", "230" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU Pro Matte 85A.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU Pro Matte 85A.json index e944d203a9..b14dd41fba 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU Pro Matte 85A.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU Pro Matte 85A.json @@ -11,6 +11,7 @@ "0" ], "additional_cooling_fan_speed": [ + "70", "70" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "100" ], "fan_max_speed": [ + "100", "100" ], "fan_min_speed": [ + "100", "100" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "5" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.2 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "250" ], "nozzle_temperature_range_high": [ + "250", "250" ], "nozzle_temperature_range_low": [ + "230", "230" ], "overhang_fan_speed": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU Pro Matte 95A.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU Pro Matte 95A.json index d0e272d902..c35736f4b5 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU Pro Matte 95A.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU Pro Matte 95A.json @@ -11,6 +11,7 @@ "0" ], "additional_cooling_fan_speed": [ + "70", "70" ], "chamber_temperatures": [ @@ -124,9 +125,11 @@ "100" ], "fan_max_speed": [ + "100", "100" ], "fan_min_speed": [ + "100", "100" ], "filament_adhesiveness_category": [ @@ -171,6 +174,7 @@ "5" ], "filament_minimal_purge_on_wipe_tower": [ + "15", "15" ], "filament_notes": "File Version:2.3 - V.Pack:18_05_2026", @@ -299,9 +303,11 @@ "235" ], "nozzle_temperature_range_high": [ + "250", "250" ], "nozzle_temperature_range_low": [ + "230", "230" ], "overhang_fan_speed": [ diff --git a/resources/shaders/110/texture_displacement_shaded.fs b/resources/shaders/110/texture_displacement_shaded.fs new file mode 100644 index 0000000000..4a2980cd64 --- /dev/null +++ b/resources/shaders/110/texture_displacement_shaded.fs @@ -0,0 +1,425 @@ +#version 110 + +// See resources/shaders/140/texture_displacement_shaded.fs for full documentation; this is the +// GLSL 1.10 compatibility variant (same logic, older syntax). + +#define INTENSITY_CORRECTION 0.6 + +#define PARALLAX_STEPS 24 +#define H_AT(uv) texture2D(height_tex, uv).r + +const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929); +#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SHININESS 20.0 + +const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074); +#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION) + +#define INTENSITY_AMBIENT 0.3 + +const vec3 ZERO = vec3(0.0, 0.0, 0.0); + +uniform vec4 uniform_color; +// The printable palette, in **CIELAB** as well as RGB, and how many entries are real. Lab because the +// match has to be perceptual - the same reason the CPU side uses CIEDE2000 - and converting the +// palette once on the CPU is what lets the fragment shader match with a plain squared distance. +// Count 0 means nothing is colouring, and every fragment falls back to uniform_color as before. +uniform vec3 palette_lab[64]; +uniform vec3 palette_rgb[64]; +uniform int palette_count; +uniform bool pure_only; // match against single filaments only (flat-colour image) +// How each entry prints. A pure entry is one filament (a == b); a mix interleaves filaments a and b, +// num parts of a in every den, and the print shows that interleave rather than the entry's average +// colour. The fragment resolves it exactly as GLGizmoTextureDisplacement::make_mix_resolver() does +// per triangle on the CPU, so the preview shows the pattern the bake will print. +uniform int palette_a[64]; +uniform int palette_b[64]; +uniform int palette_num[64]; +uniform int palette_den[64]; +uniform vec3 filament_rgb[16]; +uniform int filament_count; +uniform int mix_mode; // ColorMixMode: 0 Z bands, 1 XY dither, 2 auto +uniform float layer_height; // mm; one Z band per print layer +uniform float dither_cell; // mm; one XY dither cell +uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height +uniform bool has_color_tex; +uniform bool volume_mirrored; + +uniform mat4 view_model_matrix; +uniform mat3 view_normal_matrix; + +uniform sampler2D height_tex; +uniform vec2 height_tex_texel; +uniform float depth_mm; +uniform float tiling_scale; +// Height map width / height. Scales the v axis so a non-square image keeps its proportions +// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform(). +uniform float tex_aspect; +uniform float rotation_rad; +uniform vec2 uv_offset; +uniform bool invert; +uniform float midlevel; // the height that means "don't move"; needed by the parallax step +uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor) +uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin +uniform bool use_vertex_uv; +// 2x3 affine (lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the dragged island's uv; see the +// 140 variant. Identity when nothing is dragged. +uniform vec4 island_delta_lin; +uniform vec2 island_delta_tr; +// In-shader projection (0 Triplanar, 1 Cylindrical, 2 Spherical) and the painted patch's own frame the +// two wrapping ones wrap around, in the texture frame; see the 140 variant. +uniform int projection_mode; +uniform vec3 patch_center; +uniform vec3 patch_axis; + +varying vec3 clipping_planes_dots; +varying vec4 model_pos; +varying vec4 world_pos; +varying float weight; +varying float island_active; +varying vec2 vertex_uv; + +// The cylinder's own frame, built exactly as libslic3r's project_cylindrical() builds it - including +// the handedness, which comes out left-handed for an axis of +Z. Copied rather than "corrected", so +// the preview wraps the texture the same way round as the bake. +void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd) +{ + up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0); + vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); + right = normalize(cross(up, arbitrary)); + fwd = normalize(cross(right, up)); +} + +// The raw, millimetre-valued projection of `p` (a position in the texture frame), before the layer's +// tiling/rotation/aspect/offset - a term-for-term transcription of libslic3r's project_planar(), +// project_cylindrical() and project_spherical(). `n` is read by the planar mode only. +vec2 projection_raw(vec3 p, vec3 n) +{ + if (projection_mode == 1) { // Cylindrical: (arc length around, distance along) + vec3 up, right, fwd; + cylinder_frame(up, right, fwd); + vec3 rel = p - patch_center; + float x = dot(rel, right); + float y = dot(rel, fwd); + return vec2(atan(y, x) * sqrt(x * x + y * y), dot(rel, up)); + } + if (projection_mode == 2) { // Spherical: (longitude, latitude) * radius + vec3 rel = p - patch_center; + float radius = length(rel); + if (radius < 1e-8) + return vec2(0.0); + vec3 dir = rel / radius; + return vec2(atan(dir.y, dir.x), asin(clamp(dir.z, -1.0, 1.0))) * radius; + } + vec3 an = abs(n); // Triplanar: drop the dominant normal axis + return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy); +} + +// The two surface directions projection_raw()'s u and v run along at `p`, plus how many raw units one +// millimetre of travel along each of them covers - the factor that turns the uv-space height gradient +// into a real mm-per-mm slope. For the planar projection both axes are world axes and the factor is 1. +// Cylindrical and Spherical are arc-length parametrized, so it is 1 there too, except for the +// spherical longitude, whose circle shrinks by cos(latitude) toward the poles. Exact where the surface +// really is the cylinder/sphere the projection assumes - the same assumption libslic3r makes. +void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_mm) +{ + units_per_mm = vec2(1.0, 1.0); + if (projection_mode == 1) { + vec3 up, right, fwd; + cylinder_frame(up, right, fwd); + vec3 rel = p - patch_center; + vec2 xy = vec2(dot(rel, right), dot(rel, fwd)); + float r = length(xy); + t = (r > 1e-6) ? (fwd * xy.x - right * xy.y) / r : right; // circumferential: u runs along it + b = up; // v is the distance along the axis + return; + } + if (projection_mode == 2) { + vec3 rel = p - patch_center; + float r = length(rel); + vec3 dir = (r > 1e-8) ? rel / r : vec3(0.0, 0.0, 1.0); + float c = length(dir.xy); // cos(latitude) + t = (c > 1e-6) ? vec3(-dir.y, dir.x, 0.0) / c : vec3(1.0, 0.0, 0.0); + b = cross(dir, t); // increasing latitude, unit length + units_per_mm = vec2(1.0 / max(c, 1e-3), 1.0); + return; + } + vec3 an = abs(n); + if (an.x >= an.y && an.x >= an.z) { // planar = p.yz + t = vec3(0.0, 1.0, 0.0); + b = vec3(0.0, 0.0, 1.0); + } else if (an.y >= an.x && an.y >= an.z) { // planar = p.xz + t = vec3(1.0, 0.0, 0.0); + b = vec3(0.0, 0.0, 1.0); + } else { // planar = p.xy + t = vec3(1.0, 0.0, 0.0); + b = vec3(0.0, 1.0, 0.0); + } +} + +vec2 project_uv(vec3 p, vec3 n) +{ + vec2 planar = projection_raw(p, n); + planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0; + float cs = cos(rotation_rad); + float sn = sin(rotation_rad); + vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs); + // After the rotation, so the rotation stays a rotation rather than becoming a shear. + r.y *= tex_aspect; + return r + uv_offset; +} + +// sRGB -> CIELAB, matching slic3r/Utils/ColorSpaceConvert's RGB2Lab so this picks the same entry the +// bake does. +vec3 srgb_to_lab(vec3 c) +{ + vec3 v = vec3(c.r > 0.04045 ? pow((c.r + 0.055) / 1.055, 2.4) : c.r / 12.92, + c.g > 0.04045 ? pow((c.g + 0.055) / 1.055, 2.4) : c.g / 12.92, + c.b > 0.04045 ? pow((c.b + 0.055) / 1.055, 2.4) : c.b / 12.92); + vec3 xyz = vec3(dot(v, vec3(0.4124, 0.3576, 0.1805)) / 0.95047, + dot(v, vec3(0.2126, 0.7152, 0.0722)), + dot(v, vec3(0.0193, 0.1192, 0.9505)) / 1.08883); + vec3 f = vec3(xyz.x > 0.008856 ? pow(xyz.x, 1.0 / 3.0) : (7.787 * xyz.x) + 16.0 / 116.0, + xyz.y > 0.008856 ? pow(xyz.y, 1.0 / 3.0) : (7.787 * xyz.y) + 16.0 / 116.0, + xyz.z > 0.008856 ? pow(xyz.z, 1.0 / 3.0) : (7.787 * xyz.z) + 16.0 / 116.0); + return vec3(116.0 * f.y - 16.0, 500.0 * (f.x - f.y), 200.0 * (f.y - f.z)); +} + +// Nearest printable colour to a sampled one. Quantizing per *fragment* rather than per facet is the +// whole point of this path: it shows the image at the texture's resolution instead of the mesh's, +// which is what you need while choosing a texture and placing it. The Normal view is where the +// facet-resolution truth - what actually bakes - is shown. +// +// Squared distance in Lab (CIE76) rather than the CPU's CIEDE2000: the two agree except on near-ties, +// and CIEDE2000 per fragment across 64 entries is not worth its cost in a preview. +int nearest_palette_entry(vec3 rgb) +{ + vec3 lab = srgb_to_lab(rgb); + int best = 0; + int best_pure = -1; + float bd = 1.0e20; + float bd_pure = 1.0e20; + for (int i = 0; i < 64; ++i) { + if (i >= palette_count) + break; + if (pure_only && palette_a[i] != palette_b[i]) + continue; // a flat-colour image never takes a mix (see the bake) + vec3 d = lab - palette_lab[i]; + float d2 = dot(d, d); + if (palette_a[i] == palette_b[i] && d2 < bd_pure) { + bd_pure = d2; + best_pure = i; + } + if (d2 < bd) { + bd = d2; + best = i; + } + } + // The same bias make_palette_quantizer() applies (PREFER_PURE_DE = 10): a mix is an interleave, so + // it is only worth taking when it beats the nearest single filament by a visible step. Without it + // this picked a mix for almost every fragment - with four filaments the palette is 4 pure entries + // against 30 mixes - while the bake picked a single filament for most of them, so the preview + // interleaved the whole wall where the bake interleaves only patches. Compared on the distances + // rather than their squares, so the threshold means the same thing as it does on the CPU (up to + // CIE76 against CIEDE2000, the approximation already noted above). + if (best_pure >= 0 && palette_a[best] != palette_b[best] && sqrt(bd_pure) - sqrt(bd) < 10.0) + best = best_pure; + return best; +} + +// One 2x2 Bayer cell, {0, 2; 3, 1}, for x and y in {0, 1}. +float bayer2(float x, float y) { return 2.0 * x + 3.0 * y - 4.0 * x * y; } + +// The colour the printer lays down at world point `pos` for palette entry `index`: its filament, or +// for a mix whichever of its two filaments this point falls on. Mirrors make_mix_resolver() on the +// CPU, floors on the band/cell size included. All the modular arithmetic is done in floats with +// mod(), which wraps negative coordinates the way the CPU's ((v % n) + n) % n does and needs no +// integer % (not available on every GLSL 1.10 target). +vec3 printed_color(int index, vec3 pos, vec3 normal, vec3 footprint) +{ + int a = palette_a[index]; + int b = palette_b[index]; + if (a < 0 || a >= filament_count || b < 0 || b >= filament_count) + return palette_rgb[index]; // no filament to resolve to: the entry's own colour + if (a == b) + return filament_rgb[a]; + float num = float(palette_num[index]); + float den = float(palette_den[index]); + // Auto: bands where the surface is steeper than ~45 degrees, the dominant filament elsewhere. + if (mix_mode == 2 && abs(normal.z) >= 0.7) + return filament_rgb[(num * 2.0 >= den) ? a : b]; + + // Pre-filter. The interleave is an ordered dither the eye is meant to blend away, and no dither + // blends when it is drawn at less than a few pixels per period - it aliases, which is what turned + // every upright wall into horizontal streaks: the Z band cycle is den * layer_height (around a + // millimetre), and every pixel of a row on a vertical wall shares one z, so each row came out as a + // 1-bit threshold of the image at that row's phase. `footprint` is mm of world position per pixel, + // so this is zoom- and resolution-correct rather than a tuned constant: where the print's own + // pattern is finer than this view can resolve, show what the print looks like from here, which is + // the entry's perceptual average. The Normal view remains where the per-facet truth lives. + float period = (mix_mode == 1) ? 2.0 * max(dither_cell, 0.01) : den * max(layer_height, 0.01); + float px = (mix_mode == 1) ? max(footprint.x, footprint.y) : footprint.z; + float sharp = clamp(period / max(4.0 * px, 1e-6) - 0.5, 0.0, 1.0); + if (sharp <= 0.0) + return palette_rgb[index]; + + vec3 picked; + if (mix_mode == 1) { + // Ordered 4x4 Bayer over floor(x / cell), floor(y / cell). The CPU's table + // 0 8 2 10 + // 12 4 14 6 + // 3 11 1 9 + // 15 7 13 5 + // is 4 * bayer2(x % 2, y % 2) + bayer2(x / 2, y / 2), which needs no array (GLSL 1.10 has + // no constant arrays). + float cell = max(dither_cell, 0.01); + float gx = mod(floor(pos.x / cell), 4.0); + float gy = mod(floor(pos.y / cell), 4.0); + float bayer = 4.0 * bayer2(mod(gx, 2.0), mod(gy, 2.0)) + bayer2(floor(gx / 2.0), floor(gy / 2.0)); + picked = filament_rgb[(num / den > (bayer + 0.5) / 16.0) ? a : b]; + } else { + // Z bands: one per band height, the band's phase in the a/b cycle picks the filament. Both + // operands are integer-valued, so the half keeps "phase < num" exact under float rounding. + float slot = floor(pos.z / max(layer_height, 0.01)); + float phase = mod(slot, den); + picked = filament_rgb[(phase < num - 0.5) ? a : b]; + } + return mix(palette_rgb[index], picked, sharp); +} + +void main() +{ + if (any(lessThan(clipping_planes_dots, ZERO))) + discard; + + // World millimetres throughout, like the bake - see the 140 variant. + vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz))); + vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does + // World mm per pixel, for pre-filtering the interleave in printed_color(). Taken here because the + // albedo branch at the end of main() is non-uniform control flow, where derivatives are undefined. + vec3 pos_fwidth = fwidth(world_pos.xyz); + if (volume_mirrored) + triangle_normal = -triangle_normal; + + // Where the colour is read from. Both branches below already compute the uv this fragment's + // *height* came from - including the parallax-marched one on the triplanar path - and the colour + // has to follow it exactly, or the colour would slide off the relief as the camera orbits. + vec2 color_uv = vec2(0.0); + bool have_uv = false; + + if (use_vertex_uv) { + // Mikkelsen surface-gradient normal perturbation; see the 140 variant for the full rationale. Scale-exact + // for a conformal LSCM map (no global 1/tiling assumption), and gated by the paint weight + // via a multiply so the branch stays uniform (use_vertex_uv is a uniform). + vec2 uv = (island_active > 0.5) + ? vec2(dot(island_delta_lin.xy, vertex_uv), dot(island_delta_lin.zw, vertex_uv)) + island_delta_tr + : vertex_uv; + color_uv = uv; + have_uv = true; + float h = texture2D(height_tex, uv).r; + float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0); + vec3 sigmaS = dFdx(world_pos.xyz); + vec3 sigmaT = dFdy(world_pos.xyz); + vec3 R1 = cross(sigmaT, triangle_normal); + vec3 R2 = cross(triangle_normal, sigmaS); + float det = dot(sigmaS, R1); + float dHdx = k * dFdx(h); + float dHdy = k * dFdy(h); + if (abs(det) > 1e-12) + triangle_normal = normalize(triangle_normal - (dHdx * R1 + dHdy * R2) / det); + } else if (weight > 0.0) { + vec3 t, b; + vec2 units_per_mm; + projection_axes(tex_pos, triangle_normal, t, b, units_per_mm); + + // Parallax occlusion mapping: march the view ray through the height shell and shade at the + // first point where it drops below the displaced surface (see header). + float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0); + vec3 view_dir = normalize(eye_model_pos - tex_pos); + float v_dot_n = dot(view_dir, triangle_normal); + vec2 uv = project_uv(tex_pos, triangle_normal); + + // The shell the displaced surface lives inside, as signed heights along the normal. Taken from + // both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel, + // where the surface sits *below* the undisplaced one. + float h_end_a = amp * (0.0 - midlevel); + float h_end_b = amp * (1.0 - midlevel); + float h_hi = max(h_end_a, h_end_b); + float h_lo = min(h_end_a, h_end_b); + // How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half + // a texel there is no parallax to find and the march would be pure cost - which is the common + // case of looking straight down at a surface. + float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4); + if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) { + // A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the + // undisplaced surface. Start at the top of the shell, where the ray is outside the surface + // by construction, and step inward; the crossing is what this pixel actually sees. + float s = h_hi / v_dot_n; + float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS)); + vec2 prev_uv = project_uv(tex_pos + view_dir * s, triangle_normal); + float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction + for (int i = 0; i < PARALLAX_STEPS; ++i) { + s -= ds; + vec2 cur_uv = project_uv(tex_pos + view_dir * s, triangle_normal); + float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel); + if (gap <= 0.0) { + // Crossed between the last two samples - interpolating the hit is what stops it + // quantising to the step size, and so what keeps the step count affordable. + uv = mix(prev_uv, cur_uv, clamp(prev_gap / max(prev_gap - gap, 1e-6), 0.0, 1.0)); + break; + } + prev_uv = cur_uv; + prev_gap = gap; + } + } + + color_uv = uv; // after the parallax march, so colour and relief stay registered + have_uv = true; + + float hL = texture2D(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r; + float hR = texture2D(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r; + float hD = texture2D(height_tex, uv - vec2(0.0, height_tex_texel.y)).r; + float hU = texture2D(height_tex, uv + vec2(0.0, height_tex_texel.y)).r; + + vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y)); + float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0; + float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0); + + float cs = cos(rotation_rad); + float sn = sin(rotation_rad); + // One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v + // component of the gradient carries the extra factor before being rotated back into t/b. + vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect); + // ...and back out of raw-projection units into millimetres along t / b; see the 140 variant. + vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs) * units_per_mm; + + vec3 gradient = slope.x * t + slope.y * b; + gradient -= triangle_normal * dot(triangle_normal, gradient); + triangle_normal = normalize(triangle_normal - gradient); + } + + vec3 eye_normal = normalize(view_normal_matrix * triangle_normal); + float NdotL = max(dot(eye_normal, LIGHT_TOP_DIR), 0.0); + + vec2 intensity = vec2(0.0); + intensity.x = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE; + vec3 position = (view_model_matrix * model_pos).xyz; + intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(position), reflect(-LIGHT_TOP_DIR, eye_normal)), 0.0), LIGHT_TOP_SHININESS); + + NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0); + intensity.x += NdotL * LIGHT_FRONT_DIFFUSE; + + // Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour - + // and, where that is a mix, the filament the interleave puts here, so the pattern that prints shows. + // Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as + // the same material under the same light, and the relief this preview exists to show is unaffected. + vec3 albedo = uniform_color.rgb; + if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0) + // tex_pos, not world_pos: the bake resolves the interleave in the bake frame (world + // orientation and scale about the volume's origin, see texture_displacement_bake_frame()), so + // measuring z from the bed instead shifted the band phase by the volume origin's height - a + // different filament in the same place than the bake produces. + albedo = printed_color(nearest_palette_entry(texture2D(color_tex, color_uv).rgb), tex_pos, triangle_normal, pos_fwidth); + gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a); +} diff --git a/resources/shaders/110/texture_displacement_shaded.vs b/resources/shaders/110/texture_displacement_shaded.vs new file mode 100644 index 0000000000..468bdf56d9 --- /dev/null +++ b/resources/shaders/110/texture_displacement_shaded.vs @@ -0,0 +1,34 @@ +#version 110 + +// See resources/shaders/140/texture_displacement_shaded.vs for full documentation; this is the +// GLSL 1.10 compatibility variant. + +uniform mat4 view_model_matrix; +uniform mat4 projection_matrix; +uniform mat4 volume_world_matrix; +uniform vec2 z_range; +uniform vec4 clipping_plane; + +attribute vec3 v_position; +attribute vec3 v_normal; // .x = paint weight (0/1); .y = 1 for the dragged island's vertices +attribute vec2 v_tex_coord; // precomputed texture uv, used only when use_vertex_uv is set + +varying vec3 clipping_planes_dots; +varying vec4 model_pos; +varying vec4 world_pos; +varying float weight; +varying float island_active; +varying vec2 vertex_uv; + +void main() +{ + model_pos = vec4(v_position, 1.0); + world_pos = volume_world_matrix * model_pos; + + gl_Position = projection_matrix * view_model_matrix * model_pos; + clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z); + + weight = v_normal.x; + island_active = v_normal.y; + vertex_uv = v_tex_coord; +} diff --git a/resources/shaders/110/texture_displacement_uvcheck.fs b/resources/shaders/110/texture_displacement_uvcheck.fs new file mode 100644 index 0000000000..f9e998adc2 --- /dev/null +++ b/resources/shaders/110/texture_displacement_uvcheck.fs @@ -0,0 +1,114 @@ +#version 110 + +// See resources/shaders/140/texture_displacement_uvcheck.fs; GLSL 1.10 compatibility variant. + +#define INTENSITY_CORRECTION 0.6 +const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929); +#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION) +const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074); +#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION) +#define INTENSITY_AMBIENT 0.3 +const vec3 ZERO = vec3(0.0, 0.0, 0.0); + +uniform mat3 view_normal_matrix; +uniform bool volume_mirrored; + +uniform int mode; +uniform float checker_freq; +uniform float tiling_scale; +uniform vec3 tex_anchor; // the volume's origin in world space +uniform float rotation_rad; +uniform vec2 uv_offset; +uniform bool use_vertex_uv; +// The in-shader projection (0 Triplanar, 1 Cylindrical, 2 Spherical) and the painted patch's frame the +// wrapping ones wrap around, in the texture frame - the same uniforms, and the same formulas, as +// texture_displacement_shaded.fs, so the checker reports the projection the bake will actually use. +uniform int projection_mode; +uniform vec3 patch_center; +uniform vec3 patch_axis; +// Height map width / height, as apply_uv_transform() applies it. Without it the checker diverged from +// the bake for any non-square texture, in every in-shader projection. +uniform float tex_aspect; + +varying vec3 clipping_planes_dots; +varying vec4 model_pos; +varying vec4 world_pos; +varying float distortion; +varying vec2 vertex_uv; + +void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd) +{ + up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0); + vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); + right = normalize(cross(up, arbitrary)); + fwd = normalize(cross(right, up)); +} + +// Term for term libslic3r's project_planar() / project_cylindrical() / project_spherical(), in mm. +vec2 projection_raw(vec3 p, vec3 n) +{ + if (projection_mode == 1) { + vec3 up, right, fwd; + cylinder_frame(up, right, fwd); + vec3 rel = p - patch_center; + float x = dot(rel, right); + float y = dot(rel, fwd); + return vec2(atan(y, x) * sqrt(x * x + y * y), dot(rel, up)); + } + if (projection_mode == 2) { + vec3 rel = p - patch_center; + float radius = length(rel); + if (radius < 1e-8) + return vec2(0.0); + vec3 dir = rel / radius; + return vec2(atan(dir.y, dir.x), asin(clamp(dir.z, -1.0, 1.0))) * radius; + } + vec3 an = abs(n); + return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy); +} + +vec2 project_uv(vec3 p, vec3 n) +{ + vec2 planar = projection_raw(p, n); + planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0; + float cs = cos(rotation_rad); + float sn = sin(rotation_rad); + vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs); + r.y *= tex_aspect; // after the rotation, so the rotation stays a rotation rather than a shear + return r + uv_offset; +} + +vec3 heatmap(float t) +{ + t = clamp(t, 0.0, 1.0); + return clamp(vec3(1.5 - abs(4.0 * t - 3.0), + 1.5 - abs(4.0 * t - 2.0), + 1.5 - abs(4.0 * t - 1.0)), 0.0, 1.0); +} + +void main() +{ + if (any(lessThan(clipping_planes_dots, ZERO))) + discard; + + // World space anchored at the volume's origin, like the bake and the shaded preview. + vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz))); + vec3 tex_pos = world_pos.xyz - tex_anchor; + if (volume_mirrored) + triangle_normal = -triangle_normal; + + vec3 base; + if (mode == 1) { + base = heatmap(distortion); + } else { + vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, triangle_normal); + vec2 c = floor(uv * checker_freq); + float check = mod(c.x + c.y, 2.0); + base = (check < 0.5) ? vec3(0.22, 0.23, 0.26) : vec3(0.82, 0.83, 0.86); + } + + vec3 eye_normal = normalize(view_normal_matrix * triangle_normal); + float intensity = INTENSITY_AMBIENT + max(dot(eye_normal, LIGHT_TOP_DIR), 0.0) * LIGHT_TOP_DIFFUSE + + max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0) * LIGHT_FRONT_DIFFUSE; + gl_FragColor = vec4(base * intensity, 1.0); +} diff --git a/resources/shaders/110/texture_displacement_uvcheck.vs b/resources/shaders/110/texture_displacement_uvcheck.vs new file mode 100644 index 0000000000..06c59e5c61 --- /dev/null +++ b/resources/shaders/110/texture_displacement_uvcheck.vs @@ -0,0 +1,31 @@ +#version 110 + +// See resources/shaders/140/texture_displacement_uvcheck.vs; GLSL 1.10 compatibility variant. + +uniform mat4 view_model_matrix; +uniform mat4 projection_matrix; +uniform mat4 volume_world_matrix; +uniform vec2 z_range; +uniform vec4 clipping_plane; + +attribute vec3 v_position; +attribute vec3 v_normal; // .x = per-vertex uv distortion +attribute vec2 v_tex_coord; // precomputed texture uv, used only when use_vertex_uv is set + +varying vec3 clipping_planes_dots; +varying vec4 model_pos; +varying vec4 world_pos; +varying float distortion; +varying vec2 vertex_uv; + +void main() +{ + model_pos = vec4(v_position, 1.0); + world_pos = volume_world_matrix * model_pos; + + gl_Position = projection_matrix * view_model_matrix * model_pos; + clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z); + + distortion = v_normal.x; + vertex_uv = v_tex_coord; +} diff --git a/resources/shaders/140/texture_displacement_shaded.fs b/resources/shaders/140/texture_displacement_shaded.fs new file mode 100644 index 0000000000..1d8bee10fa --- /dev/null +++ b/resources/shaders/140/texture_displacement_shaded.fs @@ -0,0 +1,513 @@ +#version 140 + +// Fast, geometry-free preview of texture displacement: perturbs the *shading* normal from the +// height texture's local gradient, faded out by the per-vertex paint weight. The +// true, exact result is what "Bake" produces via libslic3r/TextureDisplacement.cpp on the CPU. +// +// The bake displaces each surface point along its normal by H = +/- depth_mm * (h(uv) - midlevel), +// with uv from the layer's projection. The perturbed normal is the analytic +// +// N' = normalize(N - (dH/da) * T - (dH/db) * B) +// +// over any orthonormal surface tangent pair (T, B), where the two slopes are real mm-per-mm +// derivatives. Two things have to be right for the preview's apparent depth to match the bake's: +// the tangent frame the gradient is expressed in, and the uv->mm scale that turns a texel +// difference into a slope. Getting the scale wrong is a uniform flattening (a raw texel difference +// is dh over one texel step, not over one mm); getting the frame wrong tilts the relief along the +// wrong axes. +// +// Two projection paths: +// * Triplanar (use_vertex_uv = 0): uv and the tangent axes are derived in-shader from the dominant +// normal axis, mirroring libslic3r's project_planar()/apply_uv_transform(), and the slope is +// formed analytically (there is a closed-form uv, so 1 uv unit is exactly tiling_scale mm). This +// path also runs a parallax step before shading, see below. +// +// Parallax. Perturbing the normal alone shades only, so the pattern is welded to the base surface: it +// does not shift as the camera orbits and it does not get any deeper as depth_mm grows, which is +// exactly when the preview stops reading as real geometry. The triplanar path therefore shades at the +// point the *displaced* surface would show at this pixel rather than at the pixel's own base position. +// +// Two cheaper formulations were tried first and both are wrong here, which is worth recording: +// * Solving Q = P + V * (H(Q) / dot(V, n)) by fixed-point iteration. Geometrically exact, but the +// divisor goes to zero edge-on, and an unbounded step is not a small error - the sample lands a +// large fraction of a tile away and the iteration oscillates instead of converging. It reads as a +// *second, flat copy* of the pattern ghosted over the real one. Clamping the step to one tile does +// not help either: a tile-sized shift lands on the neighbouring tile, which is the same pattern. +// * Offset limiting (Welsh): step along the tangential part of V, whose length caps the shift at one +// depth. Stable and cheap, but it understates parallax by exactly the factor that matters - the +// relief still flattens as soon as the camera tilts, which is the complaint it was meant to fix. +// +// So this ray-marches instead (parallax occlusion mapping). A point at ray parameter s, i.e. P + V * s, +// sits at height s * dot(V, n) above the undisplaced surface. The displaced surface lives in a shell +// between the extreme values of amp * (h - midlevel); the march starts at the top of that shell, where +// the ray is outside the surface by construction, and steps inward until the ray height falls below the +// sampled height. That crossing *is* the visible point - no divergence, no ghosting, and parallax stays +// correct at any angle. The hit is interpolated between the last two samples, which is what keeps +// PARALLAX_STEPS low enough to afford. The march is skipped when sweeping the shell would move the +// sample point less than half a texel (the head-on case), so the common view pays almost nothing. +// +// The gradient/shading below is evaluated at the resulting uv, so the relief both slides correctly +// under camera motion and visibly deepens with depth_mm. What it still cannot do is change the +// model's silhouette or cast shadows; for that, switch the View row to Normal. +// * Precomputed uv (use_vertex_uv = 1, used for LSCM): uv comes per-vertex from the CPU (the LSCM +// unwrap with island placement + tiling/rotation/offset already folded in), and the perturbed +// normal is built with Mikkelsen's method -- the surface gradient taken straight from the +// screen-space derivatives of the sampled height and position. This makes no uv->mm scale +// assumption, which matters because an LSCM map is conformal, not isometric: the local mm-per-uv +// varies across the chart, so a single global 1/tiling factor (what an earlier version used) got +// the apparent depth wrong. This path is also what makes the fast preview follow the UV editor: +// move an island and its uv -- hence its relief -- moves with it. + +#define INTENSITY_CORRECTION 0.6 + +#define PARALLAX_STEPS 24 +// Explicit LOD: the march samples inside non-uniform control flow, where implicit +// derivatives are undefined. +#define H_AT(uv) textureLod(height_tex, uv, 0.0).r + +// normalized values for (-0.6/1.31, 0.6/1.31, 1./1.31) +const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929); +#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SHININESS 20.0 + +// normalized values for (1./1.43, 0.2/1.43, 1./1.43) +const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074); +#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION) + +#define INTENSITY_AMBIENT 0.3 + +const vec3 ZERO = vec3(0.0, 0.0, 0.0); + +uniform vec4 uniform_color; +// The printable palette, in **CIELAB** as well as RGB, and how many entries are real. Lab because the +// match has to be perceptual - the same reason the CPU side uses CIEDE2000 - and converting the +// palette once on the CPU is what lets the fragment shader match with a plain squared distance. +// Count 0 means nothing is colouring, and every fragment falls back to uniform_color as before. +uniform vec3 palette_lab[64]; +uniform vec3 palette_rgb[64]; +uniform int palette_count; +uniform bool pure_only; // match against single filaments only (flat-colour image) +// How each entry prints. A pure entry is one filament (a == b); a mix interleaves filaments a and b, +// num parts of a in every den, and the print shows that interleave rather than the entry's average +// colour. The fragment resolves it exactly as GLGizmoTextureDisplacement::make_mix_resolver() does +// per triangle on the CPU, so the preview shows the pattern the bake will print. +uniform int palette_a[64]; +uniform int palette_b[64]; +uniform int palette_num[64]; +uniform int palette_den[64]; +uniform vec3 filament_rgb[16]; +uniform int filament_count; +uniform int mix_mode; // ColorMixMode: 0 Z bands, 1 XY dither, 2 auto +uniform float layer_height; // mm; one Z band per print layer +uniform float dither_cell; // mm; one XY dither cell +uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height +uniform bool has_color_tex; +uniform bool volume_mirrored; + +uniform mat4 view_model_matrix; +uniform mat3 view_normal_matrix; + +uniform sampler2D height_tex; +uniform vec2 height_tex_texel; // (1/width, 1/height) of height_tex +uniform float depth_mm; +uniform float tiling_scale; +// Height map width / height. Scales the v axis so a non-square image keeps its proportions +// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform(). +uniform float tex_aspect; +uniform float rotation_rad; +uniform vec2 uv_offset; +uniform bool invert; +uniform float midlevel; // the height that means "don't move"; needed by the parallax step +uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor) +uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin +uniform bool use_vertex_uv; // true: sample at vertex_uv with a derived tangent frame (LSCM) +// A 2x3 affine (columns packed as lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the uv of +// the island currently being dragged in the UV editor (island_active > 0.5). Identity when nothing is +// dragged, so this whole path is a no-op then. Lets a UV island drag move the relief on the model with +// only a uniform update +uniform vec4 island_delta_lin; +uniform vec2 island_delta_tr; +// Which projection to reconstruct in-shader: 0 Triplanar, 1 Cylindrical, 2 Spherical (the low three +// TextureProjectionMethod values; LSCM and ViewProjected arrive through use_vertex_uv and leave this +// at 0). The two wrapping projections wrap around the *whole painted patch*, so its centroid - and, +// for Cylindrical, its axis - are properties no single fragment can derive. They come from the CPU, +// in this same texture frame, computed with the bake's own texture_displacement_patch_frame(). +uniform int projection_mode; +uniform vec3 patch_center; +uniform vec3 patch_axis; + +in vec3 clipping_planes_dots; +in vec4 model_pos; +in vec4 world_pos; +in float weight; +in float island_active; +in vec2 vertex_uv; + +out vec4 out_color; + +// The cylinder's own frame, built exactly as libslic3r's project_cylindrical() builds it - including +// the handedness, which comes out left-handed for an axis of +Z. Copied rather than "corrected", so +// the preview wraps the texture the same way round as the bake. +void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd) +{ + up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0); + vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); + right = normalize(cross(up, arbitrary)); + fwd = normalize(cross(right, up)); +} + +// The raw, millimetre-valued projection of `p` (a position in the texture frame), before the layer's +// tiling/rotation/aspect/offset - a term-for-term transcription of libslic3r's project_planar(), +// project_cylindrical() and project_spherical(). `n` is read by the planar mode only. +vec2 projection_raw(vec3 p, vec3 n) +{ + if (projection_mode == 1) { // Cylindrical: (arc length around, distance along) + vec3 up, right, fwd; + cylinder_frame(up, right, fwd); + vec3 rel = p - patch_center; + float x = dot(rel, right); + float y = dot(rel, fwd); + return vec2(atan(y, x) * sqrt(x * x + y * y), dot(rel, up)); + } + if (projection_mode == 2) { // Spherical: (longitude, latitude) * radius + vec3 rel = p - patch_center; + float radius = length(rel); + if (radius < 1e-8) + return vec2(0.0); + vec3 dir = rel / radius; + return vec2(atan(dir.y, dir.x), asin(clamp(dir.z, -1.0, 1.0))) * radius; + } + vec3 an = abs(n); // Triplanar: drop the dominant normal axis + return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy); +} + +// The two surface directions projection_raw()'s u and v run along at `p`, plus how many raw units one +// millimetre of travel along each of them covers - the factor that turns the uv-space height gradient +// into a real mm-per-mm slope. For the planar projection both axes are world axes and the factor is 1. +// Cylindrical and Spherical are arc-length parametrized, so it is 1 there too, except for the +// spherical longitude, whose circle shrinks by cos(latitude) toward the poles. Exact where the surface +// really is the cylinder/sphere the projection assumes - the same assumption libslic3r makes. +void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_mm) +{ + units_per_mm = vec2(1.0, 1.0); + if (projection_mode == 1) { + vec3 up, right, fwd; + cylinder_frame(up, right, fwd); + vec3 rel = p - patch_center; + vec2 xy = vec2(dot(rel, right), dot(rel, fwd)); + float r = length(xy); + t = (r > 1e-6) ? (fwd * xy.x - right * xy.y) / r : right; // circumferential: u runs along it + b = up; // v is the distance along the axis + return; + } + if (projection_mode == 2) { + vec3 rel = p - patch_center; + float r = length(rel); + vec3 dir = (r > 1e-8) ? rel / r : vec3(0.0, 0.0, 1.0); + float c = length(dir.xy); // cos(latitude) + t = (c > 1e-6) ? vec3(-dir.y, dir.x, 0.0) / c : vec3(1.0, 0.0, 0.0); + b = cross(dir, t); // increasing latitude, unit length + units_per_mm = vec2(1.0 / max(c, 1e-3), 1.0); + return; + } + vec3 an = abs(n); + if (an.x >= an.y && an.x >= an.z) { // planar = p.yz + t = vec3(0.0, 1.0, 0.0); + b = vec3(0.0, 0.0, 1.0); + } else if (an.y >= an.x && an.y >= an.z) { // planar = p.xz + t = vec3(1.0, 0.0, 0.0); + b = vec3(0.0, 0.0, 1.0); + } else { // planar = p.xy + t = vec3(1.0, 0.0, 0.0); + b = vec3(0.0, 1.0, 0.0); + } +} + +vec2 project_uv(vec3 p, vec3 n) +{ + vec2 planar = projection_raw(p, n); + planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0; + float cs = cos(rotation_rad); + float sn = sin(rotation_rad); + vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs); + // After the rotation, so the rotation stays a rotation rather than becoming a shear. + r.y *= tex_aspect; + return r + uv_offset; +} + +// sRGB -> CIELAB, matching slic3r/Utils/ColorSpaceConvert's RGB2Lab so this picks the same entry the +// bake does. +vec3 srgb_to_lab(vec3 c) +{ + vec3 v = vec3(c.r > 0.04045 ? pow((c.r + 0.055) / 1.055, 2.4) : c.r / 12.92, + c.g > 0.04045 ? pow((c.g + 0.055) / 1.055, 2.4) : c.g / 12.92, + c.b > 0.04045 ? pow((c.b + 0.055) / 1.055, 2.4) : c.b / 12.92); + vec3 xyz = vec3(dot(v, vec3(0.4124, 0.3576, 0.1805)) / 0.95047, + dot(v, vec3(0.2126, 0.7152, 0.0722)), + dot(v, vec3(0.0193, 0.1192, 0.9505)) / 1.08883); + vec3 f = vec3(xyz.x > 0.008856 ? pow(xyz.x, 1.0 / 3.0) : (7.787 * xyz.x) + 16.0 / 116.0, + xyz.y > 0.008856 ? pow(xyz.y, 1.0 / 3.0) : (7.787 * xyz.y) + 16.0 / 116.0, + xyz.z > 0.008856 ? pow(xyz.z, 1.0 / 3.0) : (7.787 * xyz.z) + 16.0 / 116.0); + return vec3(116.0 * f.y - 16.0, 500.0 * (f.x - f.y), 200.0 * (f.y - f.z)); +} + +// Nearest printable colour to a sampled one. Quantizing per *fragment* rather than per facet is the +// whole point of this path: it shows the image at the texture's resolution instead of the mesh's, +// which is what you need while choosing a texture and placing it. The Normal view is where the +// facet-resolution truth - what actually bakes - is shown. +// +// Squared distance in Lab (CIE76) rather than the CPU's CIEDE2000: the two agree except on near-ties, +// and CIEDE2000 per fragment across 64 entries is not worth its cost in a preview. +int nearest_palette_entry(vec3 rgb) +{ + vec3 lab = srgb_to_lab(rgb); + int best = 0; + int best_pure = -1; + float bd = 1.0e20; + float bd_pure = 1.0e20; + for (int i = 0; i < 64; ++i) { + if (i >= palette_count) + break; + if (pure_only && palette_a[i] != palette_b[i]) + continue; // a flat-colour image never takes a mix (see the bake) + vec3 d = lab - palette_lab[i]; + float d2 = dot(d, d); + if (palette_a[i] == palette_b[i] && d2 < bd_pure) { + bd_pure = d2; + best_pure = i; + } + if (d2 < bd) { + bd = d2; + best = i; + } + } + // The same bias make_palette_quantizer() applies (PREFER_PURE_DE = 10): a mix is an interleave, so + // it is only worth taking when it beats the nearest single filament by a visible step. Without it + // this picked a mix for almost every fragment - with four filaments the palette is 4 pure entries + // against 30 mixes - while the bake picked a single filament for most of them, so the preview + // interleaved the whole wall where the bake interleaves only patches. Compared on the distances + // rather than their squares, so the threshold means the same thing as it does on the CPU (up to + // CIE76 against CIEDE2000, the approximation already noted above). + if (best_pure >= 0 && palette_a[best] != palette_b[best] && sqrt(bd_pure) - sqrt(bd) < 10.0) + best = best_pure; + return best; +} + +// One 2x2 Bayer cell, {0, 2; 3, 1}, for x and y in {0, 1}. +float bayer2(float x, float y) { return 2.0 * x + 3.0 * y - 4.0 * x * y; } + +// The colour the printer lays down at world point `pos` for palette entry `index`: its filament, or +// for a mix whichever of its two filaments this point falls on. Mirrors make_mix_resolver() on the +// CPU, floors on the band/cell size included. All the modular arithmetic is done in floats with +// mod(), which wraps negative coordinates the way the CPU's ((v % n) + n) % n does and needs no +// integer % (not available on every GLSL 1.10 target). +vec3 printed_color(int index, vec3 pos, vec3 normal, vec3 footprint) +{ + int a = palette_a[index]; + int b = palette_b[index]; + if (a < 0 || a >= filament_count || b < 0 || b >= filament_count) + return palette_rgb[index]; // no filament to resolve to: the entry's own colour + if (a == b) + return filament_rgb[a]; + float num = float(palette_num[index]); + float den = float(palette_den[index]); + // Auto: bands where the surface is steeper than ~45 degrees, the dominant filament elsewhere. + if (mix_mode == 2 && abs(normal.z) >= 0.7) + return filament_rgb[(num * 2.0 >= den) ? a : b]; + + // Pre-filter. The interleave is an ordered dither the eye is meant to blend away, and no dither + // blends when it is drawn at less than a few pixels per period - it aliases, which is what turned + // every upright wall into horizontal streaks: the Z band cycle is den * layer_height (around a + // millimetre), and every pixel of a row on a vertical wall shares one z, so each row came out as a + // 1-bit threshold of the image at that row's phase. `footprint` is mm of world position per pixel, + // so this is zoom- and resolution-correct rather than a tuned constant: where the print's own + // pattern is finer than this view can resolve, show what the print looks like from here, which is + // the entry's perceptual average. The Normal view remains where the per-facet truth lives. + float period = (mix_mode == 1) ? 2.0 * max(dither_cell, 0.01) : den * max(layer_height, 0.01); + float px = (mix_mode == 1) ? max(footprint.x, footprint.y) : footprint.z; + float sharp = clamp(period / max(4.0 * px, 1e-6) - 0.5, 0.0, 1.0); + if (sharp <= 0.0) + return palette_rgb[index]; + + vec3 picked; + if (mix_mode == 1) { + // Ordered 4x4 Bayer over floor(x / cell), floor(y / cell). The CPU's table + // 0 8 2 10 + // 12 4 14 6 + // 3 11 1 9 + // 15 7 13 5 + // is 4 * bayer2(x % 2, y % 2) + bayer2(x / 2, y / 2), which needs no array (GLSL 1.10 has + // no constant arrays). + float cell = max(dither_cell, 0.01); + float gx = mod(floor(pos.x / cell), 4.0); + float gy = mod(floor(pos.y / cell), 4.0); + float bayer = 4.0 * bayer2(mod(gx, 2.0), mod(gy, 2.0)) + bayer2(floor(gx / 2.0), floor(gy / 2.0)); + picked = filament_rgb[(num / den > (bayer + 0.5) / 16.0) ? a : b]; + } else { + // Z bands: one per band height, the band's phase in the a/b cycle picks the filament. Both + // operands are integer-valued, so the half keeps "phase < num" exact under float rounding. + float slot = floor(pos.z / max(layer_height, 0.01)); + float phase = mod(slot, den); + picked = filament_rgb[(phase < num - 0.5) ? a : b]; + } + return mix(palette_rgb[index], picked, sharp); +} + +void main() +{ + if (any(lessThan(clipping_planes_dots, ZERO))) + discard; + + // Everything below runs in world millimetres, like the bake: a tile is tiling_scale mm on the + // printed part whatever the instance's scale or rotation, so the preview has to project from the + // world position and perturb the world normal. + vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz))); + vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does + // World mm per pixel, for pre-filtering the interleave in printed_color(). Taken here because the + // albedo branch at the end of main() is non-uniform control flow, where derivatives are undefined. + vec3 pos_fwidth = fwidth(world_pos.xyz); + if (volume_mirrored) + triangle_normal = -triangle_normal; + + // Where the colour is read from. Both branches below already compute the uv this fragment's + // *height* came from - including the parallax-marched one on the triplanar path - and the colour + // has to follow it exactly, or the colour would slide off the relief as the camera orbits. + vec2 color_uv = vec2(0.0); + bool have_uv = false; + + if (use_vertex_uv) { + // Precomputed-uv (LSCM) path - Mikkelsen's surface-gradient normal mapping ("Bump Mapping + // Unparametrized Surfaces on the GPU"). The perturbed normal is derived straight from the + // screen-space derivatives of the *sampled height* and the position, so it is scale-exact + // with no uv->mm assumption at all - which is the whole point here: an LSCM map is conformal, + // not isometric, so the local mm-per-uv varies across the chart and the earlier "one global + // 1/tiling factor" got the depth visibly wrong. dFdx(h) captures the true on-screen rate of + // change however the chart is stretched or however fine the tiling is. + // + // use_vertex_uv is a uniform, so this whole branch is uniform control flow and the texture + // derivatives are well defined; the paint weight gates the result by a plain multiply (k) + // rather than a per-fragment branch, keeping it that way. + // The dragged island's uv rides a uniform affine so its relief moves without a rebuild; every + // other vertex (island_active == 0) samples its baked uv unchanged. + vec2 uv = (island_active > 0.5) + ? vec2(dot(island_delta_lin.xy, vertex_uv), dot(island_delta_lin.zw, vertex_uv)) + island_delta_tr + : vertex_uv; + color_uv = uv; + have_uv = true; + float h = texture(height_tex, uv).r; + float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0); + vec3 sigmaS = dFdx(world_pos.xyz); + vec3 sigmaT = dFdy(world_pos.xyz); + vec3 R1 = cross(sigmaT, triangle_normal); + vec3 R2 = cross(triangle_normal, sigmaS); + float det = dot(sigmaS, R1); + float dHdx = k * dFdx(h); + float dHdy = k * dFdy(h); + if (abs(det) > 1e-12) + triangle_normal = normalize(triangle_normal - (dHdx * R1 + dHdy * R2) / det); + } else if (weight > 0.0) { + // Triplanar path: uv and the tangent axes are reconstructed in-shader from the dominant + // normal component (see header). The gradient is expressed analytically because there is a + // closed-form uv here, unlike the LSCM case. + vec3 t, b; + vec2 units_per_mm; + projection_axes(tex_pos, triangle_normal, t, b, units_per_mm); + + // Parallax occlusion mapping: march the view ray through the height shell and shade at the + // first point where it drops below the displaced surface (see header). + float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0); + vec3 view_dir = normalize(eye_model_pos - tex_pos); + float v_dot_n = dot(view_dir, triangle_normal); + vec2 uv = project_uv(tex_pos, triangle_normal); + + // The shell the displaced surface lives inside, as signed heights along the normal. Taken from + // both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel, + // where the surface sits *below* the undisplaced one. + float h_end_a = amp * (0.0 - midlevel); + float h_end_b = amp * (1.0 - midlevel); + float h_hi = max(h_end_a, h_end_b); + float h_lo = min(h_end_a, h_end_b); + // How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half + // a texel there is no parallax to find and the march would be pure cost - which is the common + // case of looking straight down at a surface. + float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4); + if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) { + // A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the + // undisplaced surface. Start at the top of the shell, where the ray is outside the surface + // by construction, and step inward; the crossing is what this pixel actually sees. + float s = h_hi / v_dot_n; + float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS)); + vec2 prev_uv = project_uv(tex_pos + view_dir * s, triangle_normal); + float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction + for (int i = 0; i < PARALLAX_STEPS; ++i) { + s -= ds; + vec2 cur_uv = project_uv(tex_pos + view_dir * s, triangle_normal); + float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel); + if (gap <= 0.0) { + // Crossed between the last two samples - interpolating the hit is what stops it + // quantising to the step size, and so what keeps the step count affordable. + uv = mix(prev_uv, cur_uv, clamp(prev_gap / max(prev_gap - gap, 1e-6), 0.0, 1.0)); + break; + } + prev_uv = cur_uv; + prev_gap = gap; + } + } + + color_uv = uv; // after the parallax march, so colour and relief stay registered + have_uv = true; + + float hL = texture(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r; + float hR = texture(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r; + float hD = texture(height_tex, uv - vec2(0.0, height_tex_texel.y)).r; + float hU = texture(height_tex, uv + vec2(0.0, height_tex_texel.y)).r; + + // Central difference, per uv unit (not per texel). + vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y)); + + // uv -> mm is 1/tiling_scale for the triplanar projection, so this turns the uv-space + // gradient into a real surface slope. + float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0; + float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0); + // uv was rotated by project_uv() while t/b are the unrotated model axes, so rotate the + // gradient back into the axes' frame. + float cs = cos(rotation_rad); + float sn = sin(rotation_rad); + // One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v + // component of the gradient carries the extra factor before being rotated back into t/b. + vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect); + // ...and back out of raw-projection units into millimetres of travel along t / b, which is a + // no-op except for the spherical longitude (see projection_axes()). After the inverse rotation, + // because units_per_mm is expressed in the t/b frame rather than in uv. + vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs) * units_per_mm; + + vec3 gradient = slope.x * t + slope.y * b; + gradient -= triangle_normal * dot(triangle_normal, gradient); + triangle_normal = normalize(triangle_normal - gradient); + } + + vec3 eye_normal = normalize(view_normal_matrix * triangle_normal); + float NdotL = max(dot(eye_normal, LIGHT_TOP_DIR), 0.0); + + vec2 intensity = vec2(0.0); + intensity.x = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE; + vec3 position = (view_model_matrix * model_pos).xyz; + intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(position), reflect(-LIGHT_TOP_DIR, eye_normal)), 0.0), LIGHT_TOP_SHININESS); + + NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0); + intensity.x += NdotL * LIGHT_FRONT_DIFFUSE; + + // Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour - + // and, where that is a mix, the filament the interleave puts here, so the pattern that prints shows. + // Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as + // the same material under the same light, and the relief this preview exists to show is unaffected. + vec3 albedo = uniform_color.rgb; + if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0) + // tex_pos, not world_pos: the bake resolves the interleave in the bake frame (world + // orientation and scale about the volume's origin, see texture_displacement_bake_frame()), so + // measuring z from the bed instead shifted the band phase by the volume origin's height - a + // different filament in the same place than the bake produces. + albedo = printed_color(nearest_palette_entry(texture(color_tex, color_uv).rgb), tex_pos, triangle_normal, pos_fwidth); + out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a); +} diff --git a/resources/shaders/140/texture_displacement_shaded.vs b/resources/shaders/140/texture_displacement_shaded.vs new file mode 100644 index 0000000000..cd629acedc --- /dev/null +++ b/resources/shaders/140/texture_displacement_shaded.vs @@ -0,0 +1,44 @@ +#version 140 + +uniform mat4 view_model_matrix; +uniform mat4 projection_matrix; + +uniform mat4 volume_world_matrix; +// Clipping plane, x = min z, y = max z. Used by the FFF and SLA previews to clip with a top / bottom plane. +uniform vec2 z_range; +// Clipping plane - general orientation. Used by the SLA gizmo. +uniform vec4 clipping_plane; + +in vec3 v_position; +// GLModel's P3N3T2 layout (position + normal + texcoord), reused so this mesh builds and renders +// like any other GLModel rather than needing a bespoke vertex buffer. The two spare channels carry +// what the shaded preview actually needs per vertex: +// v_normal.x -- the active layer's paint weight, 0 (untouched) or 1 (painted). +// v_normal.y -- 1 for a vertex of the island currently being dragged in the UV editor, else 0. +// The fragment shader applies island_delta to those vertices' uv, so a UV drag is a +// single uniform update rather than a whole-mesh rebuild (like Adjust placement). +// v_tex_coord -- the precomputed texture uv for this vertex, valid only when use_vertex_uv is set +// (i.e. the LSCM projection, where uv can't be reconstructed in the shader). The +// triplanar path ignores it and projects in the fragment shader instead. +in vec3 v_normal; +in vec2 v_tex_coord; + +out vec3 clipping_planes_dots; +out vec4 model_pos; +out vec4 world_pos; +out float weight; +out float island_active; +out vec2 vertex_uv; + +void main() +{ + model_pos = vec4(v_position, 1.0); + world_pos = volume_world_matrix * model_pos; + + gl_Position = projection_matrix * view_model_matrix * model_pos; + clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z); + + weight = v_normal.x; + island_active = v_normal.y; + vertex_uv = v_tex_coord; +} diff --git a/resources/shaders/140/texture_displacement_uvcheck.fs b/resources/shaders/140/texture_displacement_uvcheck.fs new file mode 100644 index 0000000000..e6fc8036da --- /dev/null +++ b/resources/shaders/140/texture_displacement_uvcheck.fs @@ -0,0 +1,127 @@ +#version 140 + +// UV-check overlay for the texture-displacement gizmo, drawn over the painted patch so the LSCM +// unwrap can be sanity-checked on the real 3D surface (mode set by the `mode` uniform): +// mode 0 - Checker: a procedural checkerboard sampled at the layer's uv. Even squares that stay +// square everywhere on the model mean the unwrap is low-distortion; squares that smear or +// shear reveal exactly where it stretches. Same uv the bake samples, so what you see is +// where the texture actually lands. +// mode 1 - Distortion heatmap: the per-vertex area-distortion carried in `distortion`, blue +// (compressed) -> green (ideal) -> red (stretched). +// Both are lit with the same cheap two-light diffuse the shaded preview uses, so the surface still +// reads as 3D. + +#define INTENSITY_CORRECTION 0.6 +const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929); +#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION) +const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074); +#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION) +#define INTENSITY_AMBIENT 0.3 +const vec3 ZERO = vec3(0.0, 0.0, 0.0); + +uniform mat3 view_normal_matrix; +uniform bool volume_mirrored; + +uniform int mode; // 0 checker, 1 distortion +uniform float checker_freq; // checker squares per uv unit (one uv unit == one texture tile) +uniform float tiling_scale; +uniform vec3 tex_anchor; // the volume's origin in world space +uniform float rotation_rad; +uniform vec2 uv_offset; +uniform bool use_vertex_uv; +// The in-shader projection (0 Triplanar, 1 Cylindrical, 2 Spherical) and the painted patch's frame the +// wrapping ones wrap around, in the texture frame - the same uniforms, and the same formulas, as +// texture_displacement_shaded.fs, so the checker reports the projection the bake will actually use. +uniform int projection_mode; +uniform vec3 patch_center; +uniform vec3 patch_axis; +// Height map width / height, as apply_uv_transform() applies it. Without it the checker diverged from +// the bake for any non-square texture, in every in-shader projection. +uniform float tex_aspect; + +in vec3 clipping_planes_dots; +in vec4 model_pos; +in vec4 world_pos; +in float distortion; +in vec2 vertex_uv; + +out vec4 out_color; + +void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd) +{ + up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0); + vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); + right = normalize(cross(up, arbitrary)); + fwd = normalize(cross(right, up)); +} + +// Term for term libslic3r's project_planar() / project_cylindrical() / project_spherical(), in mm. +vec2 projection_raw(vec3 p, vec3 n) +{ + if (projection_mode == 1) { + vec3 up, right, fwd; + cylinder_frame(up, right, fwd); + vec3 rel = p - patch_center; + float x = dot(rel, right); + float y = dot(rel, fwd); + return vec2(atan(y, x) * sqrt(x * x + y * y), dot(rel, up)); + } + if (projection_mode == 2) { + vec3 rel = p - patch_center; + float radius = length(rel); + if (radius < 1e-8) + return vec2(0.0); + vec3 dir = rel / radius; + return vec2(atan(dir.y, dir.x), asin(clamp(dir.z, -1.0, 1.0))) * radius; + } + vec3 an = abs(n); + return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy); +} + +vec2 project_uv(vec3 p, vec3 n) +{ + vec2 planar = projection_raw(p, n); + planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0; + float cs = cos(rotation_rad); + float sn = sin(rotation_rad); + vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs); + r.y *= tex_aspect; // after the rotation, so the rotation stays a rotation rather than a shear + return r + uv_offset; +} + +// Blue -> cyan -> green -> yellow -> red over t in [0,1]. +vec3 heatmap(float t) +{ + t = clamp(t, 0.0, 1.0); + return clamp(vec3(1.5 - abs(4.0 * t - 3.0), + 1.5 - abs(4.0 * t - 2.0), + 1.5 - abs(4.0 * t - 1.0)), 0.0, 1.0); +} + +void main() +{ + if (any(lessThan(clipping_planes_dots, ZERO))) + discard; + + // World space anchored at the volume's origin, like the bake and the shaded preview. + vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz))); + vec3 tex_pos = world_pos.xyz - tex_anchor; + if (volume_mirrored) + triangle_normal = -triangle_normal; + + vec3 base; + if (mode == 1) { + base = heatmap(distortion); + } else { + vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, triangle_normal); + vec2 c = floor(uv * checker_freq); + float check = mod(c.x + c.y, 2.0); + // Two distinct greys, plus a faint tint on one set so orientation is readable at a glance. + base = (check < 0.5) ? vec3(0.22, 0.23, 0.26) : vec3(0.82, 0.83, 0.86); + } + + vec3 eye_normal = normalize(view_normal_matrix * triangle_normal); + float intensity = INTENSITY_AMBIENT + max(dot(eye_normal, LIGHT_TOP_DIR), 0.0) * LIGHT_TOP_DIFFUSE + + max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0) * LIGHT_FRONT_DIFFUSE; + out_color = vec4(base * intensity, 1.0); +} diff --git a/resources/shaders/140/texture_displacement_uvcheck.vs b/resources/shaders/140/texture_displacement_uvcheck.vs new file mode 100644 index 0000000000..79e858e0d7 --- /dev/null +++ b/resources/shaders/140/texture_displacement_uvcheck.vs @@ -0,0 +1,36 @@ +#version 140 + +// Vertex stage for the UV-check overlay (checker / distortion heatmap) drawn over the painted patch +// by GLGizmoTextureDisplacement. Reuses GLModel's P3N3T2 layout so it needs no bespoke buffer: +// v_normal.x - per-vertex UV distortion (uv-area / surface-area ratio, remapped so 0.5 = ideal); +// only the distortion mode reads it. +// v_tex_coord - precomputed texture uv, valid only when use_vertex_uv is set (LSCM); the checker +// mode reconstructs uv in the fragment shader otherwise. + +uniform mat4 view_model_matrix; +uniform mat4 projection_matrix; +uniform mat4 volume_world_matrix; +uniform vec2 z_range; +uniform vec4 clipping_plane; + +in vec3 v_position; +in vec3 v_normal; +in vec2 v_tex_coord; + +out vec3 clipping_planes_dots; +out vec4 model_pos; +out vec4 world_pos; +out float distortion; +out vec2 vertex_uv; + +void main() +{ + model_pos = vec4(v_position, 1.0); + world_pos = volume_world_matrix * model_pos; + + gl_Position = projection_matrix * view_model_matrix * model_pos; + clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z); + + distortion = v_normal.x; + vertex_uv = v_tex_coord; +} diff --git a/resources/textures/displacement/Bark.png b/resources/textures/displacement/Bark.png new file mode 100644 index 0000000000..a48233d01e Binary files /dev/null and b/resources/textures/displacement/Bark.png differ diff --git a/resources/textures/displacement/Basket Weave.png b/resources/textures/displacement/Basket Weave.png new file mode 100644 index 0000000000..4459d87e6b Binary files /dev/null and b/resources/textures/displacement/Basket Weave.png differ diff --git a/resources/textures/displacement/Bricks weathered.png b/resources/textures/displacement/Bricks weathered.png new file mode 100644 index 0000000000..a20d979ec8 Binary files /dev/null and b/resources/textures/displacement/Bricks weathered.png differ diff --git a/resources/textures/displacement/Bricks.png b/resources/textures/displacement/Bricks.png new file mode 100644 index 0000000000..f89431370b Binary files /dev/null and b/resources/textures/displacement/Bricks.png differ diff --git a/resources/textures/displacement/Bubbles.png b/resources/textures/displacement/Bubbles.png new file mode 100644 index 0000000000..d04c58bd7d Binary files /dev/null and b/resources/textures/displacement/Bubbles.png differ diff --git a/resources/textures/displacement/Camouflage.png b/resources/textures/displacement/Camouflage.png new file mode 100644 index 0000000000..c0cde6accf Binary files /dev/null and b/resources/textures/displacement/Camouflage.png differ diff --git a/resources/textures/displacement/Carbon Fibre.png b/resources/textures/displacement/Carbon Fibre.png new file mode 100644 index 0000000000..53212f39a0 Binary files /dev/null and b/resources/textures/displacement/Carbon Fibre.png differ diff --git a/resources/textures/displacement/Chainmail.png b/resources/textures/displacement/Chainmail.png new file mode 100644 index 0000000000..7939d5b4a0 Binary files /dev/null and b/resources/textures/displacement/Chainmail.png differ diff --git a/resources/textures/displacement/Chevron.png b/resources/textures/displacement/Chevron.png new file mode 100644 index 0000000000..0a387439e2 Binary files /dev/null and b/resources/textures/displacement/Chevron.png differ diff --git a/resources/textures/displacement/Cobblestone.png b/resources/textures/displacement/Cobblestone.png new file mode 100644 index 0000000000..f9f7ed0035 Binary files /dev/null and b/resources/textures/displacement/Cobblestone.png differ diff --git a/resources/textures/displacement/Colour Bricks.png b/resources/textures/displacement/Colour Bricks.png new file mode 100644 index 0000000000..a8b574f14b Binary files /dev/null and b/resources/textures/displacement/Colour Bricks.png differ diff --git a/resources/textures/displacement/Cracked Earth.png b/resources/textures/displacement/Cracked Earth.png new file mode 100644 index 0000000000..9876500318 Binary files /dev/null and b/resources/textures/displacement/Cracked Earth.png differ diff --git a/resources/textures/displacement/Diamond Plate.png b/resources/textures/displacement/Diamond Plate.png new file mode 100644 index 0000000000..0b474a6921 Binary files /dev/null and b/resources/textures/displacement/Diamond Plate.png differ diff --git a/resources/textures/displacement/Fine Knurl.png b/resources/textures/displacement/Fine Knurl.png new file mode 100644 index 0000000000..dddd2480a0 Binary files /dev/null and b/resources/textures/displacement/Fine Knurl.png differ diff --git a/resources/textures/displacement/Grid.png b/resources/textures/displacement/Grid.png new file mode 100644 index 0000000000..14bd166a2c Binary files /dev/null and b/resources/textures/displacement/Grid.png differ diff --git a/resources/textures/displacement/Hammered.png b/resources/textures/displacement/Hammered.png new file mode 100644 index 0000000000..45fac6009b Binary files /dev/null and b/resources/textures/displacement/Hammered.png differ diff --git a/resources/textures/displacement/Herringbone.png b/resources/textures/displacement/Herringbone.png new file mode 100644 index 0000000000..d773b9f615 Binary files /dev/null and b/resources/textures/displacement/Herringbone.png differ diff --git a/resources/textures/displacement/Hex Tiles.png b/resources/textures/displacement/Hex Tiles.png new file mode 100644 index 0000000000..2c924d18ec Binary files /dev/null and b/resources/textures/displacement/Hex Tiles.png differ diff --git a/resources/textures/displacement/Hexagons.png b/resources/textures/displacement/Hexagons.png new file mode 100644 index 0000000000..accc9f5fb3 Binary files /dev/null and b/resources/textures/displacement/Hexagons.png differ diff --git a/resources/textures/displacement/Honeycomb.png b/resources/textures/displacement/Honeycomb.png new file mode 100644 index 0000000000..d428a5d2b3 Binary files /dev/null and b/resources/textures/displacement/Honeycomb.png differ diff --git a/resources/textures/displacement/Knurl.png b/resources/textures/displacement/Knurl.png new file mode 100644 index 0000000000..373e2badd6 Binary files /dev/null and b/resources/textures/displacement/Knurl.png differ diff --git a/resources/textures/displacement/Leather.png b/resources/textures/displacement/Leather.png new file mode 100644 index 0000000000..0c16daac63 Binary files /dev/null and b/resources/textures/displacement/Leather.png differ diff --git a/resources/textures/displacement/Mosaic Tiles.png b/resources/textures/displacement/Mosaic Tiles.png new file mode 100644 index 0000000000..e5e30e0bc2 Binary files /dev/null and b/resources/textures/displacement/Mosaic Tiles.png differ diff --git a/resources/textures/displacement/Noise.png b/resources/textures/displacement/Noise.png new file mode 100644 index 0000000000..c945e9ca8c Binary files /dev/null and b/resources/textures/displacement/Noise.png differ diff --git a/resources/textures/displacement/Perforated.png b/resources/textures/displacement/Perforated.png new file mode 100644 index 0000000000..5ea3435f14 Binary files /dev/null and b/resources/textures/displacement/Perforated.png differ diff --git a/resources/textures/displacement/Pyramids.png b/resources/textures/displacement/Pyramids.png new file mode 100644 index 0000000000..bb83b97b84 Binary files /dev/null and b/resources/textures/displacement/Pyramids.png differ diff --git a/resources/textures/displacement/Quilt.png b/resources/textures/displacement/Quilt.png new file mode 100644 index 0000000000..f0a07b31b5 Binary files /dev/null and b/resources/textures/displacement/Quilt.png differ diff --git a/resources/textures/displacement/Ripples.png b/resources/textures/displacement/Ripples.png new file mode 100644 index 0000000000..d06c6a58da Binary files /dev/null and b/resources/textures/displacement/Ripples.png differ diff --git a/resources/textures/displacement/Roof Tiles.png b/resources/textures/displacement/Roof Tiles.png new file mode 100644 index 0000000000..ed28294cb6 Binary files /dev/null and b/resources/textures/displacement/Roof Tiles.png differ diff --git a/resources/textures/displacement/Rope.png b/resources/textures/displacement/Rope.png new file mode 100644 index 0000000000..da6c6c1ffa Binary files /dev/null and b/resources/textures/displacement/Rope.png differ diff --git a/resources/textures/displacement/Sand Ripples.png b/resources/textures/displacement/Sand Ripples.png new file mode 100644 index 0000000000..7b4cd77312 Binary files /dev/null and b/resources/textures/displacement/Sand Ripples.png differ diff --git a/resources/textures/displacement/Scales.png b/resources/textures/displacement/Scales.png new file mode 100644 index 0000000000..6976dfad9d Binary files /dev/null and b/resources/textures/displacement/Scales.png differ diff --git a/resources/textures/displacement/Slate.png b/resources/textures/displacement/Slate.png new file mode 100644 index 0000000000..b63853a2d7 Binary files /dev/null and b/resources/textures/displacement/Slate.png differ diff --git a/resources/textures/displacement/Star Tiles.png b/resources/textures/displacement/Star Tiles.png new file mode 100644 index 0000000000..e1406e6f26 Binary files /dev/null and b/resources/textures/displacement/Star Tiles.png differ diff --git a/resources/textures/displacement/Stone Wall.png b/resources/textures/displacement/Stone Wall.png new file mode 100644 index 0000000000..f5239f92f9 Binary files /dev/null and b/resources/textures/displacement/Stone Wall.png differ diff --git a/resources/textures/displacement/Studs.png b/resources/textures/displacement/Studs.png new file mode 100644 index 0000000000..1f9baaa7e8 Binary files /dev/null and b/resources/textures/displacement/Studs.png differ diff --git a/resources/textures/displacement/Tartan.png b/resources/textures/displacement/Tartan.png new file mode 100644 index 0000000000..a5c321daa3 Binary files /dev/null and b/resources/textures/displacement/Tartan.png differ diff --git a/resources/textures/displacement/Terrazzo.png b/resources/textures/displacement/Terrazzo.png new file mode 100644 index 0000000000..0b817546bb Binary files /dev/null and b/resources/textures/displacement/Terrazzo.png differ diff --git a/resources/textures/displacement/Triangles.png b/resources/textures/displacement/Triangles.png new file mode 100644 index 0000000000..f9f21d88e5 Binary files /dev/null and b/resources/textures/displacement/Triangles.png differ diff --git a/resources/textures/displacement/Waffle.png b/resources/textures/displacement/Waffle.png new file mode 100644 index 0000000000..06b5de6377 Binary files /dev/null and b/resources/textures/displacement/Waffle.png differ diff --git a/resources/textures/displacement/Waves.png b/resources/textures/displacement/Waves.png new file mode 100644 index 0000000000..71a262435b Binary files /dev/null and b/resources/textures/displacement/Waves.png differ diff --git a/resources/textures/displacement/Weave.png b/resources/textures/displacement/Weave.png new file mode 100644 index 0000000000..022c423b81 Binary files /dev/null and b/resources/textures/displacement/Weave.png differ diff --git a/resources/textures/displacement/Wood Grain.png b/resources/textures/displacement/Wood Grain.png new file mode 100644 index 0000000000..e65ce88d65 Binary files /dev/null and b/resources/textures/displacement/Wood Grain.png differ diff --git a/resources/textures/displacement/Wood Planks.png b/resources/textures/displacement/Wood Planks.png new file mode 100644 index 0000000000..b0e02f0b02 Binary files /dev/null and b/resources/textures/displacement/Wood Planks.png differ diff --git a/scripts/CAD/README.md b/scripts/CAD/README.md index 861b77a976..a5c6836e29 100644 --- a/scripts/CAD/README.md +++ b/scripts/CAD/README.md @@ -33,6 +33,21 @@ part of `libslic3r_tests` and run under `ctest` on every platform like any other unit test. This script exists for the local loop, where it is a two-minute round trip instead of a full application build. +## Generated sources + +Two checked-in files are emitted from data that lives here, so the source and the thing +compiled against it cannot drift apart: + +| Generator | Source | Output | +|---|---|---| +| `gen_offer_table.py` | `tool_atlas.json` — every offer verb with its row, key, icon, accepted selections and refusal string | `src/slic3r/GUI/CAD/DesignOffer.hpp` | +| `mate-glyph/emit_glyph_table.py` | `mate-glyph/bear_outline.json`, measured off `mate-glyph/bear.step` by `extract_outline.py` (needs FreeCAD) | the `kBear…` tables in `DesignSketchTool.cpp` | + +`python3 scripts/CAD/gen_offer_table.py` rewrites the header and `--check` proves the +checked-in one matches the atlas — `run-all-checks.sh` runs the check as its first rung, +which is what makes the header's "GENERATED — DO NOT EDIT" enforceable. Edit the atlas, +never the header. + ## Build and run | Script | Purpose | @@ -48,6 +63,6 @@ Two constraints that are not obvious and have each cost a session: - **A window manager is required.** Without one, windows are never focused, and an unfocused GTK app ignores synthetic keys — which looks exactly like a code bug. -`docs/rig_build_traps.md` documents these and three more, with symptoms and exact +`scripts/CAD/rig_build_traps.md` documents these and three more, with symptoms and exact recovery commands. Read it before debugging a configure or link failure one of these scripts reports. diff --git a/scripts/CAD/build-gui-incremental.sh b/scripts/CAD/build-gui-incremental.sh index 539a21c973..f87113fdaf 100755 --- a/scripts/CAD/build-gui-incremental.sh +++ b/scripts/CAD/build-gui-incremental.sh @@ -15,7 +15,7 @@ # On success the binary is inside the persistent volume at # /OrcaSlicer/build/package/bin/orca-slicer (copy it out with a follow-up # `docker run --rm -v orcacad_buildcache:/b alpine cp ...` or via this script's tail). -# Rig build traps already paid for once each (stale project, NLopt cache, pybind11, OCCT_LIBS, SLIC3R_CAD gate): docs/rig_build_traps.md +# Rig build traps already paid for once each (stale project, NLopt cache, pybind11, OCCT_LIBS, SLIC3R_CAD gate): scripts/CAD/rig_build_traps.md set -euo pipefail REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" diff --git a/scripts/CAD/build-gui.sh b/scripts/CAD/build-gui.sh index 0a1a86dd5c..edf93a5ca0 100755 --- a/scripts/CAD/build-gui.sh +++ b/scripts/CAD/build-gui.sh @@ -5,7 +5,7 @@ # NEVER build inside the GUI container (snapmaker-gui / orcacad-gui). Its baked /OrcaSlicer tree # is the Jun-13 Snapmaker-derived source, so a `cmake .` in there silently reconfigures the # shared build dir as project(Snapmaker_Orca) and this fork's targets vanish. That is Trap 1 of -# five; all of them, with symptoms and exact recovery commands, are in docs/rig_build_traps.md. +# five; all of them, with symptoms and exact recovery commands, are in scripts/CAD/rig_build_traps.md. # Read that file before debugging a configure or link failure this script reports. # # Usage: @@ -92,7 +92,7 @@ docker run --rm \ # A target-only build writes src/Release/, but this fork's start-headless-gui.sh may default BIN to the # PACKAGED path that only build_linux.sh refreshes — launching with the default would then run a -# stale binary. Pass BIN explicitly. See docs/rig_build_traps.md. +# stale binary. Pass BIN explicitly. See scripts/CAD/rig_build_traps.md. echo "=== launch the rig on the binary just built ===" echo " docker exec -e BIN=/OrcaSlicer/build/src/Release/$BIN ${PREFIX}-gui /OrcaSlicer/scripts/CAD/start-headless-gui.sh" exit "$rc" diff --git a/docs/CAD/ux/mockups/gen_offer_table.py b/scripts/CAD/gen_offer_table.py similarity index 80% rename from docs/CAD/ux/mockups/gen_offer_table.py rename to scripts/CAD/gen_offer_table.py index 9744f9288b..442be27697 100644 --- a/docs/CAD/ux/mockups/gen_offer_table.py +++ b/scripts/CAD/gen_offer_table.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 -"""Emit the C++ offer table from docs/ux/tool_atlas.json. +"""Emit the C++ offer table from scripts/CAD/tool_atlas.json. - python3 docs/ux/mockups/gen_offer_table.py + python3 scripts/CAD/gen_offer_table.py # rewrite the header + python3 scripts/CAD/gen_offer_table.py --check # verify the checked-in header -The map exists ONCE. The mockups and the shipping menu read the same rows in the same -order from the same file, so a drawing and the product cannot drift apart — which is the -only way row constancy (charter 4.1) survives contact with a codebase. +The map exists ONCE: the atlas is the only place a verb's row, key, icon and refusal +string are written down, so the menu the user sees cannot drift from the table the code +is compiled against. Output: src/slic3r/GUI/CAD/DesignOffer.hpp, checked in and never hand-edited. """ @@ -14,15 +15,9 @@ import json import os import sys -HERE = os.path.dirname(os.path.abspath(__file__)) -UX = os.path.dirname(HERE) -# One dirname more than you would expect: this generator lives at docs/CAD/ux/mockups/, -# not docs/ux/mockups/, since the design docs moved into the CAD subfolder (bbd1989e1e). -# With the old count REPO resolved to docs/, so OUT pointed at docs/src/.../DesignOffer.hpp, -# which does not exist -- and --check then diffed the real generated table against an empty -# file and reported the whole 189-line header as a difference. -REPO = os.path.dirname(os.path.dirname(os.path.dirname(UX))) -ATLAS = os.path.join(UX, "tool_atlas.json") +HERE = os.path.dirname(os.path.abspath(__file__)) # scripts/CAD +REPO = os.path.dirname(os.path.dirname(HERE)) +ATLAS = os.path.join(HERE, "tool_atlas.json") OUT = os.path.join(REPO, "src", "slic3r", "GUI", "CAD", "DesignOffer.hpp") # selection id -> C++ enumerator @@ -42,15 +37,40 @@ def cstr(s): return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"' +def validate(A): + """Refuse an atlas the header cannot represent, naming every fault at once. + + An unknown slot or selection would surface as a bare ValueError from .index() below, and a + duplicate verb id would not surface at all: DesignPanel::mcp_run_verb looks a verb up by id + and takes the first match, so the second row would be unreachable from the offer. + """ + slot_ids = {s["id"] for s in A["slots"]} + sel_ids = {s["id"] for s in A["selections"]} + errs, seen = [], set() + for v in A["verbs"]: + if v["slot"] not in slot_ids: + errs.append(f'{v["id"]}: unknown slot {v["slot"]!r}') + for a in v["accepts"]: + if a not in sel_ids: + errs.append(f'{v["id"]}: accepts unknown selection {a!r}') + if v["id"] in seen: + errs.append(f'{v["id"]}: duplicate verb id') + seen.add(v["id"]) + for s in sel_ids - set(ENUM): + errs.append(f"selection {s!r} has no OfferSel enumerator") + if errs: + sys.exit("tool_atlas.json is inconsistent:\n " + "\n ".join(errs)) + + def main(): A = json.load(open(ATLAS, encoding="utf-8")) + validate(A) sels = [s["id"] for s in A["selections"]] - assert all(s in ENUM for s in sels), [s for s in sels if s not in ENUM] slots = [s["id"] for s in A["slots"]] lines = [ "// GENERATED FILE — DO NOT EDIT.", - "// Source: docs/ux/tool_atlas.json Generator: docs/ux/mockups/gen_offer_table.py", + "// Source: scripts/CAD/tool_atlas.json Generator: scripts/CAD/gen_offer_table.py", "//", "// The object-driven tool offer (charter 4.1): every verb has ONE row index, that index", "// is the same in every selection it appears in, and verbs that do not apply are shown", diff --git a/docs/CAD/design/mate-connectors/bear.step b/scripts/CAD/mate-glyph/bear.step similarity index 100% rename from docs/CAD/design/mate-connectors/bear.step rename to scripts/CAD/mate-glyph/bear.step diff --git a/docs/CAD/design/mate-connectors/bear_outline.json b/scripts/CAD/mate-glyph/bear_outline.json similarity index 100% rename from docs/CAD/design/mate-connectors/bear_outline.json rename to scripts/CAD/mate-glyph/bear_outline.json diff --git a/docs/CAD/design/mate-connectors/emit_glyph_table.py b/scripts/CAD/mate-glyph/emit_glyph_table.py similarity index 100% rename from docs/CAD/design/mate-connectors/emit_glyph_table.py rename to scripts/CAD/mate-glyph/emit_glyph_table.py diff --git a/docs/CAD/design/mate-connectors/extract_outline.py b/scripts/CAD/mate-glyph/extract_outline.py similarity index 100% rename from docs/CAD/design/mate-connectors/extract_outline.py rename to scripts/CAD/mate-glyph/extract_outline.py diff --git a/docs/CAD/rig_build_traps.md b/scripts/CAD/rig_build_traps.md similarity index 100% rename from docs/CAD/rig_build_traps.md rename to scripts/CAD/rig_build_traps.md diff --git a/scripts/CAD/run-all-checks.sh b/scripts/CAD/run-all-checks.sh index 1c0388ac89..8a99a3b6cb 100755 --- a/scripts/CAD/run-all-checks.sh +++ b/scripts/CAD/run-all-checks.sh @@ -54,10 +54,9 @@ run_in_rig() { # copy the script in fresh, then run it ther # says. The header calls itself GENERATED and had been hand-edited anyway — which cost four rows # that existed only in the header, one row wired to the wrong action, and a count of 91 for a # 92-row array, so the last verb was unreachable (z8rs, ziam). -# docs/CAD/, not docs/: SoftFever moved the design docs into the CAD subfolder -# (bbd1989e1e) and this line kept the old path, so the rung failed on a missing file -# rather than on anything about the table. The other fork still has docs/ux/. -step "offer table matches the atlas" python3 docs/CAD/ux/mockups/gen_offer_table.py --check +# The atlas and its generator live beside this script, in scripts/CAD/: they are build inputs +# for a checked-in header, not documentation, so they do not belong under docs/. +step "offer table matches the atlas" python3 scripts/CAD/gen_offer_table.py --check step "kernel suite" scripts/CAD/run-kernel-tests.sh --vol "${KVOL:-orcacad_kerneltest}" diff --git a/scripts/CAD/run-kernel-tests.sh b/scripts/CAD/run-kernel-tests.sh index ef4356cb65..cc52a2a00b 100755 --- a/scripts/CAD/run-kernel-tests.sh +++ b/scripts/CAD/run-kernel-tests.sh @@ -19,7 +19,7 @@ # working tree to a per-volume staging dir on that host and re-runs this same script # there, so the verification contract is identical either way. Drop --host once the image # is present locally. -# Rig build traps already paid for once each (stale project, NLopt cache, pybind11, OCCT_LIBS, SLIC3R_CAD gate): docs/rig_build_traps.md +# Rig build traps already paid for once each (stale project, NLopt cache, pybind11, OCCT_LIBS, SLIC3R_CAD gate): scripts/CAD/rig_build_traps.md set -euo pipefail REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" diff --git a/docs/CAD/ux/tool_atlas.json b/scripts/CAD/tool_atlas.json similarity index 99% rename from docs/CAD/ux/tool_atlas.json rename to scripts/CAD/tool_atlas.json index 94709f2057..224b118c9e 100644 --- a/docs/CAD/ux/tool_atlas.json +++ b/scripts/CAD/tool_atlas.json @@ -1,6 +1,6 @@ { "_comment": [ - "Source of truth for the object-driven tool offer (see docs/cad_ux_guidelines.md 4.1).", + "Source of truth for the object-driven tool offer (see docs/HLSD/design-tab.md).", "Every verb, the selections it accepts, the document state it needs, and its fixed", "compass address. Extracted from the code, not from memory:", " verbs + shortcuts + refusal messages -> src/slic3r/GUI/CAD/DesignPanel.cpp (feat_dropdown", @@ -8,8 +8,8 @@ " kernel coverage -> src/libslic3r/CAD/CadDocument.hpp (enum CadFeatureType)", " headless coverage -> src/slic3r/GUI/CAD/McpControl.cpp (method == \"...\")", " selection kinds -> src/slic3r/GUI/CAD/DesignCanvas.hpp (the on_*_selected callbacks)", - "gen_offer_mockups.py reads this file; the eventual C++ offer table is generated from it", - "too, so the map exists once." + "gen_offer_table.py emits src/slic3r/GUI/CAD/DesignOffer.hpp from this file, so the map", + "exists once. Edit the atlas and regenerate; never hand-edit the header." ], "slots": [ { diff --git a/scripts/generate_displacement_textures.py b/scripts/generate_displacement_textures.py new file mode 100644 index 0000000000..0bb34c2223 --- /dev/null +++ b/scripts/generate_displacement_textures.py @@ -0,0 +1,517 @@ +#!/usr/bin/env python3 +"""Generates the displacement textures shipped in resources/textures/displacement/. + +Every texture is procedural - periodic functions and wrap-around noise on a unit tile - so the images +tile seamlessly in both directions and carry no third-party material. Grey PNGs are height maps +(white = raised). The colour PNGs are RGB where the luminance is the height, which is what +decode_height_texture() reads, so they displace and colour at the same time. + + python3 scripts/generate_displacement_textures.py [--size 1024] [--out resources/textures/displacement] + +Needs numpy and Pillow. Regenerating overwrites the files this script owns and nothing else. +""" +import argparse, os +import numpy as np +from PIL import Image + +SS = 2 # supersampling factor; the final image is the box-filtered average + +# --------------------------------------------------------------------------------------------- +# helpers: everything works on u, v in [0, 1) with period 1 on the tile +# --------------------------------------------------------------------------------------------- +def grid(n): + s = n * SS + v, u = np.meshgrid((np.arange(s) + 0.5) / s, (np.arange(s) + 0.5) / s, indexing='ij') + return u, v + +def down(img): + s = img.shape[0] // SS + return img.reshape(s, SS, s, SS).mean(axis=(1, 3)) + +def norm(h): + h = h - h.min(); m = h.max() + return h / m if m > 0 else h + +def smoothstep(e0, e1, x): + t = np.clip((x - e0) / (e1 - e0), 0.0, 1.0) + return t * t * (3 - 2 * t) + +def value_noise(u, v, freq, seed): + """Tileable value noise: a random lattice of `freq` cells with wrap-around, smooth interpolation.""" + rng = np.random.default_rng(seed) + lat = rng.random((freq, freq)) + x = u * freq; y = v * freq + x0 = np.floor(x).astype(int); y0 = np.floor(y).astype(int) + fx = x - x0; fy = y - y0 + fx = fx * fx * (3 - 2 * fx); fy = fy * fy * (3 - 2 * fy) + x1 = (x0 + 1) % freq; y1 = (y0 + 1) % freq; x0 %= freq; y0 %= freq + a = lat[y0, x0]; b = lat[y0, x1]; c = lat[y1, x0]; d = lat[y1, x1] + return (a * (1 - fx) + b * fx) * (1 - fy) + (c * (1 - fx) + d * fx) * fy + +def fbm(u, v, freq, seed, octaves=4, gain=0.5): + out = np.zeros_like(u); amp = 1.0; total = 0.0 + for o in range(octaves): + out += amp * value_noise(u, v, freq * 2 ** o, seed + o); total += amp; amp *= gain + return out / total + +def wrapped_points(n, seed, jitter=0.35): + """n x n jittered lattice points on the unit tile (periodic).""" + rng = np.random.default_rng(seed) + gx, gy = np.meshgrid((np.arange(n) + 0.5) / n, (np.arange(n) + 0.5) / n) + pts = np.stack([gx.ravel(), gy.ravel()], 1) + (rng.random((n * n, 2)) - 0.5) * (jitter / n) + return pts % 1.0 + +def voronoi(u, v, pts): + """Distance to the nearest and second-nearest point on the torus, and the nearest point's index.""" + d1 = np.full(u.shape, 9.0); d2 = np.full(u.shape, 9.0); idx = np.zeros(u.shape, int) + for i, (px, py) in enumerate(pts): + dx = u - px; dx -= np.round(dx); dy = v - py; dy -= np.round(dy) + d = np.sqrt(dx * dx + dy * dy) + closer = d < d1 + d2 = np.where(closer, d1, np.minimum(d2, d)); d1 = np.where(closer, d, d1); idx = np.where(closer, i, idx) + return d1, d2, idx + +def tri_wave(x): + return np.abs(2 * (x - np.floor(x + 0.5))) # 0..1 triangle wave, period 1 + +# --------------------------------------------------------------------------------------------- +# the textures +# --------------------------------------------------------------------------------------------- +def diamond_plate(u, v): + # Raised diamonds in two offset rows, bevelled; the classic tread plate. + n = 4 + h = np.zeros_like(u) + for ox, oy in ((0.0, 0.0), (0.5, 0.5)): + x = (u * n + ox) % 1 - 0.5; y = (v * n + oy) % 1 - 0.5 + d = np.abs(x) / 0.36 + np.abs(y) / 0.18 # a long diamond, 2:1 + h = np.maximum(h, smoothstep(1.0, 0.72, d)) + h += 0.06 * fbm(u, v, 32, 11) + return norm(h) + +def hex_cells(u, v, nx): + """Distance to the nearest hexagon centre, normalised so the hexagon's edge is at 1, plus a cell id. + nx hexagons across; the rows are stretched by at most ~2 % so a whole number fit the tile.""" + W = 1.0 / nx # hexagon width (flat-to-flat, pointy-top layout) + H = 2 * W / np.sqrt(3) # ideal corner-to-corner height + ny = 2 * max(1, int(round(1.0 / (1.5 * H)))) # rows per tile, even: the two staggered lattices + H = 1.0 / (0.75 * ny) # stretched so the rows tile exactly + best = np.full(u.shape, 9.0); cid = np.zeros(u.shape, int) + for k, (ox, oy) in enumerate(((0.0, 0.0), (0.5, 0.5))): + cx = (np.round(u / W - ox) + ox) * W; cy = (np.round(v / (1.5 * H) - oy) + oy) * 1.5 * H + dx = u - cx + dy = (v - cy) * (2 * W / np.sqrt(3)) / H # undo the row stretch: regular-hex units + ax = np.abs(dx) / (W / 2); ay = np.abs(dy) / (W / 2) + d = np.maximum(ax, (ax + ay * np.sqrt(3)) / 2) # hexagon SDF, edge at 1 + ix = np.round(u / W - ox).astype(int) % nx; iy = np.round(v / (1.5 * H) - oy).astype(int) % (ny // 2) + this = (ix * 7 + iy * 13 + k * 3) % 1009 # periodic, so a cell's colour matches across the seam + cid = np.where(d < best, this, cid); best = np.minimum(best, d) + return best, cid + +def honeycomb(u, v): + d, _ = hex_cells(u, v, 7) + return norm(smoothstep(1.0, 0.86, d)) + +def knurl(u, v): + n = 24 + a = tri_wave((u + v) * n); b = tri_wave((u - v) * n) + return norm(1 - np.maximum(a, b)) # pyramids + +def scales(u, v): + n = 8 + h = np.zeros_like(u) + # rows of circles, each row offset by half a scale and drawn over the row below + for row in range(-1, 2 * n + 1): + cy = row / (2 * n); ox = 0.5 if row % 2 else 0.0 + x = (u * n + ox) % 1 - 0.5; y = v - cy + y -= np.round(y) + r = np.sqrt((x / 1.0) ** 2 + (y * n) ** 2) + inside = r < 0.5 + dome = np.sqrt(np.clip(0.25 - r * r, 0, None)) * 2 # spherical cap + ramp = 0.4 + 0.6 * np.clip((0.5 * n * -y) / 0.5 + 0.5, 0, 1) # thicker at the exposed edge + cand = np.where(inside, 0.35 + 0.65 * dome * ramp, 0) + h = np.where(inside & (y * n <= 0.02), cand, h) + return norm(h) + +def herringbone(u, v): + # 2:1 bricks in the domino herringbone, turned 45 degrees. In cell coordinates (x, y) the brick a + # cell belongs to follows from (i - j) mod 4: 0/1 pair horizontally, 2/3 pair vertically. An even + # cell count per tile edge keeps that rule periodic. + n = 4 + x = (u + v) * n; y = (u - v) * n + i = np.floor(x); j = np.floor(y); fx = x - i; fy = y - j + k = ((i - j) % 4 + 4) % 4 + bx = np.where(k == 0, fx / 2, np.where(k == 1, 0.5 + fx / 2, fx)) + by = np.where(k == 2, 0.5 + fy / 2, np.where(k == 3, fy / 2, fy)) + # local coordinates on a 2x1 brick: the long axis is x for k in {0,1}, y for k in {2,3} + long_ = np.where(k < 2, bx, by); short = np.where(k < 2, by, bx) + gap_l = 0.03; gap_s = 0.06 + m = smoothstep(0, gap_l, long_) * smoothstep(0, gap_l, 1 - long_) * smoothstep(0, gap_s, short) * smoothstep(0, gap_s, 1 - short) + return norm(0.85 * m + 0.15 * m * fbm(u, v, 16, 5)) + +def cobblestone(u, v): + pts = wrapped_points(6, 21, 0.55) + d1, d2, idx = voronoi(u, v, pts) + edge = d2 - d1 + rng = np.random.default_rng(22); tops = rng.random(len(pts)) * 0.25 + 0.75 + h = smoothstep(0.0, 0.05, edge) * (0.55 + 0.45 * np.sqrt(np.clip(1 - (d1 / 0.11) ** 2, 0, None))) + h *= tops[idx] + h += 0.08 * fbm(u, v, 48, 23) + return norm(h) + +def leather(u, v): + pts = wrapped_points(22, 31, 0.9) + d1, d2, idx = voronoi(u, v, pts) + grooves = 1 - smoothstep(0.0, 0.012, d2 - d1) + h = 1 - 0.55 * grooves - 0.25 * fbm(u, v, 12, 33) - 0.1 * fbm(u, v, 96, 34) + return norm(h) + +def carbon_fibre(u, v): + n = 8 + x = u * n; y = v * n + cx = np.floor(x); cy = np.floor(y) + over = ((cx + cy) % 4) < 2 # 2x2 twill + fx = x % 1; fy = y % 1 + warp = 1 - 0.5 * (2 * np.abs(fx - 0.5)) ** 2 + 0.06 * np.sin(fy * 2 * np.pi * 14) + weft = 1 - 0.5 * (2 * np.abs(fy - 0.5)) ** 2 + 0.06 * np.sin(fx * 2 * np.pi * 14) + h = np.where(over, warp, weft * 0.92) + return norm(h) + +def chevron(u, v): + n = 6 + h = tri_wave(v * n * 2 + tri_wave(u * n) * 1.0) + return norm(1 - smoothstep(0.35, 0.65, h)) + +def ripples(u, v): + # Rings spreading from a few points on the torus, fading with distance - rain on water. + pts = wrapped_points(3, 81, 1.0) + h = np.zeros_like(u) + rng = np.random.default_rng(82) + for (px, py), f, ph in zip(pts, rng.random(len(pts)) * 6 + 10, rng.random(len(pts)) * 6.28): + dx = u - px; dx -= np.round(dx); dy = v - py; dy -= np.round(dy) + r = np.sqrt(dx * dx + dy * dy) + h += np.exp(-r / 0.22) * np.cos(2 * np.pi * r * f + ph) + return norm(h) + +def hammered(u, v): + pts = wrapped_points(9, 41, 0.8) + d1, d2, idx = voronoi(u, v, pts) + rng = np.random.default_rng(42); rad = rng.random(len(pts)) * 0.04 + 0.07 + dent = np.clip(1 - (d1 / rad[idx]) ** 2, 0, None) + return norm(1 - 0.8 * dent + 0.05 * fbm(u, v, 64, 43)) + +def perforated(u, v): + n = 8 + x = (u * n) % 1 - 0.5; y = (v * n) % 1 - 0.5 + r = np.sqrt(x * x + y * y) + return norm(smoothstep(0.30, 0.34, r)) + +def rope(u, v): + n = 6 # ropes per tile, running along v + x = (u * n) % 1 - 0.5 + body = np.sqrt(np.clip(0.25 - x * x, 0, None)) * 2 + twist = 0.5 + 0.5 * np.sin(2 * np.pi * (v * 12 + x * 1.6)) + return norm(body * (0.65 + 0.35 * twist)) + +def stone_wall(u, v): + rows = 5 + y = v * rows; row = np.floor(y); fy = y % 1 + rng = np.random.default_rng(51) + h = np.zeros_like(u) + for r in range(rows): + # stones of varying width along the row, periodic in u + widths = rng.random(6) * 0.6 + 0.7; widths *= 1.0 / widths.sum() + edges = np.concatenate([[0], np.cumsum(widths)]) + rng.random() * 0.3 + xu = (u + 0.0) % 1 + in_row = row == r + for i in range(len(widths)): + a = edges[i] % 1; w = widths[i] + dx = (xu - a) % 1 + inside = dx < w + gap = 0.035 + m = smoothstep(0, gap, dx) * smoothstep(0, gap, w - dx) * smoothstep(0, 0.12, fy) * smoothstep(0, 0.12, 1 - fy) + top = 0.7 + 0.3 * rng.random() + h = np.where(in_row & inside, m * top, h) + h = h * (0.85 + 0.15 * fbm(u, v, 24, 52)) + 0.04 * fbm(u, v, 96, 53) + return norm(h) + +def weathered_bricks(u, v): + # Running bond with some play in it: every row has its own offset and brick lengths, the bricks have + # rounded corners and chipped bevelled edges, a slight tilt each, and a mottled, pitted face over a + # gritty recessed mortar. + rows, per_row = 10, 4 + rng = np.random.default_rng(181) + y = v * rows; row = np.floor(y).astype(int) % rows; fy = y - np.floor(y) + offs = (np.arange(rows) % 2) * 0.5 / per_row + (rng.random(rows) - 0.5) * 0.35 / per_row + lens = rng.random((rows, per_row)) * 0.3 + 0.85 + edges = np.concatenate([np.zeros((rows, 1)), np.cumsum(lens / lens.sum(1, keepdims=True), 1)], 1) + x = (u - offs[row]) % 1 + k = np.zeros_like(row) + for i in range(1, per_row): + k += x >= edges[row, i] + a = edges[row, k]; b = edges[row, k + 1] + # rounded-rectangle distance, in tile units so the corners stay round, negative inside the brick + g, rad = 0.006, 0.012 + px = np.abs(x - (a + b) / 2) - ((b - a) / 2 - g - rad) + py = np.abs((fy - 0.5) / rows) - (0.5 / rows - g - rad) + d = np.hypot(np.maximum(px, 0), np.maximum(py, 0)) + np.minimum(np.maximum(px, py), 0) - rad + # ragged edges plus the odd bite out of an arris + d += 0.005 * (fbm(u, v, 40, 182, octaves=3) - 0.5) + 0.006 * smoothstep(0.66, 0.78, fbm(u, v, 24, 183, octaves=3)) + t = np.clip(-d / 0.014, 0, 1) + shoulder = np.sqrt(1 - (1 - t) ** 2) # rounded bevel + bid = row * per_row + k + per = rng.random((rows * per_row, 5)) + # each brick sits at its own height and leans a little + face = 0.82 + 0.1 * per[bid, 0] + 0.04 * ((per[bid, 1] - 0.5) * (x - (a + b) / 2) / ((b - a) / 2) + + (per[bid, 2] - 0.5) * (fy - 0.5) * 2) + # mottled face: domain-warped noise shifted per brick so neighbours don't share a pattern + su = (u + per[bid, 3]) % 1; sv = (v + per[bid, 4]) % 1 + wu = su + 0.08 * fbm(u, v, 6, 184, octaves=3); wv = sv + 0.08 * fbm(u, v, 6, 185, octaves=3) + mottle = smoothstep(0.3, 0.7, fbm(wu, wv, 6, 186, octaves=5, gain=0.55)) + # shallow pits that deepen gradually from the rim; the warp keeps their outlines off the noise lattice + pn = fbm(wu + 0.02 * fbm(u, v, 32, 190, octaves=2), wv, 48, 187, octaves=3) + pits = smoothstep(0.62, 0.82, pn) ** 2 * smoothstep(0.8, 0.86, fbm(u, v, 8, 188, octaves=2)) + face += 0.3 * (mottle - 0.5) - 0.16 * pits + mortar = 0.06 + 0.06 * fbm(u, v, 64, 189, octaves=3) + return norm(mortar + (face - mortar) * shoulder) + +# ---- colour textures: (rgb in 0..1, height = luminance by construction) +def lum(rgb): + return 0.299 * rgb[..., 0] + 0.587 * rgb[..., 1] + 0.114 * rgb[..., 2] + +def colour_bricks(u, v): + n = 6 + y = v * n; row = np.floor(y); x = u * n * 2 + 0.5 * (row % 2) + bx = x % 1; by = y % 1 + gap = 0.07 + m = smoothstep(0, gap, bx) * smoothstep(0, gap, 1 - bx) * smoothstep(0, gap * 2, by) * smoothstep(0, gap * 2, 1 - by) + rng = np.random.default_rng(61) + cell = (np.floor(x).astype(int) * 7 + row.astype(int) * 13) % 97 + tone = rng.random(97)[cell] + # brick reds of varying warmth over a dark grey mortar + brick = np.stack([0.70 + 0.2 * tone, 0.30 + 0.12 * tone, 0.22 + 0.06 * tone], -1) + brick *= (0.85 + 0.15 * fbm(u, v, 48, 62))[..., None] + mortar = np.array([0.30, 0.29, 0.27]) + rgb = brick * m[..., None] + mortar * (1 - m[..., None]) + return rgb + +def mosaic(u, v): + n = 8 + x = (u * n) % 1; y = (v * n) % 1 + gap = 0.08 + m = smoothstep(0, gap, x) * smoothstep(0, gap, 1 - x) * smoothstep(0, gap, y) * smoothstep(0, gap, 1 - y) + rng = np.random.default_rng(71) + cell = (np.floor(u * n).astype(int) * 31 + np.floor(v * n).astype(int) * 17) % 64 + pal = np.array([[0.90, 0.85, 0.70], [0.20, 0.45, 0.75], [0.85, 0.35, 0.25], [0.35, 0.65, 0.40]]) + tile = pal[rng.integers(0, 4, 64)[cell]] + grout = np.array([0.18, 0.18, 0.18]) + return tile * m[..., None] + grout * (1 - m[..., None]) + +def hex_tiles(u, v): + d, cid = hex_cells(u, v, 7) + m = smoothstep(1.0, 0.9, d) + pal = np.array([[0.95, 0.93, 0.88], [0.25, 0.55, 0.60], [0.80, 0.55, 0.20]]) + rng = np.random.default_rng(91) + tile = pal[rng.integers(0, 3, 1009)[cid]] + grout = np.array([0.15, 0.15, 0.16]) + return tile * m[..., None] + grout * (1 - m[..., None]) + + +def wood_planks(u, v): + n = 4 # planks across, running along v; staggered ends + x = u * n; col = np.floor(x); fx = x - col + rng = np.random.default_rng(101) + y = v * 2 + rng.random(n)[col.astype(int) % n] # each plank column has its own end offset + fy = y % 1 + gap = 0.04 + m = smoothstep(0, gap, fx) * smoothstep(0, gap, 1 - fx) * smoothstep(0, gap * 1.5, fy) * smoothstep(0, gap * 1.5, 1 - fy) + # grain: stretched noise along the plank, per plank phase + grain = fbm((u * 1.0 + rng.random(n)[col.astype(int) % n]) % 1, v, 6, 102, octaves=5) + rings = 0.5 + 0.5 * np.sin(2 * np.pi * (fx * 3 + grain * 2.5)) + return norm(m * (0.75 + 0.25 * rings)) + +def basket_weave(u, v): + n = 4 + x = u * n; y = v * n + cx = np.floor(x); cy = np.floor(y); fx = x % 1; fy = y % 1 + horiz = (cx + cy) % 2 == 0 + strips = 3 # strips per cell + along = np.where(horiz, fx, fy); across = np.where(horiz, fy, fx) + strip = (across * strips) % 1 + body = np.sqrt(np.clip(1 - (2 * strip - 1) ** 2, 0, None)) # rounded strip + ends = smoothstep(0, 0.06, along) * smoothstep(0, 0.06, 1 - along) + return norm(0.35 + 0.65 * body * ends) + +def chainmail(u, v): + n = 6 + h = np.zeros_like(u) + for ox, oy in ((0.0, 0.0), (0.5, 0.5)): + x = (u * n + ox) % 1 - 0.5; y = (v * n + oy) % 1 - 0.5 + r = np.sqrt(x * x + y * y) + ring = np.exp(-((r - 0.36) / 0.09) ** 2) + h = np.maximum(h, ring) + return norm(h) + +def pyramids(u, v): + n = 8 + x = np.abs((u * n) % 1 - 0.5); y = np.abs((v * n) % 1 - 0.5) + return norm(0.5 - np.maximum(x, y)) + +def waffle(u, v): + n = 6 + x = (u * n) % 1; y = (v * n) % 1 + gap = 0.12 + m = smoothstep(0, gap, x) * smoothstep(0, gap, 1 - x) * smoothstep(0, gap, y) * smoothstep(0, gap, 1 - y) + return norm(m) + +def bubbles(u, v): + rng = np.random.default_rng(111) + h = np.zeros_like(u) + for (px, py), r in zip(wrapped_points(7, 112, 0.9), rng.random(49) * 0.05 + 0.03): + dx = u - px; dx -= np.round(dx); dy = v - py; dy -= np.round(dy) + d2 = dx * dx + dy * dy + h = np.maximum(h, np.sqrt(np.clip(r * r - d2, 0, None)) / 0.08) + return norm(h) + +def cracked_earth(u, v): + pts = wrapped_points(7, 121, 0.7) + d1, d2, idx = voronoi(u, v, pts) + crack = 1 - smoothstep(0.0, 0.03, d2 - d1) + plates = 0.8 + 0.2 * fbm(u, v, 24, 122) + curl = 1 - 0.35 * np.clip(1 - d1 / 0.12, 0, 1) # plates curl up at the edges + return norm(plates * (2 - curl) * (1 - 0.9 * crack)) + +def sand_ripples(u, v): + n = 8 + wob = 0.06 * np.sin(2 * np.pi * u * 2) + 0.03 * fbm(u, v, 4, 131) + h = 0.5 + 0.5 * np.sin(2 * np.pi * (v * n + wob)) + h = h ** 1.6 # sharp crests, soft troughs + return norm(h + 0.05 * fbm(u, v, 64, 132)) + +def bark(u, v): + grooves = fbm((u * 3) % 1, v, 4, 141, octaves=4) # the ridges below stretch it along v + ridges = np.abs(np.sin(2 * np.pi * (u * 9 + grooves * 1.5))) + return norm(ridges ** 0.7 * (0.7 + 0.3 * fbm(u, v, 12, 142)) + 0.1 * fbm(u, v, 48, 143)) + +def slate(u, v): + h = fbm(u, v, 3, 151, octaves=6, gain=0.55) + steps = np.floor(h * 6) / 6 + 0.4 * (h * 6 - np.floor(h * 6)) / 6 # cleaved layers + return norm(steps + 0.05 * fbm(u, v, 48, 152)) + +def triangles(u, v): + n = 6 + x = u * n; y = v * n * np.sqrt(3) / 1.5 # rows of equilateral triangles + row = np.floor(y); fy = y - row + xs = x + 0.5 * (row % 2) + fx = xs % 1 + up = fx < 1 - fy # which triangle of the rhombus + # distance to the nearest edge of the triangle, in either orientation + d_up = np.minimum(np.minimum(fy, fx - 0 * fy), (1 - fy - fx)) + d_dn = np.minimum(np.minimum(1 - fy, 1 - fx), (fx + fy - 1)) + d = np.where(up, d_up, d_dn) + return norm(smoothstep(0.0, 0.08, d)) + +def roof_tiles(u, v): + n = 6 + h = np.zeros_like(u) + for row in range(-1, 2 * n + 1): + cy = row / (2 * n); ox = 0.5 if row % 2 else 0.0 + x = (u * n + ox) % 1 - 0.5; y = v - cy; y -= np.round(y) + yy = y * n * 2 # 0 at the row's exposed edge, rising toward the covered end + inside = (yy >= -0.05) & (yy < 1.0) + arch = np.cos(x * np.pi) * 0.6 + 0.4 + cand = np.where(inside, 0.3 + 0.7 * arch * (1 - 0.35 * yy), 0) + h = np.where(inside & (cand > 0), cand, h) + return norm(h) + +def star_tiles(u, v): + # 8-point stars and crosses (the classic Islamic star-and-cross tiling) + n = 4 + x = (u * n) % 1 - 0.5; y = (v * n) % 1 - 0.5 + a = np.abs(x); b = np.abs(y) + star = np.maximum(np.maximum(a, b), (a + b) / np.sqrt(2) * 1.15) + m = smoothstep(0.42, 0.36, star) + # the crosses between the stars sit on the half-offset lattice + x2 = (u * n + 0.5) % 1 - 0.5; y2 = (v * n + 0.5) % 1 - 0.5 + a2 = np.abs(x2); b2 = np.abs(y2) + cross = np.minimum(np.maximum(a2 / 0.12, b2 / 0.30), np.maximum(a2 / 0.30, b2 / 0.12)) + m2 = smoothstep(1.0, 0.85, cross) * 0.8 + return norm(np.maximum(m, m2)) + +def terrazzo(u, v): + base = np.array([0.82, 0.80, 0.76]) + rgb = np.broadcast_to(base, u.shape + (3,)).copy() * (0.95 + 0.05 * fbm(u, v, 32, 161))[..., None] + rng = np.random.default_rng(162) + pal = np.array([[0.85, 0.30, 0.25], [0.20, 0.35, 0.55], [0.25, 0.25, 0.25], [0.95, 0.90, 0.80], [0.80, 0.60, 0.20]]) + for (px, py), r, c, ang in zip(wrapped_points(12, 163, 1.0), rng.random(144) * 0.02 + 0.012, rng.integers(0, 5, 144), rng.random(144) * 3.14): + dx = u - px; dx -= np.round(dx); dy = v - py; dy -= np.round(dy) + ca, sa = np.cos(ang), np.sin(ang) + ex = (dx * ca - dy * sa) / (r * 1.4); ey = (dx * sa + dy * ca) / r + inside = (np.abs(ex) + np.abs(ey) * 0.7 + np.maximum(np.abs(ex), np.abs(ey)) * 0.5) < 1.0 + rgb[inside] = pal[c] * 0.92 # chips sit a touch below the matrix: darker = lower + return rgb + +def camouflage(u, v): + pal = np.array([[0.36, 0.42, 0.24], [0.55, 0.50, 0.32], [0.22, 0.26, 0.17], [0.60, 0.58, 0.45]]) + a = fbm(u, v, 3, 171, octaves=4); b = fbm(u, v, 3, 172, octaves=4); c = fbm(u, v, 5, 173, octaves=3) + idx = (a > 0.55).astype(int) + 2 * (b > 0.5).astype(int) + idx = np.where(c > 0.72, 3, idx) + rgb = pal[idx].astype(float) + return rgb * (0.94 + 0.06 * fbm(u, v, 48, 174))[..., None] + +def tartan(u, v): + n = 2 + def stripes(t): + t = (t * n) % 1 + band = np.zeros_like(t) + for a, w, val in ((0.0, 0.32, 1), (0.32, 0.06, 2), (0.38, 0.24, 0), (0.62, 0.06, 2), (0.68, 0.32, 1)): + band = np.where((t >= a) & (t < a + w), val, band) + return band + pal = np.array([[0.12, 0.25, 0.20], [0.55, 0.12, 0.14], [0.90, 0.80, 0.30]]) + su = stripes(u).astype(int); sv = stripes(v).astype(int) + weave = ((np.floor(u * 400) + np.floor(v * 400)) % 2) == 0 # the two thread directions alternate + rgb = np.where(weave[..., None], pal[su], pal[sv]).astype(float) + return rgb * (0.9 + 0.1 * (0.5 + 0.5 * np.sin(2 * np.pi * (u + v) * 200)))[..., None] + +GREY = { + 'Diamond Plate': diamond_plate, 'Honeycomb': honeycomb, 'Fine Knurl': knurl, 'Scales': scales, + 'Herringbone': herringbone, 'Cobblestone': cobblestone, 'Leather': leather, 'Carbon Fibre': carbon_fibre, + 'Chevron': chevron, 'Ripples': ripples, 'Hammered': hammered, 'Perforated': perforated, 'Rope': rope, + 'Stone Wall': stone_wall, 'Wood Planks': wood_planks, 'Basket Weave': basket_weave, 'Chainmail': chainmail, + 'Pyramids': pyramids, 'Waffle': waffle, 'Bubbles': bubbles, 'Cracked Earth': cracked_earth, + 'Sand Ripples': sand_ripples, 'Bark': bark, 'Slate': slate, 'Triangles': triangles, 'Roof Tiles': roof_tiles, + 'Star Tiles': star_tiles, 'Bricks weathered': weathered_bricks, +} +COLOUR = {'Colour Bricks': colour_bricks, 'Mosaic Tiles': mosaic, 'Hex Tiles': hex_tiles, 'Terrazzo': terrazzo, + 'Camouflage': camouflage, 'Tartan': tartan} + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('--size', type=int, default=1024) + ap.add_argument('--out', default='resources/textures/displacement') + ap.add_argument('--sheet', default='') + args = ap.parse_args() + os.makedirs(args.out, exist_ok=True) + u, v = grid(args.size) + thumbs = [] + for name, fn in GREY.items(): + img = (np.clip(down(fn(u, v)), 0, 1) * 255 + 0.5).astype(np.uint8) + Image.fromarray(img, 'L').save(os.path.join(args.out, name + '.png'), optimize=True) + thumbs.append((name, np.stack([img] * 3, -1))) + print('wrote', name) + for name, fn in COLOUR.items(): + rgb = fn(u, v) + rgb = np.stack([down(rgb[..., c]) for c in range(3)], -1) + img = (np.clip(rgb, 0, 1) * 255 + 0.5).astype(np.uint8) + Image.fromarray(img, 'RGB').save(os.path.join(args.out, name + '.png'), optimize=True) + thumbs.append((name, img)) + print('wrote', name, '(colour)') + if args.sheet: + t = 256; cols = 7; rows = (len(thumbs) + cols - 1) // cols + sheet = Image.new('RGB', (cols * t, rows * t), (40, 40, 40)) + for i, (name, img) in enumerate(thumbs): + im = Image.fromarray(img).resize((t, t), Image.LANCZOS) + sheet.paste(im, ((i % cols) * t, (i // cols) * t)) + sheet.save(args.sheet); print('sheet', args.sheet) + +if __name__ == '__main__': + main() diff --git a/scripts/tests/test_profile_tool.py b/scripts/tests/test_profile_tool.py index a4e9a6f81b..d32dbd144e 100644 --- a/scripts/tests/test_profile_tool.py +++ b/scripts/tests/test_profile_tool.py @@ -940,6 +940,21 @@ class TestNormalized(TreeCase): # --------------------------------------------------------------------------- class TestFixVariant(TreeCase): + def test_cooling_arrays_require_one_value_per_declared_variant(self): + keys = ("fan_min_speed", "fan_max_speed", "additional_cooling_fan_speed") + variants = ["Direct Drive Standard", "Direct Drive High Flow", "Bowden Standard"] + for width in (1, 2, 3, 4): + with self.subTest(width=width): + self.preset("filament/F.json", instantiation="true", + filament_extruder_variant=variants, + **{key: ["20"] * width for key in keys}) + apt.load_vendor_configs.cache_clear() + errors, out = self.width_errors() + self.assertEqual(errors, 0 if width == 3 else len(keys), out) + if width != 3: + for key in keys: + self.assertIn(f'"{key}" has {width} values', out) + def preset(self, rel, **data): name = os.path.splitext(os.path.basename(rel))[0] self.t.write("V", rel, {"type": rel.split("/")[0], "name": name, **data}) diff --git a/src/dev-utils/OrcaSlicer_profile_validator.cpp b/src/dev-utils/OrcaSlicer_profile_validator.cpp index 4bb7eab64d..c328c565c8 100644 --- a/src/dev-utils/OrcaSlicer_profile_validator.cpp +++ b/src/dev-utils/OrcaSlicer_profile_validator.cpp @@ -308,12 +308,18 @@ DynamicPrintConfig slice_config(PresetBundle &bundle) // type (Direct Drive + Bowden) the mismatched lookup spams [error] lines. Single-nozzle and non-BBL // printers keep the default map (their toolchange rides the AMS/tool-changer path unchanged). const bool pin_filament_map = bundle.is_bbl_vendor() && nozzles > 1; + auto &fmap = bundle.project_config.option("filament_map", true)->values; if (pin_filament_map) { - auto &fmap = bundle.project_config.option("filament_map", true)->values; for (size_t i = 0; i < fmap.size(); ++i) fmap[i] = int(i % nozzles) + 1; } + // A fresh printer selection uses its declared nozzle volumes, just like the + // app. Otherwise a high-flow preset is silently sliced with Standard tuning. + bundle.reset_default_nozzle_volume_type(); + bundle.project_config.option("filament_volume_map", true)->values = + bundle.get_default_nozzle_volume_types_for_filaments(fmap); + DynamicPrintConfig cfg = bundle.full_config(); cfg.set_key_value("enable_prime_tower", new ConfigOptionBool(true)); // force a purge tower so the change is detectable // The map above drives full_config()'s per-filament variant collapse; fmmManual on the sliced config diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 246bb46937..1857412466 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -480,6 +480,30 @@ set(lisbslic3r_sources Tesselate.cpp Tesselate.hpp TextConfiguration.hpp + TextureDisplacement.cpp + TextureDisplacement.hpp + TextureBake/TextureBakeIndex.cpp + TextureBake/TextureBakeIndex.hpp + TextureBake/TextureBakeSubdivide.cpp + TextureBake/TextureBakeSubdivide.hpp + TextureBake/TextureBakeRegularize.cpp + TextureBake/TextureBakeRegularize.hpp + TextureBake/TextureBakeRelocate.cpp + TextureBake/TextureBakeRelocate.hpp + TextureBake/TextureBakeFlip.cpp + TextureBake/TextureBakeFlip.hpp + TextureBake/TextureBakeDebug.cpp + TextureBake/TextureBakeDebug.hpp + TextureBake/TextureBakeDisplace.cpp + TextureBake/TextureBakeDisplace.hpp + TextureBake/TextureBakeDecimate.cpp + TextureBake/TextureBakeDecimate.hpp + TextureBake/TextureBakeRepair.cpp + TextureBake/TextureBakeRepair.hpp + TextureBake/TextureBakePipeline.cpp + TextureBake/TextureBakePipeline.hpp + TextureBake/TextureBakeMesh.cpp + TextureBake/TextureBakeMesh.hpp Thread.cpp Thread.hpp Time.cpp diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index 52b16f9458..9818887c1d 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -2702,6 +2702,9 @@ public: virtual ConfigOption* optptr(const t_config_option_key &opt_key, bool create = false) = 0; // Collect names of all configuration values maintained by this configuration store. virtual t_config_option_keys keys() const = 0; + // Set this config's options on target member by member, when target is of this config's static type or + // derives from it, and return true. apply() prefers this to looking every key up by name. + virtual bool apply_to(ConfigBase &/*target*/) const { return false; } protected: // Verify whether the opt_key has not been obsoleted or renamed. @@ -2753,7 +2756,8 @@ public: // Apply all keys of other ConfigBase defined by this->def() to this ConfigBase. // An UnknownOptionException is thrown in case some option keys of other are not defined by this->def(), // or this ConfigBase is of a StaticConfig type and it does not support some of the keys, and ignore_nonexistent is not set. - void apply(const ConfigBase &other, bool ignore_nonexistent = false) { this->apply_only(other, other.keys(), ignore_nonexistent); } + void apply(const ConfigBase &other, bool ignore_nonexistent = false) + { if (! other.apply_to(*this)) this->apply_only(other, other.keys(), ignore_nonexistent); } // Apply explicitely enumerated keys of other ConfigBase defined by this->def() to this ConfigBase. // An UnknownOptionException is thrown in case some option keys are not defined by this->def(), // or this ConfigBase is of a StaticConfig type and it does not support some of the keys, and ignore_nonexistent is not set. diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 90cca3f084..dedd024f2b 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1258,7 +1258,7 @@ static std::vector get_path_of_change_filament(const Print& print) config.set_key_value("old_filament_temp", new ConfigOptionInt(old_filament_temp)); int interface_temp = full_config.filament_tower_interface_print_temp.get_at(new_filament_id); if (interface_temp == -1) - interface_temp = full_config.nozzle_temperature_range_high.get_at(new_filament_id); + interface_temp = full_config.nozzle_temperature_range_high.get_at(new_fi); if (full_config.enable_tower_interface_features && tcr.is_contact) new_filament_temp = interface_temp; config.set_key_value("new_filament_temp", new ConfigOptionInt(new_filament_temp)); @@ -1309,7 +1309,7 @@ static std::vector get_path_of_change_filament(const Print& print) flush_temps[idx] = use_fast_flush ? m_print_config->filament_flush_temp_fast.get_at(fi) : m_print_config->filament_flush_temp.get_at(fi); if (flush_temps[idx] == 0) - flush_temps[idx] = m_print_config->nozzle_temperature_range_high.get_at(idx); + flush_temps[idx] = m_print_config->nozzle_temperature_range_high.get_at(fi); filament_cooling_before_tower[idx] = m_print_config->filament_cooling_before_tower.get_at(fi); } if (tcr.is_contact || gcodegen.m_layer_index == 0) @@ -1563,7 +1563,7 @@ static std::vector get_path_of_change_filament(const Print& print) const bool will_go_down = !is_approx(z, current_z); const bool is_ramming = (gcodegen.config().single_extruder_multi_material) || (!gcodegen.config().single_extruder_multi_material && - gcodegen.config().filament_multitool_ramming.get_at(tcr.initial_tool)); + gcodegen.config().filament_multitool_ramming.get_at(gcodegen.get_filament_config_index(tcr.initial_tool))); // Orca: user-facing override (Printer Settings > Wipe tower > "Tool change on wipe tower"). // Forces the toolhead to travel over the wipe tower before issuing Tx even on multi-toolhead // printers without ramming, where Orca would otherwise emit Tx in place (potentially over the part). @@ -1620,7 +1620,7 @@ static std::vector get_path_of_change_filament(const Print& print) if (gcodegen.config().enable_tower_interface_features && tcr.is_contact) { interface_temp = gcodegen.config().filament_tower_interface_print_temp.get_at(new_extruder_id); if (interface_temp == -1) - interface_temp = gcodegen.config().nozzle_temperature_range_high.get_at(new_extruder_id); + interface_temp = gcodegen.config().nozzle_temperature_range_high.get_at(gcodegen.get_filament_config_index(new_extruder_id)); toolchange_temp_override = interface_temp; } toolchange_gcode_str = gcodegen.set_extruder(new_extruder_id, tcr.print_z, false, toolchange_temp_override, @@ -4342,6 +4342,12 @@ size_t GCode::get_filament_config_index(int filament_id) const return filament_id; } +size_t GCode::get_filament_config_index(int filament_id, size_t layer_id) const +{ + // Orca: uncached, as the stages after the generator run concurrently with it. + return m_print ? m_print->get_filament_config_indx(filament_id, (int) layer_id, false) : filament_id; +} + size_t GCode::get_nozzle_config_index(int filament_id) const { if (m_print) { @@ -9409,10 +9415,13 @@ void GCode::update_placeholder_parser_with_variant_params() // Helper: remap config arrays from variant index space to filament_id index space. // After remapping, gcode templates can use param[filament_id] directly. + std::vector config_index(num_filaments); + for (size_t i = 0; i < num_filaments; ++i) + config_index[i] = get_filament_config_index(i); auto remap_by_filament = [&](const auto &src) { std::decay_t dst(num_filaments); for (size_t i = 0; i < num_filaments; ++i) - dst[i] = src.get_at(get_filament_config_index(i)); + dst[i] = src.get_at(config_index[i]); return dst; }; @@ -9427,6 +9436,16 @@ void GCode::update_placeholder_parser_with_variant_params() this->placeholder_parser().set("first_layer_temperature", new ConfigOptionInts(remap_by_filament(m_config.nozzle_temperature_initial_layer))); this->placeholder_parser().set("pressure_advance", new ConfigOptionFloats(remap_by_filament(m_config.pressure_advance))); this->placeholder_parser().set("enable_pressure_advance", new ConfigOptionBools(remap_by_filament(m_config.enable_pressure_advance))); + this->placeholder_parser().set("fan_min_speed", new ConfigOptionFloats(remap_by_filament(m_config.fan_min_speed))); + this->placeholder_parser().set("fan_max_speed", new ConfigOptionFloats(remap_by_filament(m_config.fan_max_speed))); + this->placeholder_parser().set("additional_cooling_fan_speed", new ConfigOptionInts(remap_by_filament(m_config.additional_cooling_fan_speed))); + this->placeholder_parser().set("filament_minimal_purge_on_wipe_tower", new ConfigOptionFloats(remap_by_filament(m_config.filament_minimal_purge_on_wipe_tower))); + this->placeholder_parser().set("filament_multitool_ramming", new ConfigOptionBools(remap_by_filament(m_config.filament_multitool_ramming))); + this->placeholder_parser().set("filament_multitool_ramming_volume", new ConfigOptionFloats(remap_by_filament(m_config.filament_multitool_ramming_volume))); + this->placeholder_parser().set("filament_multitool_ramming_flow", new ConfigOptionFloats(remap_by_filament(m_config.filament_multitool_ramming_flow))); + this->placeholder_parser().set("nozzle_temperature_range_low", new ConfigOptionInts(remap_by_filament(m_config.nozzle_temperature_range_low))); + const auto nozzle_temperature_range_high = remap_by_filament(m_config.nozzle_temperature_range_high); + this->placeholder_parser().set("nozzle_temperature_range_high", new ConfigOptionInts(nozzle_temperature_range_high)); // --- printer_options_with_variant_1: in m_config these are already merged as filament-indexed --- this->placeholder_parser().set("retraction_distances_when_cut", new ConfigOptionFloats(remap_by_filament(m_config.retraction_distances_when_cut))); @@ -9447,7 +9466,7 @@ void GCode::update_placeholder_parser_with_variant_params() if (flush_v_speed[i] == 0) flush_v_speed[i] = filament_max_v[i]; if (flush_temps[i] == 0) - flush_temps[i] = m_config.nozzle_temperature_range_high.get_at(i); + flush_temps[i] = nozzle_temperature_range_high[i]; } this->placeholder_parser().set("flush_volumetric_speeds", new ConfigOptionFloats(flush_v_speed)); this->placeholder_parser().set("flush_temperatures", new ConfigOptionInts(flush_temps)); @@ -9706,7 +9725,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo { int interface_temp = m_config.filament_tower_interface_print_temp.get_at(new_filament_id); if (interface_temp == -1) - interface_temp = m_config.nozzle_temperature_range_high.get_at(new_filament_id); + interface_temp = m_config.nozzle_temperature_range_high.get_at(new_fi); dyn_config.set_key_value("filament_tower_interface_print_temp", new ConfigOptionInt(interface_temp)); } if (toolchange_temp_override > 0) { @@ -9745,7 +9764,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo flush_temps[idx] = use_fast_flush ? m_print->config().filament_flush_temp_fast.get_at(fi) : m_print->config().filament_flush_temp.get_at(fi); if (flush_temps[idx] == 0) - flush_temps[idx] = m_print->config().nozzle_temperature_range_high.get_at(idx); + flush_temps[idx] = m_print->config().nozzle_temperature_range_high.get_at(fi); filament_cooling_before_tower[idx] = m_print->config().filament_cooling_before_tower.get_at(fi); } std::fill(filament_cooling_before_tower.begin(), filament_cooling_before_tower.end(), 0); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index bded77d9d1..6516a3f6fd 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -296,6 +296,9 @@ public: // resolver keys filament-indexed arrays, the nozzle resolver keys (extruder x volume-type) // slot arrays. Both degenerate to filament_id / extruder index on single-volume printers. size_t get_filament_config_index(int filament_id) const; + // The filament resolver for a given layer, for the export pipeline stages after the generator, + // which run behind the current layer and concurrently with the generator. + size_t get_filament_config_index(int filament_id, size_t layer_id) const; size_t get_nozzle_config_index(int filament_id) const; // Object and support extrusions of the same PrintObject at the same print_z. diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp index e55b89212e..7f6e9c67d5 100644 --- a/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -18,7 +18,7 @@ namespace Slic3r { -CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_toolchange_prefix(gcodegen.writer().toolchange_prefix()), m_current_extruder(0), m_current_nozzle(0) +CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_gcodegen(gcodegen), m_toolchange_prefix(gcodegen.writer().toolchange_prefix()), m_current_extruder(0), m_current_nozzle(0) { this->reset(gcodegen.writer().get_position()); @@ -737,10 +737,12 @@ std::string CoolingBuffer::apply_layer_cooldown( &ironing_fan_control, &ironing_fan_speed ](bool immediately_apply) { #define EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(m_current_extruder) - float fan_min_speed = EXTRUDER_CONFIG(fan_min_speed); + // The per-variant options take the extruder variant the filament prints with on this layer + const size_t config_index = m_gcodegen.get_filament_config_index(m_current_extruder, layer_id); + float fan_min_speed = m_config.fan_min_speed.get_at(config_index); float fan_speed_new = EXTRUDER_CONFIG(reduce_fan_stop_start_freq) ? fan_min_speed : 0; //BBS - int additional_fan_speed_new = EXTRUDER_CONFIG(additional_cooling_fan_speed); + int additional_fan_speed_new = m_config.additional_cooling_fan_speed.get_at(config_index); int close_fan_the_first_x_layers = EXTRUDER_CONFIG(close_fan_the_first_x_layers); // Is the fan speed ramp enabled? int full_fan_speed_layer = EXTRUDER_CONFIG(full_fan_speed_layer); @@ -776,7 +778,7 @@ std::string CoolingBuffer::apply_layer_cooldown( // additional_fan_speed_new is left at its configured value (auxiliary fan is independent of the // part-cooling override). } else if (int(layer_id) >= close_fan_the_first_x_layers) { - float fan_max_speed = EXTRUDER_CONFIG(fan_max_speed); + float fan_max_speed = m_config.fan_max_speed.get_at(config_index); float slow_down_layer_time = float(EXTRUDER_CONFIG(slow_down_layer_time)); float fan_cooling_layer_time = float(EXTRUDER_CONFIG(fan_cooling_layer_time)); //BBS: always enable the fan speed interpolation according to layer time diff --git a/src/libslic3r/GCode/CoolingBuffer.hpp b/src/libslic3r/GCode/CoolingBuffer.hpp index 9b0a5ab9df..742b8f78b7 100644 --- a/src/libslic3r/GCode/CoolingBuffer.hpp +++ b/src/libslic3r/GCode/CoolingBuffer.hpp @@ -54,6 +54,8 @@ private: // Referencs GCode::m_config, which is FullPrintConfig. While the PrintObjectConfig slice of FullPrintConfig is being modified, // the PrintConfig slice of FullPrintConfig is constant, thus no thread synchronization is required. const PrintConfig &m_config; + // Resolves the filament config index of the per-variant options. + const GCode &m_gcodegen; unsigned int m_current_extruder; unsigned int m_current_nozzle; //BBS: current fan speed diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index 7ea46cf8a7..5026323d54 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -1230,15 +1230,21 @@ void ToolOrdering::cal_most_used_extruder(const PrintConfig &config) float ToolOrdering::cal_max_additional_fan(const PrintConfig &config) { - // record + std::set used_filaments; + for (const LayerTools &layer_tools : m_layer_tools) + used_filaments.insert(layer_tools.extruders.begin(), layer_tools.extruders.end()); + if (used_filaments.empty()) + return 0; + + // Orca: additional_cooling_fan_speed can hold one value per extruder variant a filament prints with; + // filament_self_index maps such a column to its filament. + const std::vector &self_index = config.filament_self_index.values; + const size_t columns = std::max(config.additional_cooling_fan_speed.size(), size_t(*used_filaments.rbegin()) + 1); float max_fan = 0; - for (LayerTools &layer_tools : m_layer_tools) { - std::vector filaments = layer_tools.extruders; - std::set layer_extruder_count; - // count once only - for (unsigned int &filament : filaments) - if (max_fan < config.additional_cooling_fan_speed.get_at(filament)) - max_fan = config.additional_cooling_fan_speed.get_at(filament); + for (size_t column = 0; column < columns; ++column) { + const unsigned int filament_id = self_index.size() == columns ? self_index[column] - 1 : column; + if (used_filaments.count(filament_id) && max_fan < config.additional_cooling_fan_speed.get_at(column)) + max_fan = config.additional_cooling_fan_speed.get_at(column); } return max_fan; } diff --git a/src/libslic3r/MeshBoolean.cpp b/src/libslic3r/MeshBoolean.cpp index 7497f92abf..4d9dda0c2e 100644 --- a/src/libslic3r/MeshBoolean.cpp +++ b/src/libslic3r/MeshBoolean.cpp @@ -26,6 +26,13 @@ #include #include #include +// For parameterize_lscm() +#include +#include +#include +#include +#include +#include // BBS: for boolean using mcut #include "mcut/include/mcut/mcut.h" @@ -249,6 +256,151 @@ indexed_triangle_set cgal_to_indexed_triangle_set(const CGALMesh &cgalmesh) return cgal_to_indexed_triangle_set(cgalmesh.m); } +// ///////////////////////////////////////////////////////////////////////////// +// Isotropic remeshing +// ///////////////////////////////////////////////////////////////////////////// + +indexed_triangle_set remesh_isotropic(const indexed_triangle_set &mesh, double target_edge_length, + unsigned n_iterations, double sharp_angle_deg, + unsigned n_relaxation_steps) +{ + if (mesh.indices.empty() || target_edge_length <= 0.0) + return mesh; + + _EpicMesh cgal_mesh; + triangle_mesh_to_cgal(mesh.vertices, mesh.indices, cgal_mesh); + if (cgal_mesh.is_empty() || cgal_mesh.number_of_faces() == 0) + return mesh; + + // Surface_mesh::add_face() refuses any face that would make the mesh non-manifold and returns a + // null descriptor instead. Remeshing a mesh that silently lost faces that way produces holes in + // the output, so bail out and let the caller report it rather than hand back a punctured model. + if (cgal_mesh.number_of_faces() != mesh.indices.size()) + return mesh; + + using edge_descriptor = boost::graph_traits<_EpicMesh>::edge_descriptor; + try { + // Sharp edges and open borders are pinned before remeshing. Without that, the tangential + // relaxation pass slides vertices along the surface and rounds every hard feature off - a + // cube comes back with wobbly, eroded edges, which is the most visible way "remeshing does + // not work properly". protect_constraints() forbids splitting or collapsing them, but it + // requires each constrained edge to already be shorter than 4/3 * target, hence the split + // first (passing the map so the halves inherit the constraint). This mirrors CGAL's own + // isotropic_remeshing example. + auto ecm = cgal_mesh.add_property_map("e:is_constrained", false).first; + if (sharp_angle_deg > 0.0) + CGALProc::detect_sharp_edges(cgal_mesh, sharp_angle_deg, ecm); + for (edge_descriptor e : edges(cgal_mesh)) { + const auto h = halfedge(e, cgal_mesh); + if (is_border(h, cgal_mesh) || is_border(opposite(h, cgal_mesh), cgal_mesh)) + put(ecm, e, true); + } + + std::vector constrained; + for (edge_descriptor e : edges(cgal_mesh)) + if (get(ecm, e)) + constrained.push_back(e); + if (!constrained.empty()) + CGALProc::split_long_edges(constrained, target_edge_length, cgal_mesh, + CGALParams::edge_is_constrained_map(ecm)); + + CGALProc::isotropic_remeshing(faces(cgal_mesh), target_edge_length, cgal_mesh, + CGALParams::number_of_iterations(n_iterations) + .number_of_relaxation_steps(n_relaxation_steps) + .edge_is_constrained_map(ecm) + .protect_constraints(true)); + } catch (const std::exception &) { + return mesh; // CGAL throws on some non-manifold / degenerate inputs; leave the mesh untouched + } + if (cgal_mesh.number_of_faces() == 0) + return mesh; + + // isotropic_remeshing edits in place, and its edge collapses only *mark* vertices and faces as + // removed - the underlying arrays keep the holes until the garbage is collected. That matters + // because cgal_to_indexed_triangle_set() numbers its output vertices by iteration order (which + // skips removed slots) while reading each face's corner as the raw integer value of the vertex + // descriptor (which does not). Past the first collapse the two disagree, so every triangle + // points at the wrong vertices, and any descriptor beyond the live vertex count is dropped + // together with its triangle. Compacting first makes descriptor == iteration order again. + cgal_mesh.collect_garbage(); + return cgal_to_indexed_triangle_set(cgal_mesh); +} + +// ///////////////////////////////////////////////////////////////////////////// +// UV parameterization +// ///////////////////////////////////////////////////////////////////////////// + +std::optional> parameterize_lscm(const indexed_triangle_set &mesh) +{ + namespace SMP = CGAL::Surface_mesh_parameterization; + + if (mesh.indices.empty()) + return std::nullopt; + + _EpicMesh cgal_mesh; + triangle_mesh_to_cgal(mesh.vertices, mesh.indices, cgal_mesh); + + using vertex_descriptor = boost::graph_traits<_EpicMesh>::vertex_descriptor; + using halfedge_descriptor = boost::graph_traits<_EpicMesh>::halfedge_descriptor; + + // LSCM assumes a single topological disk: one connected component, one boundary loop. A patch + // with several disconnected painted islands, or with a hole in it, doesn't qualify -- bail out + // rather than silently parameterizing just one arbitrary piece of it. + { + std::vector component_id(num_faces(cgal_mesh)); + const std::size_t num_components = CGAL::Polygon_mesh_processing::connected_components( + cgal_mesh, CGAL::make_property_map(component_id)); + if (num_components != 1) + return std::nullopt; + } + + const halfedge_descriptor border = CGAL::Polygon_mesh_processing::longest_border(cgal_mesh).first; + if (border == halfedge_descriptor()) + return std::nullopt; // no boundary at all -- a closed patch, which isn't a disk either + + // ...and exactly one boundary loop. One connected component is not enough on its own: a patch with + // a hole in it (paint a ring, or erase the middle of a stroke) is a single component with two + // loops, and LSCM will happily "parameterize" it into an overlapping, folded-over chart rather + // than fail. Walk the border halfedges and check every one of them belongs to the longest loop. + { + std::size_t border_halfedges = 0; + for (halfedge_descriptor h : halfedges(cgal_mesh)) + if (is_border(h, cgal_mesh)) + ++border_halfedges; + std::size_t loop_length = 0; + halfedge_descriptor h = border; + do { + ++loop_length; + h = next(h, cgal_mesh); + } while (h != border && loop_length <= border_halfedges); + if (loop_length != border_halfedges) + return std::nullopt; // more than one boundary loop -- not a topological disk + } + + using Point_2 = EpicKernel::Point_2; + using UV_pmap = _EpicMesh::Property_map; + UV_pmap uv_map = cgal_mesh.add_property_map("h:uv", Point_2(0, 0)).first; + + using Parameterizer = SMP::LSCM_parameterizer_3<_EpicMesh>; + const SMP::Error_code err = SMP::parameterize(cgal_mesh, Parameterizer(), border, uv_map); + if (err != SMP::OK) + return std::nullopt; + + // triangle_mesh_to_cgal() adds vertices in the exact same order as mesh.vertices (see above), + // and Surface_mesh assigns indices sequentially on insertion into a fresh mesh, so a + // vertex_descriptor's index here is guaranteed to match the original input vertex index -- + // the same assumption cgal_to_indexed_triangle_set() above already relies on. + std::vector result(mesh.vertices.size(), Vec2f::Zero()); + for (vertex_descriptor vd : vertices(cgal_mesh)) { + const std::size_t idx = std::size_t(vd); + if (idx < result.size()) { + const Point_2 &uv = uv_map[vd]; + result[idx] = Vec2f(float(uv.x()), float(uv.y())); + } + } + return result; +} + // ///////////////////////////////////////////////////////////////////////////// // Boolean operations for CGAL meshes // ///////////////////////////////////////////////////////////////////////////// diff --git a/src/libslic3r/MeshBoolean.hpp b/src/libslic3r/MeshBoolean.hpp index d9a5d196fb..c307d1528b 100644 --- a/src/libslic3r/MeshBoolean.hpp +++ b/src/libslic3r/MeshBoolean.hpp @@ -3,6 +3,8 @@ #include #include +#include +#include #include #include @@ -73,6 +75,27 @@ bool empty(const CGALMesh &mesh); // Repair a mesh using CGAL. Returns true on success. Optionally returns a summary of repairs and an error string. bool repair(TriangleMesh &mesh, RepairedMeshErrors *repaired_errors = nullptr, std::string *error = nullptr); + +// Real UV unwrap of an open mesh patch via CGAL's LSCM (Least Squares Conformal Maps) surface +// parameterization. Returns one UV coordinate per input vertex (same indexing as `mesh.vertices`), +// or nullopt if `mesh` isn't a single topological disk -- LSCM needs exactly one connected +// component with exactly one boundary loop, true for a typical single brush stroke/patch but not +// guaranteed for multiple disconnected painted islands merged into one mesh. +std::optional> parameterize_lscm(const indexed_triangle_set &mesh); + +// Isotropic remeshing (CGAL): rebuilds the mesh so its triangles are close to a uniform target edge +// length, splitting oversized triangles and collapsing undersized ones. Used to even out a model with +// wildly varying triangle sizes so texture displacement has a consistent vertex density to work with. +// Edges whose dihedral angle exceeds `sharp_angle_deg`, and any open border, are held fixed so hard +// features survive instead of being eroded by the relaxation pass; pass 0 to remesh everything. +// Returns the input unchanged if remeshing fails (e.g. a non-manifold or self-intersecting input). +// `n_relaxation_steps` is the number of tangential relaxation passes run inside each iteration. That +// relaxation is what actually evens out the triangle distribution - splitting and collapsing alone +// only bring edge *lengths* near the target, leaving the vertices wherever they happened to land. CGAL +// defaults it to 1, which on a few iterations is not enough to look uniform. +indexed_triangle_set remesh_isotropic(const indexed_triangle_set &mesh, double target_edge_length, + unsigned n_iterations = 3, double sharp_angle_deg = 40.0, + unsigned n_relaxation_steps = 1); } namespace mcut { diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 1c6086dd80..fbc299cf72 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -2087,6 +2087,11 @@ void ModelVolume::reset_extra_facets() this->seam_facets.reset(); this->mmu_segmentation_facets.reset(); this->fuzzy_skin_facets.reset(); + // Texture-displacement paint data has no remap-across-topology-change support yet (see + // build_texture_displacement()'s documented limitation), so it must be dropped here rather + // than left referring to a mesh that no longer matches it. + for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) + this->texture_displacement_facet(i).reset(); } std::optional ModelVolume::save_painting() const @@ -2989,6 +2994,11 @@ void ModelVolume::assign_new_unique_ids_recursive() seam_facets.set_new_unique_id(); mmu_segmentation_facets.set_new_unique_id(); fuzzy_skin_facets.set_new_unique_id(); + // As set_new_unique_id() already does: the undo/redo stack stores FacetsAnnotation contents keyed + // by ObjectID, so a clone left sharing these ids with its source can be handed the source's mask + // on an undo - after which a paint mask and the mesh it was recorded against no longer match. + for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) + texture_displacement_facet(i).set_new_unique_id(); } void ModelVolume::rotate(double angle, Axis axis) diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 9b8c340ac8..895ea30d09 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -19,6 +19,7 @@ #include "TextConfiguration.hpp" #include "EmbossShape.hpp" #include "TriangleSelector.hpp" +#include "TextureDisplacement.hpp" //BBS: add bbs 3mf #include "Format/bbs_3mf.hpp" @@ -28,6 +29,7 @@ #include "Format/STL.hpp" #include "Format/OBJ.hpp" +#include #include #include #include @@ -901,6 +903,69 @@ public: // List of mesh facets painted for fuzzy skin. FacetsAnnotation fuzzy_skin_facets; + // One independent paint mask per texture-displacement layer slot (see texture_displacement_layers + // below). Unlike the other facets fields above, a triangle may be painted (ENFORCER) in more + // than one of these simultaneously -- that overlap is what makes the layers "blend". + // + // These are 8 plain named fields rather than a std::array: FacetsAnnotation's + // default/copy constructors are private and friended only to ModelVolume, but std::array's own + // implicitly-defined default/copy constructors are generated with std::array's access rights, + // not ModelVolume's -- so an array of FacetsAnnotation ends up with its default/copy + // constructors implicitly deleted regardless of the friend declaration. Use + // texture_displacement_facet(slot) below for array-like indexed access. + FacetsAnnotation texture_displacement_facets_0; + FacetsAnnotation texture_displacement_facets_1; + FacetsAnnotation texture_displacement_facets_2; + FacetsAnnotation texture_displacement_facets_3; + FacetsAnnotation texture_displacement_facets_4; + FacetsAnnotation texture_displacement_facets_5; + FacetsAnnotation texture_displacement_facets_6; + FacetsAnnotation texture_displacement_facets_7; + + FacetsAnnotation& texture_displacement_facet(int slot) { + switch (slot) { + case 0: return texture_displacement_facets_0; + case 1: return texture_displacement_facets_1; + case 2: return texture_displacement_facets_2; + case 3: return texture_displacement_facets_3; + case 4: return texture_displacement_facets_4; + case 5: return texture_displacement_facets_5; + case 6: return texture_displacement_facets_6; + default: assert(slot == 7); return texture_displacement_facets_7; + } + } + const FacetsAnnotation& texture_displacement_facet(int slot) const { return const_cast(this)->texture_displacement_facet(slot); } + + // Small helpers for the constructor asserts below (kept out of line-noise at each call site). + bool texture_displacement_facets_ids_valid() const { + for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) + if (!texture_displacement_facet(i).id().valid() || texture_displacement_facet(i).id() == this->id()) + return false; + return true; + } + bool texture_displacement_facets_ids_invalid() const { + for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) + if (texture_displacement_facet(i).id().valid()) + return false; + return true; + } + bool texture_displacement_facets_all_empty() const { + for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) + if (!texture_displacement_facet(i).empty()) + return false; + return true; + } + + // Texture assets (height maps) and their projection/displacement parameters. Element order + // is not meaningful for baking (layers are applied in TextureDisplacementLayer::slot order, + // see build_texture_displacement()); it only reflects UI insertion order. + std::vector texture_displacement_layers; + + // Whole-stack displacement settings (border handling, post-process smoothing) - see + // TextureDisplacementOptions. They live beside the layers rather than on one of them because + // they are not a property of any single layer. + TextureDisplacementOptions texture_displacement_options; + // Save painting data before reset_extra_facets() discards it. // Used for replacing mesh without losing painting data. // Only for model parts (not modifiers/connectors). @@ -1046,13 +1111,18 @@ public: this->seam_facets.set_new_unique_id(); this->mmu_segmentation_facets.set_new_unique_id(); this->fuzzy_skin_facets.set_new_unique_id(); + for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) + this->texture_displacement_facet(i).set_new_unique_id(); } bool is_fdm_support_painted() const { return !this->supported_facets.empty(); } bool is_seam_painted() const { return !this->seam_facets.empty(); } bool is_mm_painted() const { return !this->mmu_segmentation_facets.empty(); } bool is_fuzzy_skin_painted() const { return !this->fuzzy_skin_facets.empty(); } - bool is_any_painted() const { return is_fdm_support_painted() || is_seam_painted() || is_mm_painted() || is_fuzzy_skin_painted(); } + bool is_texture_displacement_painted() const { return !this->texture_displacement_facets_all_empty(); } + bool is_any_painted() const { + return is_fdm_support_painted() || is_seam_painted() || is_mm_painted() || is_fuzzy_skin_painted() || is_texture_displacement_painted(); + } // Orca: Implement prusa's filament shrink compensation approach // Returns 0-based indices of extruders painted by multi-material painting gizmo. @@ -1105,6 +1175,7 @@ private: assert(this->seam_facets.id().valid()); assert(this->mmu_segmentation_facets.id().valid()); assert(this->fuzzy_skin_facets.id().valid()); + assert(this->texture_displacement_facets_ids_valid()); assert(this->id() != this->config.id()); assert(this->id() != this->supported_facets.id()); assert(this->id() != this->seam_facets.id()); @@ -1121,6 +1192,7 @@ private: assert(this->seam_facets.id().valid()); assert(this->mmu_segmentation_facets.id().valid()); assert(this->fuzzy_skin_facets.id().valid()); + assert(this->texture_displacement_facets_ids_valid()); assert(this->id() != this->config.id()); assert(this->id() != this->supported_facets.id()); assert(this->id() != this->seam_facets.id()); @@ -1135,6 +1207,7 @@ private: assert(this->seam_facets.id().valid()); assert(this->mmu_segmentation_facets.id().valid()); assert(this->fuzzy_skin_facets.id().valid()); + assert(this->texture_displacement_facets_ids_valid()); assert(this->id() != this->config.id()); assert(this->id() != this->supported_facets.id()); assert(this->id() != this->seam_facets.id()); @@ -1148,10 +1221,17 @@ private: name(other.name), source(other.source), m_mesh(other.m_mesh), m_convex_hull(other.m_convex_hull), config(other.config), m_type(other.m_type), object(object), m_transformation(other.m_transformation), supported_facets(other.supported_facets), seam_facets(other.seam_facets), mmu_segmentation_facets(other.mmu_segmentation_facets), - fuzzy_skin_facets(other.fuzzy_skin_facets), cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape) + fuzzy_skin_facets(other.fuzzy_skin_facets), + texture_displacement_facets_0(other.texture_displacement_facets_0), texture_displacement_facets_1(other.texture_displacement_facets_1), + texture_displacement_facets_2(other.texture_displacement_facets_2), texture_displacement_facets_3(other.texture_displacement_facets_3), + texture_displacement_facets_4(other.texture_displacement_facets_4), texture_displacement_facets_5(other.texture_displacement_facets_5), + texture_displacement_facets_6(other.texture_displacement_facets_6), texture_displacement_facets_7(other.texture_displacement_facets_7), + texture_displacement_layers(other.texture_displacement_layers), + texture_displacement_options(other.texture_displacement_options), + cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape) { - assert(this->id().valid()); - assert(this->config.id().valid()); + assert(this->id().valid()); + assert(this->config.id().valid()); assert(this->supported_facets.id().valid()); assert(this->seam_facets.id().valid()); assert(this->mmu_segmentation_facets.id().valid()); @@ -1201,6 +1281,8 @@ private: assert(this->seam_facets.empty()); assert(this->mmu_segmentation_facets.empty()); assert(this->fuzzy_skin_facets.empty()); + assert(this->texture_displacement_facets_all_empty()); + assert(this->texture_displacement_layers.empty()); } ModelVolume& operator=(ModelVolume &rhs) = delete; @@ -1208,13 +1290,17 @@ private: friend class cereal::access; friend class UndoRedo::StackImpl; // Used for deserialization, therefore no IDs are allocated. - ModelVolume() : ObjectBase(-1), config(-1), supported_facets(-1), seam_facets(-1), mmu_segmentation_facets(-1), fuzzy_skin_facets(-1), object(nullptr) { + ModelVolume() : ObjectBase(-1), config(-1), supported_facets(-1), seam_facets(-1), mmu_segmentation_facets(-1), fuzzy_skin_facets(-1), + texture_displacement_facets_0(-1), texture_displacement_facets_1(-1), texture_displacement_facets_2(-1), texture_displacement_facets_3(-1), + texture_displacement_facets_4(-1), texture_displacement_facets_5(-1), texture_displacement_facets_6(-1), texture_displacement_facets_7(-1), + object(nullptr) { assert(this->id().invalid()); assert(this->config.id().invalid()); assert(this->supported_facets.id().invalid()); assert(this->seam_facets.id().invalid()); assert(this->mmu_segmentation_facets.id().invalid()); assert(this->fuzzy_skin_facets.id().invalid()); + assert(this->texture_displacement_facets_ids_invalid()); } template void load(Archive &ar) { bool has_convex_hull; @@ -1234,6 +1320,13 @@ private: mesh_changed |= t != mmu_segmentation_facets.timestamp(); cereal::load_by_value(ar, fuzzy_skin_facets); mesh_changed |= t != fuzzy_skin_facets.timestamp(); + for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) { + FacetsAnnotation &f = texture_displacement_facet(i); + Timestamp tf = f.timestamp(); + cereal::load_by_value(ar, f); + mesh_changed |= tf != f.timestamp(); + } + ar(texture_displacement_layers, texture_displacement_options); cereal::load_by_value(ar, config); cereal::load(ar, text_configuration); cereal::load(ar, emboss_shape); @@ -1255,6 +1348,9 @@ private: cereal::save_by_value(ar, seam_facets); cereal::save_by_value(ar, mmu_segmentation_facets); cereal::save_by_value(ar, fuzzy_skin_facets); + for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) + cereal::save_by_value(ar, texture_displacement_facet(i)); + ar(texture_displacement_layers, texture_displacement_options); cereal::save_by_value(ar, config); cereal::save(ar, text_configuration); cereal::save(ar, emboss_shape); diff --git a/src/libslic3r/PNGReadWrite.cpp b/src/libslic3r/PNGReadWrite.cpp index 32b9a8f79e..9f6a8b739d 100644 --- a/src/libslic3r/PNGReadWrite.cpp +++ b/src/libslic3r/PNGReadWrite.cpp @@ -54,9 +54,43 @@ static void png_read_callback(png_struct *png_ptr, // Retrieve our input buffer through the png_ptr auto reader = static_cast(png_get_io_ptr(png_ptr)); - if (!reader || !reader->is_ok()) return; + // libpng expects a short read to be reported through png_error(); returning quietly would leave + // it decoding whatever happened to be in outBytes. + if (!reader || !reader->is_ok() || + reader->read(static_cast(outBytes), byteCountToRead) != byteCountToRead) + png_error(png_ptr, "PNG data is truncated"); +} - reader->read(static_cast(outBytes), byteCountToRead); +// libpng reports a corrupt or truncated image by longjmp()ing back to the jump buffer set with +// setjmp(). The frame it lands in must own nothing that needs destroying: with exceptions enabled +// MSVC unwinds the stack as part of longjmp, and returning from a frame unwound that way crashes - +// which is what a truncated texture did on Windows while working everywhere else. So the calls that +// can fail live in these two helpers, which hold nothing but pointers, and every C++ object the +// decoders need stays in their own frames. +static bool png_read_header_guarded(png_struct *png, png_info *info, IStream *in_buf, int sig_bytes) +{ + if (setjmp(png_jmpbuf(png))) + return false; + + png_set_read_fn(png, static_cast(in_buf), png_read_callback); + // Tell that we have already read the first bytes to check the signature + png_set_sig_bytes(png, sig_bytes); + png_read_info(png, info); + return true; +} + +// `bottom_up` fills the buffer last row first, which is the order the colour decoder hands back. +static bool png_read_rows_guarded(png_struct *png, png_info *info, png_bytep dst, size_t rows, size_t rowbytes, + bool bottom_up, bool read_end) +{ + if (setjmp(png_jmpbuf(png))) + return false; + + for (size_t i = 0; i < rows; ++i) + png_read_row(png, dst + (bottom_up ? rows - 1 - i : i) * rowbytes, nullptr); + if (read_end) + png_read_end(png, info); + return true; } bool decode_png(IStream &in_buf, ImageGreyscale &out_img) @@ -77,12 +111,8 @@ bool decode_png(IStream &in_buf, ImageGreyscale &out_img) dsc.info = png_create_info_struct(dsc.png); if(!dsc.info) return false; - png_set_read_fn(dsc.png, static_cast(&in_buf), png_read_callback); - - // Tell that we have already read the first bytes to check the signature - png_set_sig_bytes(dsc.png, PNG_SIG_BYTES); - - png_read_info(dsc.png, dsc.info); + if (!png_read_header_guarded(dsc.png, dsc.info, &in_buf, PNG_SIG_BYTES)) + return false; out_img.cols = png_get_image_width(dsc.png, dsc.info); out_img.rows = png_get_image_height(dsc.png, dsc.info); @@ -94,11 +124,8 @@ bool decode_png(IStream &in_buf, ImageGreyscale &out_img) out_img.buf.resize(out_img.rows * out_img.cols); - auto readbuf = static_cast(out_img.buf.data()); - for (size_t r = 0; r < out_img.rows; ++r) - png_read_row(dsc.png, readbuf + r * out_img.cols, nullptr); - - return true; + return png_read_rows_guarded(dsc.png, dsc.info, static_cast(out_img.buf.data()), out_img.rows, + out_img.cols, /* bottom_up */ false, /* read_end */ false); } bool decode_colored_png(IStream &in_buf, ImageColorscale &out_img) @@ -128,12 +155,10 @@ bool decode_colored_png(IStream &in_buf, ImageColorscale &out_img) return false; } - png_set_read_fn(dsc.png, static_cast(&in_buf), png_read_callback); - - // Tell that we have already read the first bytes to check the signature - png_set_sig_bytes(dsc.png, PNG_SIG_BYTES); - - png_read_info(dsc.png, dsc.info); + if (!png_read_header_guarded(dsc.png, dsc.info, &in_buf, PNG_SIG_BYTES)) { + BOOST_LOG_TRIVIAL(error) << "decode_colored_png: corrupt or truncated PNG data"; + return false; + } out_img.cols = png_get_image_width(dsc.png, dsc.info); out_img.rows = png_get_image_height(dsc.png, dsc.info); @@ -162,13 +187,12 @@ bool decode_colored_png(IStream &in_buf, ImageColorscale &out_img) int interlace_type = png_get_interlace_type(dsc.png, dsc.info); BOOST_LOG_TRIVIAL(info) << boost::format("filter_type %1%, compression_type %2%, interlace_type %3%, rowbytes %4%")%filter_type %compression_type %interlace_type %rowbytes; - auto readbuf = static_cast(out_img.buf.data()); - for (size_t r = out_img.rows; r > 0; r--) - { - png_read_row(dsc.png, readbuf + (r - 1) * rowbytes, nullptr); + if (!png_read_rows_guarded(dsc.png, dsc.info, static_cast(out_img.buf.data()), out_img.rows, rowbytes, + /* bottom_up */ true, /* read_end */ true)) { + BOOST_LOG_TRIVIAL(error) << "decode_colored_png: corrupt or truncated PNG data"; + return false; } - png_read_end(dsc.png, dsc.info); png_destroy_read_struct(&dsc.png, &dsc.info, NULL); return true; diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index d2913db55e..07d20cfb8b 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -4452,8 +4452,17 @@ std::vector PresetBundle::get_filament_presets_for_machine(const std:: return compatible; } +int PresetBundle::get_filament_variant_index(const DynamicPrintConfig &filament_config, const DynamicPrintConfig &printer_config, + int extruder_id, NozzleVolumeType nozzle_volume_type) +{ + const auto *extruder_types = printer_config.option("extruder_type"); + const ExtruderType extruder_type = extruder_types && !extruder_types->empty() ? ExtruderType(extruder_types->get_at(extruder_id)) : etDirectDrive; + return std::max(0, filament_config.get_index_for_extruder(1, "", extruder_type, nozzle_volume_type, "filament_extruder_variant")); +} + bool PresetBundle::check_filament_temp_equation_by_printer_type_and_nozzle_for_mas_tray( - const std::string &printer_type, std::string& nozzle_diameter_str, std::string &setting_id, std::string &tag_uid, std::string &nozzle_temp_min, std::string &nozzle_temp_max, std::string& preset_setting_id) + const std::string &printer_type, std::string& nozzle_diameter_str, std::string &setting_id, std::string &tag_uid, std::string &nozzle_temp_min, std::string &nozzle_temp_max, std::string& preset_setting_id, + int extruder_id, NozzleVolumeType nozzle_volume_type) { bool is_equation = true; @@ -4476,13 +4485,15 @@ bool PresetBundle::check_filament_temp_equation_by_printer_type_and_nozzle_for_m // Compare only once if (!compared) { compared = true; + const Preset *printer = printers.find_preset(printer_str); + const int variant_index = printer ? get_filament_variant_index(preset->config, printer->config, extruder_id, nozzle_volume_type) : 0; bool min_temp_equation = false, max_temp_equation = false; int min_nozzle_temp = std::stoi(nozzle_temp_min); int max_nozzle_temp = std::stoi(nozzle_temp_max); ConfigOption *opt_min = const_cast(preset)->config.option("nozzle_temperature_range_low"); if (opt_min) { ConfigOptionInts *opt_min_ints = dynamic_cast(opt_min); - min_nozzle_temp = opt_min_ints->get_at(0); + min_nozzle_temp = opt_min_ints->get_at(variant_index); if (std::to_string(min_nozzle_temp) == nozzle_temp_min) min_temp_equation = true; else { @@ -4493,7 +4504,7 @@ bool PresetBundle::check_filament_temp_equation_by_printer_type_and_nozzle_for_m ConfigOption *opt_max = const_cast(preset)->config.option("nozzle_temperature_range_high"); if (opt_max) { ConfigOptionInts *opt_max_ints = dynamic_cast(opt_max); - max_nozzle_temp = opt_max_ints->get_at(0); + max_nozzle_temp = opt_max_ints->get_at(variant_index); if (std::to_string(max_nozzle_temp) == nozzle_temp_max) max_temp_equation = true; else { diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 0db1f6d9b1..e09ce9dcb5 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -407,13 +407,22 @@ public: std::vector get_filament_presets_for_machine(const std::string &printer_type, const std::string &nozzle_diameter_str, bool include_user_presets); + // Orca: the variant index of a filament preset's per-variant options on extruder extruder_id of a printer + // preset, with the nozzle volume type the machine reports; 0 when the filament has no such variant. + static int get_filament_variant_index(const DynamicPrintConfig &filament_config, + const DynamicPrintConfig &printer_config, + int extruder_id, + NozzleVolumeType nozzle_volume_type); + // extruder_id and nozzle_volume_type identify the tray's nozzle, whose variant the temperature range is compared for. bool check_filament_temp_equation_by_printer_type_and_nozzle_for_mas_tray(const std::string &printer_type, std::string & nozzle_diameter_str, std::string & setting_id, std::string & tag_uid, std::string & nozzle_temp_min, std::string & nozzle_temp_max, - std::string & preset_setting_id); + std::string & preset_setting_id, + int extruder_id, + NozzleVolumeType nozzle_volume_type); Preset * get_similar_printer_preset(std::string printer_model, std::string printer_variant); PresetCollection prints; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index be4e217d5c..5f5592cfce 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -4329,9 +4329,9 @@ bool Print::is_dynamic_group_reorder() const return true; } -int Print::get_filament_config_indx(int filament_id, int layer_id) +int Print::get_filament_config_indx(int filament_id, int layer_id, bool use_cache) { - return get_config_index(filament_id, layer_id, m_config.filament_extruder_variant.values, m_filament_self_index, m_filament_index_map); + return get_config_index(filament_id, layer_id, m_config.filament_extruder_variant.values, m_filament_self_index, use_cache ? &m_filament_index_map : nullptr); } void Print::update_filament_self_index_cache() @@ -4374,7 +4374,7 @@ int Print::get_nozzle_config_index(int filament_id, int layer_id) return get_config_index(filament_id, layer_id, m_default_region_config.print_extruder_variant.values, m_default_region_config.print_extruder_id.values, m_nozzle_index_map); } -int Print::get_config_index(int filament_id, int layer_id, const std::vector &variant_list, const std::vector& self_index_list, FilamentIndexMap &index_map) +int Print::get_config_index(int filament_id, int layer_id, const std::vector &variant_list, const std::vector& self_index_list, FilamentIndexMap *index_map) { auto group_result = get_layered_nozzle_group_result(); // Orca: defensive — when no grouping producer has published a result yet, fall back to the @@ -4385,7 +4385,8 @@ int Print::get_config_index(int filament_id, int layer_id, const std::vectorextruder_id)); NozzleVolumeType nozzle_volume_type = nozzle_info->volume_type; + if (!index_map) + return get_config_index_base(nozzle_volume_type, extruder_type, filament_id + 1, variant_list, self_index_list); FilamentIndexKey key{filament_id, extruder_type, nozzle_volume_type}; - auto iter = index_map.find(key); - if (iter == index_map.end()) { + auto iter = index_map->find(key); + if (iter == index_map->end()) { int index = get_config_index_base(nozzle_volume_type, extruder_type, filament_id + 1, variant_list, self_index_list); - index_map[key] = index; + (*index_map)[key] = index; return index; } else { - return index_map[key]; + return iter->second; } } diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index dc4323e537..f185962b1e 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -1222,7 +1222,9 @@ public: // Post-slicing config-slot resolvers: map a (filament, layer) pair to the index of its // per-(extruder x volume type) column in the expanded variant arrays, cached by grouping context. - int get_filament_config_indx(int filament_id, int layer_id); + // Orca: without use_cache, the filament resolver leaves the cache alone, for the G-code export + // pipeline's cooling stage, which runs concurrently with the generator stage filling it. + int get_filament_config_indx(int filament_id, int layer_id, bool use_cache = true); int get_nozzle_config_index(int filament_id, int layer_id); // Orca: Implement prusa's filament shrink compensation approach @@ -1282,7 +1284,7 @@ protected: }; using FilamentIndexMap = std::unordered_map; using PrintIndexMap = std::unordered_map; - int get_config_index(int filament_id, int layer_id, const std::vector &variant_list, const std::vector& self_index_list, FilamentIndexMap &index_map); + int get_config_index(int filament_id, int layer_id, const std::vector &variant_list, const std::vector& self_index_list, FilamentIndexMap *index_map); int get_config_index(int filament_id, int layer_id, const std::vector &variant_list, const std::vector& self_index_list, PrintIndexMap &index_map); // Invalidates the step, and its depending steps in Print. diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 94220e31fd..d4d0c90272 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -9516,6 +9516,16 @@ std::set filament_options_with_variant = { "adaptive_pressure_advance_model", "adaptive_pressure_advance_overhangs", "adaptive_pressure_advance_bridges", + // Orca: cooling fans, multi-tool ramming and recommended nozzle temperature range + "fan_min_speed", + "fan_max_speed", + "additional_cooling_fan_speed", + "filament_minimal_purge_on_wipe_tower", + "filament_multitool_ramming", + "filament_multitool_ramming_volume", + "filament_multitool_ramming_flow", + "nozzle_temperature_range_low", + "nozzle_temperature_range_high", "activate_air_filtration", "activate_air_filtration_during_print", "activate_air_filtration_on_completion", @@ -11958,6 +11968,21 @@ PRINT_CONFIG_CACHE_INITIALIZE(( SLAMaterialConfig, SLAPrintConfig, SLAPrintObjectConfig, SLAPrinterConfig, SLAFullPrintConfig)) static int print_config_static_initialized = print_config_static_initializer(); +// The same set() calls ConfigBase::apply_only() makes, without looking every key up by name. Out of line so the +// option list is expanded for this once, not in every file that includes PrintConfig.hpp. +#define PRINT_CONFIG_APPLY_TO_DEFINITION(r, data, CLASS_NAME) \ + bool CLASS_NAME::apply_to(ConfigBase &target) const \ + { \ + auto *dst = dynamic_cast(&target); \ + if (dst == nullptr) \ + return false; \ + visit_option_pairs(*dst, *this, [](const char*, ConfigOption &a, const ConfigOption &b) { a.set(&b); return true; }); \ + return true; \ + } +BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_APPLY_TO_DEFINITION, _, (PrintObjectConfig)(PrintRegionConfig)(MachineEnvelopeConfig)(GCodeConfig) + (SLAMaterialConfig)(SLAPrintConfig)(SLAPrintObjectConfig)(SLAPrinterConfig)) +#undef PRINT_CONFIG_APPLY_TO_DEFINITION + //BBS: remove unused command currently CLIActionsConfigDef::CLIActionsConfigDef() { diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 1e91f2e451..bbd92079c0 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1033,9 +1033,10 @@ public: \ #define PRINT_CONFIG_CLASS_ELEMENT_DEFINITION(r, data, elem) BOOST_PP_TUPLE_ELEM(0, elem) BOOST_PP_TUPLE_ELEM(1, elem); #define PRINT_CONFIG_CLASS_ELEMENT_VISIT(r, data, elem) if (! f(BOOST_PP_STRINGIZE(BOOST_PP_TUPLE_ELEM(1, elem)), this->BOOST_PP_TUPLE_ELEM(1, elem), rhs.BOOST_PP_TUPLE_ELEM(1, elem))) return; +#define PRINT_CONFIG_CLASS_ELEMENT_VISIT_SELF(r, data, elem) if (! f(BOOST_PP_STRINGIZE(BOOST_PP_TUPLE_ELEM(1, elem)), self.BOOST_PP_TUPLE_ELEM(1, elem), rhs.BOOST_PP_TUPLE_ELEM(1, elem))) return; // Each option list is expanded into the members and again into for_each_option_pair(), which calls // f(key, this->option, rhs.option) in declaration order and stops when f returns false. hash(), -// operator==, operator< and initialize() iterate the options through that visitor. +// operator==, operator<, initialize() and apply_to() iterate the options through that visitor. #define PRINT_CONFIG_CLASS_COMMON_BODY(CLASS_NAME) \ size_t hash() const throw() \ { \ @@ -1067,11 +1068,16 @@ class CLASS_NAME : public StaticPrintConfig { \ STATIC_PRINT_CONFIG_CACHE(CLASS_NAME) \ public: \ BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_DEFINITION, _, PARAMETER_DEFINITION_SEQ) \ - template void for_each_option_pair(const CLASS_NAME &rhs, F &&f) const \ - { \ - BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_VISIT, _, PARAMETER_DEFINITION_SEQ) \ - } \ + template void for_each_option_pair(const CLASS_NAME &rhs, F &&f) const { visit_option_pairs(*this, rhs, f); } \ + /* Defined in PrintConfig.cpp. */ \ + bool apply_to(ConfigBase &target) const override; \ PRINT_CONFIG_CLASS_COMMON_BODY(CLASS_NAME) \ +private: \ + /* The one expansion of the option list, for a const self and for apply_to()'s mutable target. */ \ + template static void visit_option_pairs(Self &self, const CLASS_NAME &rhs, F &&f) \ + { \ + BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_VISIT_SELF, _, PARAMETER_DEFINITION_SEQ) \ + } \ }; #define PRINT_CONFIG_CLASS_DERIVED_CLASS_LIST_ITEM(r, data, i, elem) BOOST_PP_COMMA_IF(i) public elem @@ -1092,6 +1098,8 @@ class CLASS_NAME : PRINT_CONFIG_CLASS_DERIVED_CLASS_LIST(CLASSES_PARENTS_TUPLE) public: \ PARAMETER_DEFINITION \ template void for_each_option_pair(const CLASS_NAME &rhs, F &&f) const { PARAMETER_VISIT } \ + /* Its parents each apply themselves to a target member by member, so this one keeps the lookup by name. */ \ + bool apply_to(ConfigBase &/*target*/) const override { return false; } \ size_t hash() const throw() \ { \ size_t seed = 0; \ @@ -2182,6 +2190,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE0( #undef STATIC_PRINT_CONFIG_CACHE_DERIVED #undef PRINT_CONFIG_CLASS_ELEMENT_DEFINITION #undef PRINT_CONFIG_CLASS_ELEMENT_VISIT +#undef PRINT_CONFIG_CLASS_ELEMENT_VISIT_SELF #undef PRINT_CONFIG_CLASS_COMMON_BODY #undef PRINT_CONFIG_CLASS_DEFINE #undef PRINT_CONFIG_CLASS_DERIVED_CLASS_LIST diff --git a/src/libslic3r/TextureBake/TextureBakeDebug.cpp b/src/libslic3r/TextureBake/TextureBakeDebug.cpp new file mode 100644 index 0000000000..9465080e21 --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeDebug.cpp @@ -0,0 +1,219 @@ +#include "TextureBakeDebug.hpp" + +#include +#include +#include + +#include +#include + +namespace Slic3r { + +namespace { + +// Half-edge key, low index first so both sides of an edge form the same one. +inline uint64_t edge_key(int a, int b) +{ + const uint32_t lo = uint32_t(std::min(a, b)), hi = uint32_t(std::max(a, b)); + return (uint64_t(lo) << 32) | uint64_t(hi); +} + +} // namespace + +void bake_stage_topology(const BakeStageMesh &mesh, size_t &open_edges, size_t &non_manifold_edges, + size_t °enerate) +{ + open_edges = non_manifold_edges = degenerate = 0; + + // Sorted half-edges rather than a hash map: same answer, but it is one allocation and a sort + // instead of three million node allocations, which on a stage this size is the whole cost. + std::vector keys; + keys.reserve(mesh.indices.size() * 3); + for (const Vec3i32 &t : mesh.indices) { + if (t[0] == t[1] || t[1] == t[2] || t[0] == t[2]) { + ++degenerate; + continue; // a collapsed face has no edges worth counting + } + const Vec3f &a = mesh.vertices[size_t(t[0])]; + if ((mesh.vertices[size_t(t[1])] - a).cross(mesh.vertices[size_t(t[2])] - a).squaredNorm() <= 0.f) + ++degenerate; // zero area but three distinct corners: still counted as an edge carrier + for (int e = 0; e < 3; ++e) + keys.push_back(edge_key(t[e], t[(e + 1) % 3])); + } + std::sort(keys.begin(), keys.end()); + for (size_t i = 0; i < keys.size();) { + size_t j = i + 1; + while (j < keys.size() && keys[j] == keys[i]) + ++j; + const size_t incident = j - i; + if (incident == 1) + ++open_edges; + else if (incident > 2) + ++non_manifold_edges; + i = j; + } +} + +void BakeStageRecorder::finish(BakeStageSnapshot &s) +{ + s.triangles = s.mesh.indices.size(); + s.vertices = s.mesh.vertices.size(); + if (m_check_topology) + bake_stage_topology(s.mesh, s.open_edges, s.non_manifold_edges, s.degenerate); + s.topology_checked = m_check_topology; + if (s.triangles > m_mesh_cap) { + s.mesh_dropped = true; + s.mesh.vertices.clear(); + s.mesh.vertices.shrink_to_fit(); + s.mesh.indices.clear(); + s.mesh.indices.shrink_to_fit(); + } + m_stages.push_back(std::move(s)); +} + +void BakeStageRecorder::capture(const char *name, const TextureBake::TriSoup &soup, double ms, + const std::string &detail) +{ + if (!m_enabled) + return; + + BakeStageSnapshot s; + s.name = name; + s.detail = detail; + s.ms = ms; + + // The pipeline works on non-indexed soup, so welding here is what turns it back into something + // renderable. The geometry grid, matching to_indexed_triangle_set(), so the debug view shows the + // same sharing the bake's own output would have. + const size_t n = soup.pos.size(); + TextureBake::QuantizedPointMap map(TextureBake::WELD_GRID_GEOMETRY, std::min(n, size_t(1) << 22)); + std::vector id(n); + s.mesh.vertices.reserve(n / 3); + for (size_t i = 0; i < n; ++i) { + id[i] = map.get_or_set(soup.pos[i], int(s.mesh.vertices.size())); + if (map.inserted()) + s.mesh.vertices.push_back(soup.pos[i]); + } + s.mesh.indices.reserve(n / 3); + for (size_t t = 0; t + 2 < n; t += 3) { + // Corners that welded together carry no area; the bake's own conversion drops them too, so + // dropping them here keeps the stage count honest against what would be committed. + if (id[t] == id[t + 1] || id[t + 1] == id[t + 2] || id[t] == id[t + 2]) + continue; + s.mesh.indices.emplace_back(id[t], id[t + 1], id[t + 2]); + } + finish(s); +} + +void BakeStageRecorder::capture(const char *name, const std::vector &vertices, + const std::vector &indices, double ms, + const std::string &detail) +{ + if (!m_enabled) + return; + BakeStageSnapshot s; + s.name = name; + s.detail = detail; + s.ms = ms; + s.mesh.vertices = vertices; + s.mesh.indices = indices; + finish(s); +} + +void BakeStageRecorder::capture_note(const char *name, double ms, const std::string &detail) +{ + if (!m_enabled) + return; + BakeStageSnapshot s; + s.name = name; + s.detail = detail; + s.ms = ms; + s.topology_checked = false; + m_stages.push_back(std::move(s)); +} + +void BakeStageRecorder::rebase(size_t from, const Transform3d *to_local, bool flip_winding) +{ + for (size_t i = from; i < m_stages.size(); ++i) { + BakeStageMesh &m = m_stages[i].mesh; + if (to_local != nullptr) + for (Vec3f &v : m.vertices) + v = (*to_local * v.cast()).cast(); + if (flip_winding) + for (Vec3i32 &t : m.indices) + std::swap(t[1], t[2]); + } +} + +double BakeStageRecorder::total_ms() const +{ + double sum = 0.0; + for (const BakeStageSnapshot &s : m_stages) + sum += s.ms; + return sum; +} + +size_t dump_bake_stages(const std::vector &stages, const std::string &dir) +{ + boost::system::error_code ec; + boost::filesystem::create_directories(dir, ec); + if (ec) { + BOOST_LOG_TRIVIAL(error) << "BakeStageRecorder: cannot create " << dir << ": " << ec.message(); + return 0; + } + + size_t written = 0; + for (size_t i = 0; i < stages.size(); ++i) { + const BakeStageSnapshot &s = stages[i]; + if (s.mesh.empty()) + continue; + + // Stage names carry spaces and punctuation; keep the filename to what every shell and viewer + // handles without quoting. + std::string safe; + for (const char c : s.name) + safe += (std::isalnum(static_cast(c)) != 0) ? c : '_'; + + char path[1024]; + std::snprintf(path, sizeof(path), "%s/%02zu_%s.obj", dir.c_str(), i, safe.c_str()); + std::FILE *f = std::fopen(path, "wb"); + if (f == nullptr) { + BOOST_LOG_TRIVIAL(error) << "BakeStageRecorder: cannot write " << path; + continue; + } + std::fprintf(f, "# texture bake stage %zu: %s\n", i, s.name.c_str()); + if (!s.detail.empty()) + std::fprintf(f, "# %s\n", s.detail.c_str()); + std::fprintf(f, "# %zu triangles, %.2f ms\n", s.triangles, s.ms); + for (const Vec3f &v : s.mesh.vertices) + std::fprintf(f, "v %.6f %.6f %.6f\n", double(v.x()), double(v.y()), double(v.z())); + for (const Vec3i32 &t : s.mesh.indices) // OBJ indices are 1-based + std::fprintf(f, "f %d %d %d\n", t[0] + 1, t[1] + 1, t[2] + 1); + std::fclose(f); + ++written; + } + + char summary[1024]; + std::snprintf(summary, sizeof(summary), "%s/stages.txt", dir.c_str()); + if (std::FILE *f = std::fopen(summary, "wb"); f != nullptr) { + std::fprintf(f, "%-3s %-24s %10s %12s %10s %8s %8s %8s %s\n", "#", "stage", "ms", "triangles", + "vertices", "open", "nonman", "degen", "detail"); + double total = 0.0; + for (size_t i = 0; i < stages.size(); ++i) { + const BakeStageSnapshot &s = stages[i]; + total += s.ms; + std::fprintf(f, "%-3zu %-24s %10.2f %12zu %10zu ", i, s.name.c_str(), s.ms, s.triangles, + s.vertices); + if (s.topology_checked) + std::fprintf(f, "%8zu %8zu %8zu", s.open_edges, s.non_manifold_edges, s.degenerate); + else + std::fprintf(f, "%8s %8s %8s", "-", "-", "-"); + std::fprintf(f, " %s%s\n", s.detail.c_str(), s.mesh_dropped ? " [mesh over cap, not written]" : ""); + } + std::fprintf(f, "\ntotal %.2f ms across %zu stages\n", total, stages.size()); + std::fclose(f); + } + return written; +} + +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeDebug.hpp b/src/libslic3r/TextureBake/TextureBakeDebug.hpp new file mode 100644 index 0000000000..5c6d987e44 --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeDebug.hpp @@ -0,0 +1,123 @@ +#pragma once + +// Step-by-step capture of a bake. +// +// A bake is a chain of stages that each rewrite the whole mesh, so when the result looks wrong the +// only useful question is which stage made it wrong. This records the geometry, the wall time and the +// topology after every stage, which is what the gizmo's debug view steps through and what the +// benchmark's --dump-stages writes out. +// +// Deliberately independent of TriangleMesh: a stage is held as a plain vertex/index pair, which is +// layout-compatible with indexed_triangle_set's own members (stl_vertex is Vec3f, +// stl_triangle_vertex_indices is Vec3i32), so the GUI assigns rather than converts and the standalone +// benchmark does not have to link admesh to use this. +// +// Recording is off unless enable(true) was called, and every capture site is a null-pointer check, so +// a normal bake pays nothing for this being here. + +#include +#include +#include +#include + +#include "TextureBakeIndex.hpp" + +namespace Slic3r { + +struct BakeStageMesh +{ + std::vector vertices; + std::vector indices; + + bool empty() const { return indices.empty(); } + size_t triangle_count() const { return indices.size(); } +}; + +struct BakeStageSnapshot +{ + std::string name; // "remesh", "subdivide", ... + std::string detail; // whatever the stage has to say: collapse counts, rejected moves, ... + BakeStageMesh mesh; // empty when the stage was over the memory cap - see mesh_dropped + + double ms = 0.0; + size_t triangles = 0; + size_t vertices = 0; + + // Filled only when the recorder was asked to check topology: it is a sort over every half-edge, + // which on a multi-million triangle stage costs more than the stage being measured. + size_t open_edges = 0; + size_t non_manifold_edges = 0; + size_t degenerate = 0; + bool topology_checked = false; + + // The geometry was dropped to stay inside the memory cap; every count above is still real. + bool mesh_dropped = false; +}; + +// Edge and area defects of a captured stage. Split out so a caller can run it on its own. +void bake_stage_topology(const BakeStageMesh &mesh, size_t &open_edges, size_t &non_manifold_edges, + size_t °enerate); + +// Writes `/NN_name.obj` for every stage that still holds geometry, plus a `stages.txt` summary. +// Returns how many meshes were written. Existing files with the same names are overwritten. +// +// A free function rather than a recorder method because by the time anyone wants the files the +// recorder is usually gone and only the stages survive - that is how the gizmo holds them. +size_t dump_bake_stages(const std::vector &stages, const std::string &dir); + +class BakeStageRecorder +{ +public: + // Nothing is recorded until this is on. + void enable(bool on) { m_enabled = on; } + bool enabled() const { return m_enabled; } + + // The edge scan is optional because it is O(n log n) over every half-edge, and a debug run that + // only wants to see the geometry should not pay for it on every stage. + void set_check_topology(bool on) { m_check_topology = on; } + bool check_topology() const { return m_check_topology; } + + // Stages above this keep their counts but not their geometry. A debug run holds every stage at + // once, and a 4 M triangle stage is about 150 MB on its own, so without a cap stepping through a + // fine bake would need more memory than the bake did. + void set_mesh_cap(size_t triangles) { m_mesh_cap = triangles; } + size_t mesh_cap() const { return m_mesh_cap; } + + // `ms` is passed in rather than measured here: the caller is already timing the stage, and the + // capture itself (a weld, a copy, possibly an edge scan) must not land inside that measurement. + void capture(const char *name, const TextureBake::TriSoup &soup, double ms, + const std::string &detail = {}); + void capture(const char *name, const std::vector &vertices, + const std::vector &indices, double ms, const std::string &detail = {}); + // For a stage that changed nothing a caller can still show, e.g. a skipped remesh. + void capture_note(const char *name, double ms, const std::string &detail); + + // Index of the next stage to be recorded. Paired with rebase() to fix up a range afterwards. + size_t mark() const { return m_stages.size(); } + + // Brings stages [from, end) into the caller's own space and winding. The bake runs in world + // millimetres and, for a mirrored placement, against a reversed winding; the debug view draws in + // the volume's local frame, so a captured range has to be brought back the same way the bake's + // own result is. `to_local` may be null for no transform. + void rebase(size_t from, const Transform3d *to_local, bool flip_winding); + + const std::vector &stages() const { return m_stages; } + std::vector take() { return std::move(m_stages); } + void clear() { m_stages.clear(); } + bool empty() const { return m_stages.empty(); } + + // Total recorded wall time, which is the bake's own time minus whatever it does outside a stage. + double total_ms() const; + + size_t dump_obj(const std::string &dir) const { return dump_bake_stages(m_stages, dir); } + +private: + void finish(BakeStageSnapshot &s); + + std::vector m_stages; + bool m_enabled = false; + bool m_check_topology = true; + size_t m_mesh_cap = 4'000'000; +}; + +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeDecimate.cpp b/src/libslic3r/TextureBake/TextureBakeDecimate.cpp new file mode 100644 index 0000000000..ee335cff3c --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeDecimate.cpp @@ -0,0 +1,562 @@ +#include "TextureBakeDecimate.hpp" + +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace Slic3r { +namespace TextureBake { + +namespace { + +// Symmetric 4x4 quadric, as its 10 upper-triangle values. +struct Quadric +{ + std::array q{}; + + void add_plane(double a, double b, double c, double d) + { + q[0] += a * a; q[1] += a * b; q[2] += a * c; q[3] += a * d; + q[4] += b * b; q[5] += b * c; q[6] += b * d; + q[7] += c * c; q[8] += c * d; + q[9] += d * d; + } + void operator+=(const Quadric &o) + { + for (int i = 0; i < 10; ++i) + q[size_t(i)] += o.q[size_t(i)]; + } + double eval(double x, double y, double z) const + { + return q[0] * x * x + 2 * q[1] * x * y + 2 * q[2] * x * z + 2 * q[3] * x + + q[4] * y * y + 2 * q[5] * y * z + 2 * q[6] * y + + q[7] * z * z + 2 * q[8] * z + q[9]; + } +}; + +double eval_sum(const std::vector &qs, int v1, int v2, const Vec3d &p) +{ + return qs[size_t(v1)].eval(p.x(), p.y(), p.z()) + qs[size_t(v2)].eval(p.x(), p.y(), p.z()); +} + +// The position minimising the summed quadric, if the system is well conditioned enough to trust. +bool solve_q(const std::vector &qs, int v1, int v2, Vec3d &out) +{ + const auto &A = qs[size_t(v1)].q; + const auto &B = qs[size_t(v2)].q; + const double a00 = A[0] + B[0], a01 = A[1] + B[1], a02 = A[2] + B[2]; + const double a11 = A[4] + B[4], a12 = A[5] + B[5], a22 = A[7] + B[7]; + const double b0 = -(A[3] + B[3]), b1 = -(A[6] + B[6]), b2 = -(A[8] + B[8]); + + const double det = a00 * (a11 * a22 - a12 * a12) - a01 * (a01 * a22 - a12 * a02) + + a02 * (a01 * a12 - a11 * a02); + const double max_el = std::max({ std::abs(a00), std::abs(a01), std::abs(a02), std::abs(a11), + std::abs(a12), std::abs(a22) }); + // Scaled with the matrix, so it means the same at any model scale. + const double threshold = max_el * max_el * max_el * 1e-10; + if (std::abs(det) < std::max(threshold, 1e-30)) + return false; + + const double inv = 1.0 / det; + out.x() = inv * (b0 * (a11 * a22 - a12 * a12) - a01 * (b1 * a22 - a12 * b2) + a02 * (b1 * a12 - a11 * b2)); + out.y() = inv * (a00 * (b1 * a22 - a12 * b2) - b0 * (a01 * a22 - a12 * a02) + a02 * (a01 * b2 - b1 * a02)); + out.z() = inv * (a00 * (a11 * b2 - b1 * a12) - a01 * (a01 * b2 - b1 * a02) + b0 * (a01 * a12 - a11 * a02)); + return true; +} + +Vec3d face_normal_unit(const std::vector &pos, int a, int b, int c) +{ + const Vec3d n = (pos[size_t(b)] - pos[size_t(a)]).cross(pos[size_t(c)] - pos[size_t(a)]); + const double len = n.norm(); + return (len > 0.0) ? Vec3d(n / len) : Vec3d::Zero(); +} + +// Versions are captured at push time; a mismatch on pop means a later collapse invalidated the entry. +// Lazy deletion, far cheaper than removing entries eagerly - but it means the heap accumulates stale +// duplicates, so its size has to be reserved up front and it is compacted once the dead entries +// dominate (see maybe_compact below). +// +// The collapse target is not stored. A matching version stamp means neither endpoint's quadric nor its +// position has changed since the push, so the target recomputes to exactly the same value on pop - and +// the entry drops from 40 bytes to 24. Sifting is most of this stage's time, and it is memory traffic. +struct HeapEntry +{ + double cost; + int v1, v2; + uint32_t ver1, ver2; + bool operator>(const HeapEntry &o) const { return cost > o.cost; } +}; + +} // namespace + +DecimateResult decimate(const TriSoup &geometry, size_t target_triangles, bool harvest_flat, + double harvest_tol, const std::vector &locked_faces, + const DecimateProgressFn &on_progress, const std::vector &face_color) +{ + DecimateResult result; + const size_t n = geometry.pos.size(); + if (n < 3) { + result.geometry = geometry; + return result; + } + + // The finest grid. Anything coarser fuses distinct fine-feature vertices on a displaced mesh, + // leaving it non-manifold before decimation starts and producing open edges afterwards. + QuantizedPointMap vert_map(WELD_GRID_DECIMATION, std::min(n, size_t(1) << 22)); + std::vector pos; + std::vector remap(n); + for (size_t i = 0; i < n; ++i) { + const int idx = vert_map.get_or_set(geometry.pos[i], int(pos.size())); + if (vert_map.inserted()) + pos.push_back(geometry.pos[i].cast()); + remap[i] = idx; + } + const size_t vert_count = pos.size(); + const size_t face_count = n / 3; + std::vector faces(face_count * 3); + for (size_t i = 0; i < n; ++i) + faces[i] = remap[i]; + + if (face_count <= target_triangles && !harvest_flat) { + result.geometry = geometry; + return result; + } + + // An edge with a locked endpoint never reaches the heap. + std::vector locked_vert; + size_t locked_face_count = 0; + if (!locked_faces.empty()) { + locked_vert.assign(vert_count, 0); + for (size_t f = 0; f < face_count && f < locked_faces.size(); ++f) { + if (!locked_faces[f]) + continue; + ++locked_face_count; + for (int k = 0; k < 3; ++k) + locked_vert[size_t(faces[f * 3 + size_t(k)])] = 1; + } + } + // With the locked faces alone at the target, chasing it would grind the free region to its guard + // limit for nothing - harvest only, and say so. + const bool locked_over_budget = + !locked_vert.empty() && face_count > target_triangles && locked_face_count >= target_triangles; + result.locked_over_budget = locked_over_budget; + if (locked_over_budget && !harvest_flat) { + result.geometry = geometry; + return result; + } + + std::vector quadrics(vert_count); + { + // The plane per face is independent; accumulating it into the three incident vertices is not, + // so only the first half is parallel. + std::vector planes(face_count, Vec4d::Zero()); + tbb::parallel_for(tbb::blocked_range(0, face_count), + [&](const tbb::blocked_range &range) { + for (size_t f = range.begin(); f < range.end(); ++f) { + const int a = faces[f * 3], b = faces[f * 3 + 1], c = faces[f * 3 + 2]; + if (a < 0) + continue; + const Vec3d nrm = face_normal_unit(pos, a, b, c); + if (nrm.isZero()) + continue; + planes[f] = Vec4d(nrm.x(), nrm.y(), nrm.z(), -nrm.dot(pos[size_t(a)])); + } + }); + for (size_t f = 0; f < face_count; ++f) { + const Vec4d &pl = planes[f]; + if (pl.head<3>().isZero()) + continue; + for (int k = 0; k < 3; ++k) + quadrics[size_t(faces[f * 3 + size_t(k)])].add_plane(pl.x(), pl.y(), pl.z(), pl.w()); + } + } + + // Two penalty planes per endpoint on a sharp interior edge, each perpendicular to one adjacent + // face and containing the edge, constraining the vertex to the crease line. + { + struct EdgeRec { int va, vb, f0, f1; uint8_t count; }; + std::vector edges; + QuantizedPointMap edge_idx(1.0, std::min(face_count * 3, size_t(1) << 22)); + for (size_t f = 0; f < face_count; ++f) { + if (faces[f * 3] < 0) + continue; + for (int e = 0; e < 3; ++e) { + const int va = faces[f * 3 + size_t(e)]; + const int vb = faces[f * 3 + size_t((e + 1) % 3)]; + const int lo = std::min(va, vb), hi = std::max(va, vb); + const int ei = edge_idx.get_or_set_key(lo, hi, 0, int(edges.size())); + if (edge_idx.inserted()) + edges.push_back({ lo, hi, int(f), -1, 1 }); + else if (edges[size_t(ei)].count == 1) { + edges[size_t(ei)].f1 = int(f); + edges[size_t(ei)].count = 2; + } else + // Non-manifold; never feeds a crease. + edges[size_t(ei)].count = 3; + } + } + + const double sqrt_w = std::sqrt(DECIMATE_CREASE_WEIGHT); + for (const EdgeRec &er : edges) { + if (er.count != 2) + continue; // boundary or non-manifold + const Vec3d n0 = face_normal_unit(pos, faces[size_t(er.f0) * 3], faces[size_t(er.f0) * 3 + 1], + faces[size_t(er.f0) * 3 + 2]); + const Vec3d n1 = face_normal_unit(pos, faces[size_t(er.f1) * 3], faces[size_t(er.f1) * 3 + 1], + faces[size_t(er.f1) * 3 + 2]); + const bool color_edge = face_color.size() > std::max(size_t(er.f0), size_t(er.f1)) && + face_color[size_t(er.f0)] != face_color[size_t(er.f1)]; + if (!color_edge && n0.dot(n1) >= DECIMATE_CREASE_COS) + continue; // smooth enough to be no crease, and no colour changes across it + + const Vec3d e = pos[size_t(er.vb)] - pos[size_t(er.va)]; + const double elen = e.norm(); + if (elen <= 0.0) + continue; + const Vec3d ed = e / elen; + for (const Vec3d &fn : { n0, n1 }) { + Vec3d pn = fn.cross(ed); + const double plen = pn.norm(); + if (plen < 1e-10) + continue; // edge parallel to the face normal + pn /= plen; + const double d = -pn.dot(pos[size_t(er.va)]); + // sqrt(w) on the inputs gives w times the accumulated products. + for (const int v : { er.va, er.vb }) + quadrics[size_t(v)].add_plane(pn.x() * sqrt_w, pn.y() * sqrt_w, pn.z() * sqrt_w, + d * sqrt_w); + } + } + } + + // Vertex-face incidence as intrusive linked lists of slots over flat arrays, with each list's length. + const size_t S = face_count * 3; + std::vector vf_head(vert_count, -1), vf_count(vert_count, 0), slot_face(S), slot_vert(S), + slot_next(S, -1), slot_prev(S, -1), face_slot(S, -1); + for (size_t f = 0; f < face_count; ++f) + for (int k = 0; k < 3; ++k) { + const int s = int(f) * 3 + k; + const int v = faces[size_t(s)]; + slot_face[size_t(s)] = int(f); + slot_vert[size_t(s)] = v; + slot_next[size_t(s)] = vf_head[size_t(v)]; + slot_prev[size_t(s)] = -1; + if (vf_head[size_t(v)] >= 0) + slot_prev[size_t(vf_head[size_t(v)])] = s; + vf_head[size_t(v)] = s; + face_slot[size_t(s)] = s; + ++vf_count[size_t(v)]; + } + const auto unlink_slot = [&](int s) { + const int p = slot_prev[size_t(s)], nx = slot_next[size_t(s)], v = slot_vert[size_t(s)]; + if (p >= 0) slot_next[size_t(p)] = nx; + else vf_head[size_t(v)] = nx; + if (nx >= 0) slot_prev[size_t(nx)] = p; + --vf_count[size_t(v)]; + }; + const auto move_slot = [&](int s, int nv) { + unlink_slot(s); + slot_next[size_t(s)] = vf_head[size_t(nv)]; + slot_prev[size_t(s)] = -1; + if (vf_head[size_t(nv)] >= 0) + slot_prev[size_t(vf_head[size_t(nv)])] = s; + vf_head[size_t(nv)] = s; + slot_vert[size_t(s)] = nv; + ++vf_count[size_t(nv)]; + }; + + std::vector active(vert_count, 1); + std::vector version(vert_count, 0); + std::vector nb_stamp(vert_count, 0), lk_stamp(vert_count, 0); + uint32_t epoch = 1, lk_epoch = 1; + size_t active_faces = face_count; + + // A plain vector driven by the heap algorithms, so the capacity can be reserved. Lazy deletion + // means roughly one entry per edge plus one per re-push after each collapse; the reserve below is + // sized from the edge count and simply grows if a mesh needs more. + std::vector heap; + heap.reserve(std::min(face_count * 3, size_t(1) << 24)); + const auto heap_push = [&](HeapEntry e) { + heap.push_back(e); + std::push_heap(heap.begin(), heap.end(), std::greater()); + }; + const auto heap_pop = [&]() { + std::pop_heap(heap.begin(), heap.end(), std::greater()); + const HeapEntry e = heap.back(); + heap.pop_back(); + return e; + }; + size_t pops = 0, stale_pops = 0, compactions = 0; + + // An entry is stale once either endpoint has been removed or moved by a later collapse. + const auto is_stale = [&](const HeapEntry &e) { + return !active[size_t(e.v1)] || !active[size_t(e.v2)] || version[size_t(e.v1)] != e.ver1 || + version[size_t(e.v2)] != e.ver2; + }; + // Measured on a 2.4 M -> 750 k run, 82% of pops were stale: every collapse re-pushes the survivor's + // edges and orphans the old ones, so the heap grows to several times the live edge set and every + // sift walks that much further through memory. Dropping the dead entries and re-heapifying once + // they dominate costs one linear pass, amortised against the growth that triggered it. + // + // Keyed to the live face count rather than to the heap's own size: lazy popping keeps the heap from + // ever doubling, but the live edge set (about 1.5 per face) shrinks as decimation proceeds, so by the + // end the heap is several times what is still collapsible. Compact once it passes twice that. + const auto maybe_compact = [&]() { + if (heap.size() < std::max(size_t(1) << 16, active_faces * 3)) + return; + heap.erase(std::remove_if(heap.begin(), heap.end(), is_stale), heap.end()); + std::make_heap(heap.begin(), heap.end(), std::greater()); + ++compactions; + }; + + // Where an edge collapses to. Also re-run on pop instead of stored - see HeapEntry. + const auto collapse_target = [&](int v1, int v2) -> Vec3d { + Vec3d p; + if (!solve_q(quadrics, v1, v2, p)) { + const Vec3d mid = (pos[size_t(v1)] + pos[size_t(v2)]) * 0.5; + const double e1 = eval_sum(quadrics, v1, v2, pos[size_t(v1)]); + const double e2 = eval_sum(quadrics, v1, v2, pos[size_t(v2)]); + const double em = eval_sum(quadrics, v1, v2, mid); + const double emin = std::min({ e1, e2, em }); + const double etol = emin * 1e-2 + 1e-12; + // The midpoint when the three are near-equal, i.e. flat: it moves adjacent triangles + // least, so fewer normal flips and no stalling on coplanar geometry. + if (em <= emin + etol) p = mid; + else if (e1 <= e2) p = pos[size_t(v1)]; + else p = pos[size_t(v2)]; + } + return p; + }; + const auto push_edge = [&](int v1, int v2) { + const Vec3d p = collapse_target(v1, v2); + // The cost is evaluated at the exact target and the collapse moves to its float rounding - + // the same split as when the rounded target was stored in the entry, so no ordering changes. + // Where quadric costs are all near zero, shorter edges first keeps triangle quality up. + const double len2 = (pos[size_t(v2)] - pos[size_t(v1)]).squaredNorm(); + heap_push({ eval_sum(quadrics, v1, v2, p) + len2 * 1e-8, v1, v2, version[size_t(v1)], + version[size_t(v2)] }); + }; + + { + QuantizedPointMap seed_seen(1.0, std::min(face_count * 3, size_t(1) << 22)); + for (size_t f = 0; f < face_count; ++f) { + if (faces[f * 3] < 0) + continue; + for (int e = 0; e < 3; ++e) { + const int va = faces[f * 3 + size_t(e)]; + const int vb = faces[f * 3 + size_t((e + 1) % 3)]; + if (!locked_vert.empty() && (locked_vert[size_t(va)] || locked_vert[size_t(vb)])) + continue; + seed_seen.get_or_set_key(std::min(va, vb), std::max(va, vb), 0, 1); + if (seed_seen.inserted()) + push_edge(va, vb); + } + } + } + + // 0 means a stale entry, 1 a boundary edge, 2 or more safe. + const auto shared_face_count = [&](int v1, int v2) { + int count = 0; + for (int s = vf_head[size_t(v1)]; s >= 0; s = slot_next[size_t(s)]) { + const int f = slot_face[size_t(s)]; + if (faces[size_t(f) * 3] < 0) + continue; + for (int k = 0; k < 3; ++k) + if (faces[size_t(f) * 3 + size_t(k)] == v2) { + if (++count >= 2) + return 2; + break; + } + } + return count; + }; + + // Safe only when the sole common neighbours of the endpoints are the apexes of the faces the edge + // already shares; any other would pile a third triangle onto an edge after the collapse. + const auto has_link_violation = [&](int v1, int v2, uint32_t ep) { + for (int s = vf_head[size_t(v1)]; s >= 0; s = slot_next[size_t(s)]) { + const int f = slot_face[size_t(s)]; + if (faces[size_t(f) * 3] < 0) + continue; + for (int k = 0; k < 3; ++k) + if (const int x = faces[size_t(f) * 3 + size_t(k)]; x != v1) + lk_stamp[size_t(x)] = ep; + } + int shared = 0; + for (int s = vf_head[size_t(v1)]; s >= 0; s = slot_next[size_t(s)]) { + const int f = slot_face[size_t(s)]; + if (faces[size_t(f) * 3] < 0) + continue; + const int a = faces[size_t(f) * 3], b = faces[size_t(f) * 3 + 1], c = faces[size_t(f) * 3 + 2]; + if (a == v2 || b == v2 || c == v2) { + ++shared; + const int apex = (a != v1 && a != v2) ? a : (b != v1 && b != v2) ? b : c; + lk_stamp[size_t(apex)] = ep + 1; // a legal shared-face apex + } + } + if (shared > 2) + return true; // already non-manifold + for (int s = vf_head[size_t(v2)]; s >= 0; s = slot_next[size_t(s)]) { + const int f = slot_face[size_t(s)]; + if (faces[size_t(f) * 3] < 0) + continue; + for (int k = 0; k < 3; ++k) { + const int x = faces[size_t(f) * 3 + size_t(k)]; + if (x != v2 && x != v1 && lk_stamp[size_t(x)] == ep) + return true; + } + } + return false; + }; + + // Squared-dot, so no square root or division. Faces containing the other endpoint are the ones + // being removed, so they are skipped. + const auto check_flipped = [&](int vc, int vo, const Vec3d &np) { + for (int s = vf_head[size_t(vc)]; s >= 0; s = slot_next[size_t(s)]) { + const size_t f = size_t(slot_face[size_t(s)]); + if (faces[f * 3] < 0) + continue; + const int fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2]; + if (fa == vo || fb == vo || fc == vo) + continue; + const Vec3d oa = pos[size_t(fa)], ob = pos[size_t(fb)], oc = pos[size_t(fc)]; + const Vec3d on = (ob - oa).cross(oc - oa); + const Vec3d na = (fa == vc) ? np : oa; + const Vec3d nb = (fb == vc) ? np : ob; + const Vec3d nc = (fc == vc) ? np : oc; + const Vec3d nn = (nb - na).cross(nc - na); + const double raw = on.dot(nn); + if (raw < 0.0) + return true; + if (raw * raw < DECIMATE_FLIP_DOT * DECIMATE_FLIP_DOT * on.squaredNorm() * nn.squaredNorm()) + return true; + } + return false; + }; + + const size_t init_faces = active_faces; + const size_t to_remove = std::max(1, init_faces > target_triangles + ? init_faces - target_triangles : init_faces); + const double harvest_ceil = harvest_tol * harvest_tol; + bool reached_target = locked_over_budget; + double last_progress = 0.0; + + while (!heap.empty()) { + if (active_faces <= target_triangles) { + if (!harvest_flat) + break; + reached_target = true; + } + + const HeapEntry top = heap_pop(); + ++pops; + if (on_progress) { + // Every 16 k pops as well as whenever the fraction moves: this is the only place a cancel + // is seen, and past the target the fraction stops moving. + const double p = std::min(1.0, double(init_faces - active_faces) / double(to_remove)); + if (p - last_progress > 0.005 || (pops & 0x3fff) == 0) { + last_progress = p; + if (!on_progress(p)) + break; + } + } + // The popped entry is the cheapest left, so exceeding the tolerance ends the run. + if (reached_target && top.cost > harvest_ceil) + break; + + const int v1 = top.v1, v2 = top.v2; + if (is_stale(top)) { + ++stale_pops; + continue; + } + // Ahead of the checks below, which walk the fans (see DECIMATE_MAX_VALENCE). The two shared + // faces, counted in both fans, go. + if (vf_count[size_t(v1)] + vf_count[size_t(v2)] - 4 > DECIMATE_MAX_VALENCE) + continue; + if (shared_face_count(v1, v2) < 2) + continue; + lk_epoch += 2; // +2 so ep and ep+1 cannot collide with the next call + if (has_link_violation(v1, v2, lk_epoch)) + continue; + const Vec3d target = collapse_target(v1, v2).cast().cast(); + if (check_flipped(v1, v2, target) || check_flipped(v2, v1, target)) + continue; + if (!reached_target && top.cost > harvest_ceil) + result.target_cost_detail = true; + + // v1 survives at the new position, v2 goes. + pos[size_t(v1)] = target; + quadrics[size_t(v1)] += quadrics[size_t(v2)]; + ++version[size_t(v1)]; + + for (int s = vf_head[size_t(v2)]; s >= 0;) { + const size_t f = size_t(slot_face[size_t(s)]); + const int s_next = slot_next[size_t(s)]; // read before the list is modified + if (faces[f * 3] >= 0) { + for (int k = 0; k < 3; ++k) + if (faces[f * 3 + size_t(k)] == v2) { + faces[f * 3 + size_t(k)] = v1; + break; + } + const int fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2]; + if (fa == fb || fb == fc || fa == fc) { + for (int k = 0; k < 3; ++k) + if (const int sk = face_slot[f * 3 + size_t(k)]; sk >= 0) { + unlink_slot(sk); + face_slot[f * 3 + size_t(k)] = -1; + } + faces[f * 3] = faces[f * 3 + 1] = faces[f * 3 + 2] = -1; + --active_faces; + } else + move_slot(s, v1); + } + s = s_next; + } + active[size_t(v2)] = 0; + + ++epoch; + for (int sv = vf_head[size_t(v1)]; sv >= 0; sv = slot_next[size_t(sv)]) { + const size_t f = size_t(slot_face[size_t(sv)]); + if (faces[f * 3] < 0) + continue; + for (int k = 0; k < 3; ++k) { + const int nb = faces[f * 3 + size_t(k)]; + if (nb == v1 || nb_stamp[size_t(nb)] == epoch) + continue; + nb_stamp[size_t(nb)] = epoch; + // v1 is never locked - a locked edge never entered the heap. + if (active[size_t(nb)] && (locked_vert.empty() || !locked_vert[size_t(nb)])) + push_edge(v1, nb); + } + } + maybe_compact(); + } + + BOOST_LOG_TRIVIAL(info) << "TextureBake decimate: pops=" << pops << " stale=" << stale_pops + << " compactions=" << compactions << " heap_peak=" << heap.capacity() + << " faces=" << active_faces; + + // Rebuild from the surviving faces, with per-face normals. + TriSoup &out = result.geometry; + for (size_t f = 0; f < face_count; ++f) { + if (faces[f * 3] < 0) + continue; + const Vec3f a = pos[size_t(faces[f * 3])].cast(); + const Vec3f b = pos[size_t(faces[f * 3 + 1])].cast(); + const Vec3f c = pos[size_t(faces[f * 3 + 2])].cast(); + const Vec3f nrm = (b - a).cross(c - a).normalized(); + out.pos.insert(out.pos.end(), { a, b, c }); + out.nrm.insert(out.nrm.end(), { nrm, nrm, nrm }); + } + return result; +} + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeDecimate.hpp b/src/libslic3r/TextureBake/TextureBakeDecimate.hpp new file mode 100644 index 0000000000..5579778050 --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeDecimate.hpp @@ -0,0 +1,68 @@ +#pragma once + +// Quadric error metric decimation (Garland & Heckbert), with two additions that matter on a +// displaced mesh. +// +// Crease quadrics: an interior edge sharper than the threshold gets penalty planes at both endpoints, +// perpendicular to each adjacent face and through the edge, weighted so such edges collapse last or +// not at all. A texture's hard step keeps its geometry while the flat ground around it reduces. +// +// Flat-face harvesting: the loop keeps going past the triangle target while each collapse's error +// stays under an absolute bound, so flat faces that cost nothing to remove are not left behind. + +#include +#include +#include + +#include "TextureBakeIndex.hpp" + +namespace Slic3r { +namespace TextureBake { + +// Reject a collapse deviating more than about 78 degrees from the old face normal. +static constexpr double DECIMATE_FLIP_DOT = 0.2; +// Edges sharper than 60 degrees are treated as creases. +static constexpr double DECIMATE_CREASE_COS = 0.5; +// Quadric penalty weight for a crease plane. +static constexpr double DECIMATE_CREASE_WEIGHT = 1e4; +// Most faces a collapse may leave around its surviving vertex. Uncapped, a pinned vertex on flat +// ground grows a fan of thousands of slivers, and validating each of its edges walks the whole fan, +// which turns a decimation of a second or two into minutes. At 64 the triangle count moves by about +// 1% at most, and by far less on large bakes. +static constexpr int DECIMATE_MAX_VALENCE = 64; + +// Upper bound in mm on the deviation a harvested collapse may introduce; the real one is smaller, +// since the cost sums squared distances over all incident faces. +// +// Absolute, not relative to the cost at which the target was crossed. A relative band fails in the +// case with the most to shed: when the target is reached with a large flat surplus left, the crossing +// cost is essentially zero, so the band is too and nothing is harvested. +static constexpr double DECIMATE_DEFAULT_HARVEST_TOL = 0.005; + +// Returns false to cancel. +using DecimateProgressFn = std::function; + +struct DecimateResult +{ + TriSoup geometry; + // The locked faces alone met the target, so it was unreachable without touching preserved + // geometry. + bool locked_over_budget = false; + // Reaching the target took a collapse costing more than harvest_tol, so the target removed detail + // and not only flat faces a harvest would have taken anyway. + bool target_cost_detail = false; +}; + +// `locked_faces`: one entry per input triangle; a vertex touching one may neither move nor be +// removed, which also pins the ring between the two regions. +// `face_color`: optional, one entry per input triangle. An edge between two faces of different +// colour is treated as a crease, so the simplified triangles never span a colour boundary and the +// boundary keeps its place - a per-triangle colour read off the result then has nothing to smear. +DecimateResult decimate(const TriSoup &geometry, size_t target_triangles, bool harvest_flat = true, + double harvest_tol = DECIMATE_DEFAULT_HARVEST_TOL, + const std::vector &locked_faces = {}, + const DecimateProgressFn &on_progress = {}, + const std::vector &face_color = {}); + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeDisplace.cpp b/src/libslic3r/TextureBake/TextureBakeDisplace.cpp new file mode 100644 index 0000000000..6e306b1c43 --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeDisplace.cpp @@ -0,0 +1,285 @@ +#include "TextureBakeDisplace.hpp" + +#include +#include +#include + +#include +#include + +namespace Slic3r { +namespace TextureBake { + +TriSoup apply_displacement(const TriSoup &geometry, const HeightSampleFn &sample, + const DisplaceSettings &settings, const DisplaceBounds &bounds, + const DisplaceProgressFn &on_progress) +{ + TriSoup out; + const size_t count = geometry.pos.size(); + if (count == 0 || !sample) + return geometry; + + out.pos.resize(count); + out.nrm.resize(count); + + // Everything below is keyed by this id, which is what makes one vector per position expressible. + const bool need_id_positions = settings.boundary_falloff > 0.f; + QuantizedPointMap dedup(WELD_GRID_GEOMETRY, std::min(count, size_t(1) << 22)); + std::vector vertex_id(count); + std::vector id_pos; + int next_id = 0; + for (size_t i = 0; i < count; ++i) { + const int id = dedup.get_or_set(geometry.pos[i], next_id); + if (dedup.inserted()) { + ++next_id; + if (need_id_positions) + id_pos.push_back(geometry.pos[i]); + } + vertex_id[i] = id; + } + const size_t unique_count = size_t(next_id); + + // Pass 1: area-weighted smooth normals per position, plus what masking and falloff need. + std::vector smooth_nrm(unique_count, Vec3d::Zero()); + std::vector masked_area(unique_count, 0.0), total_area(unique_count, 0.0); + const bool have_weights = !geometry.exclude_weight.empty(); + std::vector user_excluded_face(have_weights ? count / 3 : 0, 0); + std::vector excluded_pos(have_weights ? unique_count : 0, 0); + + for (size_t t = 0; t + 2 < count; t += 3) { + const Vec3d a = geometry.pos[t].cast(); + const Vec3d face_n = (geometry.pos[t + 1].cast() - a).cross(geometry.pos[t + 2].cast() - a); + const double face_area = face_n.norm(); // twice the triangle area, so weighting is natural + const double nz = face_area > 1e-12 ? face_n.z() / face_area : 0.0; + const double face_angle = std::acos(std::min(1.0, std::abs(nz))) * (180.0 / M_PI); + const bool angle_masked = + nz < 0.0 ? (settings.bottom_angle_limit > 0.f && face_angle <= settings.bottom_angle_limit) + : (settings.top_angle_limit > 0.f && face_angle <= settings.top_angle_limit); + + // Thresholded high, not at a half: merging by maximum leaves a face bordering an excluded one + // with two corners at 1.0, averaging about 0.67, which a half threshold would misread. + bool user_excluded = false; + if (have_weights) { + const float avg = (geometry.exclude_weight[t] + geometry.exclude_weight[t + 1] + + geometry.exclude_weight[t + 2]) / 3.f; + user_excluded = avg > 0.99f; + if (user_excluded) + user_excluded_face[t / 3] = 1; + } + + for (int v = 0; v < 3; ++v) { + const size_t vid = size_t(vertex_id[t + size_t(v)]); + if (user_excluded && have_weights) + excluded_pos[vid] = 1; + // Subdivision split vertices at sharp edges, so these are smooth across soft edges and + // sharp across hard ones - no faceting on round surfaces, no rounding of corners. + smooth_nrm[vid] += geometry.nrm[t + size_t(v)].cast() * face_area; + if (angle_masked) + masked_area[vid] += face_area; + total_area[vid] += face_area; + } + } + + // The pre-normalisation magnitude over the total area says how much the neighbouring faces agree: + // near 1 they do, near 0 they cancelled, meaning a knife edge with no usable surface direction. + std::vector reliability(unique_count, 0.0); + for (size_t id = 0; id < unique_count; ++id) { + const double len = smooth_nrm[id].norm(); + reliability[id] = (len > 0.0 && total_area[id] > 0.0) ? len / total_area[id] : 0.0; + smooth_nrm[id] = (len > 0.0) ? Vec3d(smooth_nrm[id] / len) : Vec3d(0.0, 0.0, 1.0); + } + + // Pass 1.5: the smoothed blend normal - see the header for why it is separate. + std::vector blend_nrm = smooth_nrm; + if (settings.blend_normal_smoothing > 0 && unique_count > 0) { + // CSR adjacency over the welded graph, deliberately a multigraph: duplicates weight a pair by + // how often it shares an edge, so a well-connected surface couples more strongly. + std::vector degree(unique_count, 0); + const auto add_degree = [&](int a, int b) { + if (a != b) { ++degree[size_t(a)]; ++degree[size_t(b)]; } + }; + for (size_t t = 0; t + 2 < count; t += 3) { + const int a = vertex_id[t], b = vertex_id[t + 1], c = vertex_id[t + 2]; + add_degree(a, b); add_degree(b, c); add_degree(c, a); + } + std::vector csr_start(unique_count + 1, 0); + for (size_t id = 0; id < unique_count; ++id) + csr_start[id + 1] = csr_start[id] + degree[id]; + std::vector neighbors(csr_start[unique_count]); + std::vector cursor(unique_count, 0); + const auto add_edge = [&](int a, int b) { + if (a == b) + return; + neighbors[csr_start[size_t(a)] + cursor[size_t(a)]++] = uint32_t(b); + neighbors[csr_start[size_t(b)] + cursor[size_t(b)]++] = uint32_t(a); + }; + for (size_t t = 0; t + 2 < count; t += 3) { + const int a = vertex_id[t], b = vertex_id[t + 1], c = vertex_id[t + 2]; + add_edge(a, b); add_edge(b, c); add_edge(c, a); + } + + std::vector cur = smooth_nrm, nxt(unique_count, Vec3d::Zero()); + for (int iter = 0; iter < settings.blend_normal_smoothing; ++iter) { + // Jacobi, so every vertex reads the previous iteration and the rows are independent. + tbb::parallel_for(tbb::blocked_range(0, unique_count, 4096), [&](const tbb::blocked_range &r) { + for (size_t id = r.begin(); id < r.end(); ++id) { + const uint32_t s = csr_start[id], e = csr_start[id + 1]; + if (e == s) { + nxt[id] = cur[id]; + continue; + } + Vec3d sum = Vec3d::Zero(); + for (uint32_t k = s; k < e; ++k) + sum += cur[neighbors[k]]; + sum /= double(e - s); + const double len = sum.norm(); + // Cancelling neighbours mean a knife edge; keep what we had. + nxt[id] = (len > 1e-12) ? Vec3d(sum / len) : cur[id]; + } + }); + cur.swap(nxt); + } + blend_nrm = std::move(cur); + } + + // A boundary position borders both masked and unmasked faces, or sits on the exclusion seam. + // Every other position gets its distance to the nearest one, ramped to 1 at the falloff distance. + std::vector falloff; + if (settings.boundary_falloff > 0.f && unique_count > 0) { + std::vector boundary; + for (size_t id = 0; id < unique_count; ++id) { + const double frac = total_area[id] > 0.0 ? masked_area[id] / total_area[id] : 0.0; + const bool on_excl = !excluded_pos.empty() && excluded_pos[id] != 0; + if (on_excl || (frac > 0.0 && frac < 1.0)) + boundary.push_back(id_pos[id]); + } + falloff.assign(unique_count, 1.0); + if (!boundary.empty()) { + // A uniform grid: the query is nearest-point only, so a tree costs more than it saves. + Vec3f lo = boundary.front(), hi = boundary.front(); + for (const Vec3f &p : boundary) { + lo = lo.cwiseMin(p); + hi = hi.cwiseMax(p); + } + const Vec3f span = (hi - lo).cwiseMax(Vec3f(1e-6f, 1e-6f, 1e-6f)); + const int res = std::clamp(int(std::ceil(std::cbrt(double(boundary.size())) * 2.0)), 4, 128); + const Vec3f cell = span / float(res); + const float cell_min = cell.minCoeff(); + const auto cell_of = [&](const Vec3f &p) { + Vec3i32 c; + for (int k = 0; k < 3; ++k) + c[k] = std::clamp(int((p[k] - lo[k]) / span[k] * float(res)), 0, res - 1); + return c; + }; + const auto cell_index = [&](int x, int y, int z) { + return size_t(z) * size_t(res) * size_t(res) + size_t(y) * size_t(res) + size_t(x); + }; + std::vector> grid(size_t(res) * size_t(res) * size_t(res)); + for (size_t i = 0; i < boundary.size(); ++i) { + const Vec3i32 c = cell_of(boundary[i]); + grid[cell_index(c.x(), c.y(), c.z())].push_back(int(i)); + } + + const double radius = double(settings.boundary_falloff); + for (size_t id = 0; id < unique_count; ++id) { + const Vec3f &p = id_pos[id]; + const Vec3i32 c = cell_of(p); + double best = std::numeric_limits::max(); + // Anything in shell r is at least (r - 1) cells away, so once the best found is within + // that bound nothing closer can be hiding further out. + for (int r = 0; r < res; ++r) { + for (int dz = -r; dz <= r; ++dz) + for (int dy = -r; dy <= r; ++dy) + for (int dx = -r; dx <= r; ++dx) { + // The shell only; its interior was covered by a smaller r. + if (r > 0 && std::abs(dx) != r && std::abs(dy) != r && std::abs(dz) != r) + continue; + const int qx = c.x() + dx, qy = c.y() + dy, qz = c.z() + dz; + if (qx < 0 || qy < 0 || qz < 0 || qx >= res || qy >= res || qz >= res) + continue; + for (const int bi : grid[cell_index(qx, qy, qz)]) + best = std::min(best, double((boundary[size_t(bi)] - p).norm())); + } + if (best <= double(r) * double(cell_min)) + break; + } + falloff[id] = (best == std::numeric_limits::max() || radius <= 0.0) + ? 1.0 + : std::clamp(best / radius, 0.0, 1.0); + } + } + } + + // Pass 2: one sample per unique position. A representative corner is picked first so the sampling + // itself is a flat parallel loop - it is a texture fetch plus projection maths per layer, and by + // far the most expensive thing in this stage. + std::vector grey(unique_count, 0.0); + std::vector representative(unique_count, -1); + for (size_t i = 0; i < count; ++i) + if (representative[size_t(vertex_id[i])] < 0) + representative[size_t(vertex_id[i])] = int(i); + tbb::parallel_for(tbb::blocked_range(0, unique_count), + [&](const tbb::blocked_range &range) { + for (size_t vid = range.begin(); vid < range.end(); ++vid) { + const int rep = representative[vid]; + if (rep < 0) + continue; + grey[vid] = double(sample(geometry.pos[size_t(rep)], + smooth_nrm[vid].cast(), + blend_nrm[vid].cast())); + } + }); + + // Pass 3: move every copy of a position by the identical vector. Each iteration writes only its + // own output slot, so the loop is independent per corner. + tbb::parallel_for(tbb::blocked_range(0, count), [&](const tbb::blocked_range &range) { + for (size_t i = range.begin(); i < range.end(); ++i) { + const Vec3f &p = geometry.pos[i]; + const size_t vid = size_t(vertex_id[i]); + + // Only angle masking uses the per-position blend, so an excluded face never dims its + // neighbours through a shared vertex. + const bool face_excluded = !user_excluded_face.empty() && user_excluded_face[i / 3] != 0; + // Pinned where an included face shares a position with an excluded one, sealing the boundary. + const bool sealed_boundary = + !face_excluded && !excluded_pos.empty() && excluded_pos[vid] != 0; + const double masked_frac = total_area[vid] > 0.0 ? masked_area[vid] / total_area[vid] : 0.0; + const double centered = settings.symmetric ? (grey[vid] - 0.5) : grey[vid]; + const double ramp = falloff.empty() ? 1.0 : falloff[vid]; + const double disp = (face_excluded || sealed_boundary) + ? 0.0 + : ramp * (1.0 - masked_frac) * centered * double(settings.amplitude); + + Vec3d moved = p.cast() + smooth_nrm[vid] * disp; + + // Stop a partly masked vertex poking through the surface it borders. + if (masked_frac > 0.0) { + if (settings.bottom_angle_limit > 0.f && moved.z() < double(p.z())) moved.z() = double(p.z()); + if (settings.top_angle_limit > 0.f && moved.z() > double(p.z())) moved.z() = double(p.z()); + } + if (settings.no_downward_z && moved.z() < double(p.z())) + moved.z() = double(p.z()); + // A vertex starting on the bottom plane stays there: otherwise a downward-facing face pulls + // *up* where the sample is below mid-grey, leaving bed-contact vertices at differing heights. + if (settings.no_downward_z && double(p.z()) <= double(bounds.min.z()) + 1e-5) + moved.z() = double(p.z()); + + out.pos[i] = moved.cast(); + } + }); + + // Per-face, not averaged across shared positions: averaging can flip an excluded face's normal + // when its neighbours moved outward. + tbb::parallel_for(tbb::blocked_range(0, count / 3), [&](const tbb::blocked_range &r) { + for (size_t f = r.begin(); f < r.end(); ++f) { + const size_t t = f * 3; + const Vec3f n = (out.pos[t + 1] - out.pos[t]).cross(out.pos[t + 2] - out.pos[t]).normalized(); + out.nrm[t] = out.nrm[t + 1] = out.nrm[t + 2] = n; + } + }); + out.exclude_weight = geometry.exclude_weight; + return out; +} + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeDisplace.hpp b/src/libslic3r/TextureBake/TextureBakeDisplace.hpp new file mode 100644 index 0000000000..eee6ca0626 --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeDisplace.hpp @@ -0,0 +1,68 @@ +#pragma once + +// Displacement along surface normals. +// +// The mesh is non-indexed, so at a shared edge two triangles hold the same position with different +// face normals; displacing each copy along its own normal sends them to different points and opens a +// crack. So one smooth (area-weighted) normal per unique position drives both the sample lookup and +// the displacement direction, every copy moves by the same vector, and the result is watertight by +// construction. Displaced normals are then smooth at hard edges, but the geometry is still faceted, +// so printed edges stay sharp. + +#include +#include +#include + +#include "TextureBakeIndex.hpp" + +namespace Slic3r { +namespace TextureBake { + +// Height at a point, called once per unique welded position. `smooth_normal` is the vector the +// displacement will move along; `blend_normal` is that after smoothing, for projection blend weights. +using HeightSampleFn = std::function; + +struct DisplaceSettings +{ + // Displacement height in mm, applied to the sampled value. + float amplitude = 0.4f; + + // Sample around a mid-grey rest level rather than displacing outward only. + bool symmetric = false; + + // Faces flatter than these (degrees from horizontal) are held back, leaving bed-contact and top + // surfaces alone. 0 disables that side. + float bottom_angle_limit = 5.f; + float top_angle_limit = 0.f; + + // Never move a vertex below its original Z, so no new overhang. The sideways component is kept. + bool no_downward_z = false; + + // Distance in mm over which displacement ramps up from a mask boundary. 0 leaves a hard edge. + float boundary_falloff = 0.f; + + // Laplacian iterations on the blend normal only - the displacement direction must stay the exact + // smooth normal or copies of a position move differently and the mesh cracks. Inside a blend band + // the weight gradient is largest, so a few degrees of vertex-to-vertex jitter multiplies the + // difference between two unrelated height samples into visible seam noise. A no-op on an + // already-smooth surface. + int blend_normal_smoothing = 32; +}; + +// Model extents; only the minimum Z is read, for the bottom-plane clamp. +struct DisplaceBounds +{ + Vec3f min = Vec3f::Zero(); + Vec3f max = Vec3f::Zero(); +}; + +// Returns false to cancel. +using DisplaceProgressFn = std::function; + +TriSoup apply_displacement(const TriSoup &geometry, const HeightSampleFn &sample, + const DisplaceSettings &settings, const DisplaceBounds &bounds, + const DisplaceProgressFn &on_progress = {}); + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeFlip.cpp b/src/libslic3r/TextureBake/TextureBakeFlip.cpp new file mode 100644 index 0000000000..f4d9997f71 --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeFlip.cpp @@ -0,0 +1,212 @@ +#include "TextureBakeFlip.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +namespace Slic3r { +namespace TextureBake { + +FlipResult flip_edges_to_height(const TriSoup &geometry, const std::vector &face_parent_id, + const HeightSampleFn &sample, const FlipSettings &settings, + const std::vector &locked) +{ + FlipResult result; + result.geometry = geometry; + result.face_parent_id = face_parent_id; + const size_t count = geometry.pos.size(); + const size_t tri_ct = count / 3; + if (count == 0 || !sample || settings.passes <= 0) + return result; + + // Weld, so a flip rewrites triangles in terms of shared vertices rather than positions. + QuantizedPointMap weld(WELD_GRID_GEOMETRY, std::min(count, size_t(1) << 22)); + std::vector vid(count); + std::vector pos; + std::vector weight; // exclude weight per unique vertex, the max over its copies + const bool has_weight = !geometry.exclude_weight.empty(); + for (size_t i = 0; i < count; ++i) { + vid[i] = weld.get_or_set(geometry.pos[i], int(pos.size())); + if (weld.inserted()) { + pos.push_back(geometry.pos[i]); + weight.push_back(has_weight ? geometry.exclude_weight[i] : 0.f); + } else if (has_weight) { + weight[size_t(vid[i])] = std::max(weight[size_t(vid[i])], geometry.exclude_weight[i]); + } + } + const size_t nv = pos.size(); + + // Triangles as vertex ids; a locked or excluded triangle never takes part. + std::vector> tri(tri_ct); + std::vector fixed(tri_ct, 0); + for (size_t t = 0; t < tri_ct; ++t) { + tri[t] = { vid[t * 3], vid[t * 3 + 1], vid[t * 3 + 2] }; + if (!locked.empty() && t < locked.size() && locked[t]) + fixed[t] = 1; + if (has_weight && geometry.exclude_weight[t * 3] > 0.99f) + fixed[t] = 1; + } + + // Height per unique vertex along its area-weighted normal, the direction displacement will use. + std::vector nrm(nv, Vec3f::Zero()); + std::vector face_n(tri_ct); + const auto rebuild_normals = [&]() { + std::fill(nrm.begin(), nrm.end(), Vec3f::Zero()); + for (size_t t = 0; t < tri_ct; ++t) { + const Vec3f fn = (pos[size_t(tri[t][1])] - pos[size_t(tri[t][0])]).cross(pos[size_t(tri[t][2])] - pos[size_t(tri[t][0])]); + face_n[t] = fn; + for (int k = 0; k < 3; ++k) + nrm[size_t(tri[t][size_t(k)])] += fn; + } + for (Vec3f &n : nrm) { + const float l = n.norm(); + n = (l > 0.f) ? Vec3f(n / l) : Vec3f(0.f, 0.f, 1.f); + } + }; + rebuild_normals(); + std::vector h(nv, 0.f); + tbb::parallel_for(tbb::blocked_range(0, nv), [&](const tbb::blocked_range &r) { + for (size_t v = r.begin(); v < r.end(); ++v) + h[v] = sample(pos[v], nrm[v], nrm[v]); + }); + float h_lo = std::numeric_limits::max(), h_hi = -h_lo; + for (const float x : h) { h_lo = std::min(h_lo, x); h_hi = std::max(h_hi, x); } + const float range = h_hi - h_lo; + if (!(range > 0.f)) + return result; // flat: every diagonal is as good as the other + const float min_gain = float(settings.min_gain_fraction) * range; + const float planar = float(settings.min_planar_cos); + + struct Candidate + { + uint32_t t1, t2; // the two triangles + uint8_t k1, k2; // corner index in each where the shared edge starts (t1: a->c, t2: c->a) + float gain; + }; + + for (int pass = 0; pass < settings.passes; ++pass) { + // Half-edges keyed by their undirected edge, sorted so the two halves of an interior edge land + // next to each other; a run of exactly two with opposite directions is a manifold interior + // edge. Sorting beats hashing here by an order of magnitude on a few million triangles. + struct Half { uint64_t key; uint32_t corner; }; + std::vector half; + half.reserve(count); + for (size_t t = 0; t < tri_ct; ++t) + for (int k = 0; k < 3; ++k) { + const int from = tri[t][size_t(k)], to = tri[t][size_t((k + 1) % 3)]; + if (from == to) continue; + const uint32_t lo = uint32_t(std::min(from, to)), hi = uint32_t(std::max(from, to)); + half.push_back({ (uint64_t(lo) << 32) | hi, uint32_t(t * 3 + size_t(k)) }); + } + tbb::parallel_sort(half.begin(), half.end(), [](const Half &x, const Half &y) { + return x.key != y.key ? x.key < y.key : x.corner < y.corner; + }); + std::vector> edges; // (corner in t1, corner in t2), t1 < t2 + edges.reserve(half.size() / 2); + for (size_t i = 0; i < half.size();) { + size_t j = i + 1; + while (j < half.size() && half[j].key == half[i].key) ++j; + if (j - i == 2) { + // Opposite directions: the lower vertex id is `from` in exactly one of the two. + const uint32_t c1 = half[i].corner, c2 = half[i + 1].corner; + const int f1 = tri[c1 / 3][size_t(c1 % 3)], f2 = tri[c2 / 3][size_t(c2 % 3)]; + if (f1 != f2) + edges.emplace_back(c1, c2); + } + i = j; + } + std::vector cands(edges.size()); + std::vector valid(edges.size(), 0); + tbb::parallel_for(tbb::blocked_range(0, edges.size()), [&](const tbb::blocked_range &r) { + for (size_t i = r.begin(); i < r.end(); ++i) { + const uint32_t c1 = uint32_t(edges[i].first), c2 = uint32_t(edges[i].second); + const uint32_t t1 = c1 / 3, t2 = c2 / 3; + const int k1 = int(c1 % 3), k2 = int(c2 % 3); + if (fixed[t1] || fixed[t2]) continue; + if (!face_parent_id.empty() && face_parent_id[t1] != face_parent_id[t2]) continue; + // a->c is the shared edge in t1, with b opposite; t2 runs c->a with d opposite. + const int a = tri[t1][size_t(k1)], c = tri[t1][size_t((k1 + 1) % 3)], b = tri[t1][size_t((k1 + 2) % 3)]; + const int d = tri[t2][size_t((k2 + 2) % 3)]; + if (b == d) continue; + // Level quads have nothing to gain; the test is on the corners, before any sampling. + const float hmin = std::min({ h[size_t(a)], h[size_t(b)], h[size_t(c)], h[size_t(d)] }); + const float hmax = std::max({ h[size_t(a)], h[size_t(b)], h[size_t(c)], h[size_t(d)] }); + if (hmax - hmin < min_gain) continue; + // Coplanar enough to have a real alternative, and convex so the alternative is valid: + // the new triangles (a, d, b) and (d, c, b) must both face the way the quad does, with + // a decent share of its area. + const Vec3f n1 = face_n[t1], n2 = face_n[t2]; + const float l1 = n1.norm(), l2 = n2.norm(); + if (l1 <= 0.f || l2 <= 0.f || n1.dot(n2) < planar * l1 * l2) continue; + const Vec3f quad_n = (n1 + n2).normalized(); + const Vec3f &pa = pos[size_t(a)], &pb = pos[size_t(b)], &pc = pos[size_t(c)], &pd = pos[size_t(d)]; + const Vec3f m1 = (pd - pa).cross(pb - pa), m2 = (pc - pd).cross(pb - pd); + const float area_old = l1 + l2, area_new = m1.dot(quad_n) + m2.dot(quad_n); + const float min_part = 0.05f * area_old; + if (m1.dot(quad_n) < min_part || m2.dot(quad_n) < min_part) continue; + if (std::abs(area_new - area_old) > 0.02f * area_old) continue; // not the same quad: folded + // The diagonals' midpoint errors. + const auto mid_err = [&](int u, int w) { + const Vec3f p = 0.5f * (pos[size_t(u)] + pos[size_t(w)]); + Vec3f n = nrm[size_t(u)] + nrm[size_t(w)]; + const float l = n.norm(); + n = (l > 0.f) ? Vec3f(n / l) : quad_n; + return std::abs(sample(p, n, n) - 0.5f * (h[size_t(u)] + h[size_t(w)])); + }; + const float gain = mid_err(a, c) - mid_err(b, d); + if (gain < min_gain) continue; + cands[i] = { t1, t2, uint8_t(k1), uint8_t(k2), gain }; + valid[i] = 1; + } + }); + std::vector chosen; + for (size_t i = 0; i < cands.size(); ++i) + if (valid[i]) chosen.push_back(cands[i]); + std::sort(chosen.begin(), chosen.end(), [](const Candidate &x, const Candidate &y) { + return x.gain != y.gain ? x.gain > y.gain : (x.t1 != y.t1 ? x.t1 < y.t1 : x.t2 < y.t2); + }); + // Best first, and a triangle changes at most once per pass. + std::vector touched(tri_ct, 0); + size_t applied = 0; + for (const Candidate &cd : chosen) { + if (touched[cd.t1] || touched[cd.t2]) continue; + const int a = tri[cd.t1][cd.k1], c = tri[cd.t1][size_t((cd.k1 + 1) % 3)], b = tri[cd.t1][size_t((cd.k1 + 2) % 3)]; + const int d = tri[cd.t2][size_t((cd.k2 + 2) % 3)]; + tri[cd.t1] = { a, d, b }; + tri[cd.t2] = { d, c, b }; + touched[cd.t1] = touched[cd.t2] = 1; + ++applied; + } + result.flipped += applied; + if (applied == 0) + break; + rebuild_normals(); // face normals feed the planarity test of the next pass + } + + if (result.flipped == 0) + return result; + + // Back to the soup: positions, weights and per-face normals from the (possibly rewritten) triangles. + for (size_t t = 0; t < tri_ct; ++t) { + for (int k = 0; k < 3; ++k) { + const size_t i = t * 3 + size_t(k); + const int v = tri[t][size_t(k)]; + result.geometry.pos[i] = pos[size_t(v)]; + if (has_weight) + result.geometry.exclude_weight[i] = weight[size_t(v)]; + } + Vec3f n = (result.geometry.pos[t * 3 + 1] - result.geometry.pos[t * 3]).cross(result.geometry.pos[t * 3 + 2] - result.geometry.pos[t * 3]); + const float len = n.norm(); + n = (len > 0.f) ? Vec3f(n / len) : Vec3f(0.f, 0.f, 1.f); + result.geometry.nrm[t * 3] = result.geometry.nrm[t * 3 + 1] = result.geometry.nrm[t * 3 + 2] = n; + } + return result; +} + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeFlip.hpp b/src/libslic3r/TextureBake/TextureBakeFlip.hpp new file mode 100644 index 0000000000..02701bbced --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeFlip.hpp @@ -0,0 +1,57 @@ +#pragma once + +// Data-dependent edge flipping: choose each quad's diagonal to follow the height field, before any +// displacement happens. +// +// Refinement produces a regular grid, and a step in the height map that crosses that grid at an +// angle lands on alternating corners: one triangle of a quad gets a raised corner, the next does not, +// and the displaced wall comes out as a sawtooth the size of the grid. Finer triangles make the teeth +// smaller, never straight. The cause is the diagonal, not the density: with the diagonal running +// along the step both triangles of the quad sit cleanly on one side or the other, and the wall is a +// straight line between them. +// +// So, for every interior edge, compare the two diagonals of the quad it spans by how well each +// interpolates the field at its own midpoint - the sampled height there against the mean of its two +// endpoints - and keep the better one. A quad whose four corners are level is skipped outright, so +// flat regions cost nothing; a non-planar quad (a model crease) is never touched, nor is one whose +// triangles belong to different source faces or straddle the painted boundary. + +#include +#include + +#include "TextureBakeDisplace.hpp" +#include "TextureBakeIndex.hpp" + +namespace Slic3r { +namespace TextureBake { + +struct FlipSettings +{ + // Passes over all edges. Flips interact through shared triangles, so a pass applies non-conflicting + // ones and the next pass picks up the rest; two or three settle a grid. + int passes = 3; + + // A flip has to reduce the midpoint error by at least this fraction of the height range, so noise + // on a rough surface does not toggle diagonals for nothing. + double min_gain_fraction = 0.02; + + // The two triangles have to be this coplanar (cosine of their normals' angle) for the quad to have + // a meaningful alternative diagonal at all: across a real crease there is none. + double min_planar_cos = 0.985; // ~10 degrees +}; + +struct FlipResult +{ + TriSoup geometry; + std::vector face_parent_id; + size_t flipped = 0; +}; + +// `face_parent_id` may be empty; when given it is carried through unchanged (a flip never crosses a +// parent boundary). `locked` flags triangles that must not change (per triangle, may be empty). +FlipResult flip_edges_to_height(const TriSoup &geometry, const std::vector &face_parent_id, + const HeightSampleFn &sample, const FlipSettings &settings, + const std::vector &locked); + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeIndex.cpp b/src/libslic3r/TextureBake/TextureBakeIndex.cpp new file mode 100644 index 0000000000..ac7234774b --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeIndex.cpp @@ -0,0 +1,25 @@ +#include "TextureBakeIndex.hpp" + +#include + +namespace Slic3r { +namespace TextureBake { + +WeldResult weld_vertices(const std::vector &positions, double quant) +{ + WeldResult out; + QuantizedPointMap map(quant, std::min(positions.size(), size_t(1) << 22)); + out.vertex_id.resize(positions.size()); + int next_id = 0; + for (size_t i = 0; i < positions.size(); ++i) { + const int id = map.get_or_set(positions[i], next_id); + if (map.inserted()) + ++next_id; + out.vertex_id[i] = id; + } + out.unique_count = next_id; + return out; +} + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeIndex.hpp b/src/libslic3r/TextureBake/TextureBakeIndex.hpp new file mode 100644 index 0000000000..2b1dd72395 --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeIndex.hpp @@ -0,0 +1,158 @@ +#pragma once + +// Vertex welding for the texture bake pipeline. The pipeline works on non-indexed triangle soup, so +// a shared point exists once per incident triangle with float noise between the copies; welding maps +// each quantised position to one integer id. +// +// The three grids below are deliberately not unified - changing one at a call site changes +// watertightness. 100 um matches the precision files are written with; 10 um keeps small fillet +// vertices distinct (they merge at 100 um, giving needle artifacts after displacement) while still +// absorbing float noise; 1 um is what collapse positioning needs. + +#include +#include +#include +#include + +#include "../Point.hpp" + +namespace Slic3r { +namespace TextureBake { + +static constexpr double WELD_GRID_EXPORT = 1e4; // 100 um +static constexpr double WELD_GRID_GEOMETRY = 1e5; // 10 um +static constexpr double WELD_GRID_DECIMATION = 1e6; // 1 um + +// Round half toward positive infinity. Quantised coordinates hit exact halves often enough that the +// tie rule matters. +inline int64_t grid_round(double v) { return int64_t(std::floor(v + 0.5)); } + +// Open-addressing table, linear probing: no allocation per lookup, exact integer key comparison. +// Values must be non-negative; -1 is the empty sentinel and what get() returns on a miss. +// +// Key and value live together in one 32-byte cell. They used to be four parallel arrays, which made a +// single probe touch four cache lines - and probing this table was 13% of a whole bake, because every +// stage welds the full soup through it. +class QuantizedPointMap +{ +public: + explicit QuantizedPointMap(double quant, size_t expected = 256) : m_quant(quant) + { + size_t cap = 16; + const size_t target = std::max(16, size_t(std::ceil(double(expected) / 0.6))); + while (cap < target) + cap *= 2; + alloc(cap); + } + + size_t size() const { return m_size; } + // Whether the last get_or_set() inserted rather than found. + bool inserted() const { return m_inserted; } + + int get(float x, float y, float z) + { + return m_cells[slot(grid_round(double(x) * m_quant), grid_round(double(y) * m_quant), + grid_round(double(z) * m_quant))].val; + } + int get(const Vec3f &p) { return get(p.x(), p.y(), p.z()); } + + // The value already stored for this position's grid cell; if there is none, store `value` and + // return it. inserted() then says which of the two happened. + int get_or_set(float x, float y, float z, int value) + { + return get_or_set_key(grid_round(double(x) * m_quant), grid_round(double(y) * m_quant), + grid_round(double(z) * m_quant), value); + } + int get_or_set(const Vec3f &p, int value) { return get_or_set(p.x(), p.y(), p.z(), value); } + + // The same table as a set of integer tuples (edge marking, midpoint cache). Quantisation is + // bypassed: routing ids through the float overloads loses precision above 2^24. + int get_key(int64_t a, int64_t b, int64_t c) { return m_cells[slot(a, b, c)].val; } + int get_or_set_key(int64_t a, int64_t b, int64_t c, int value) + { + const size_t i = slot(a, b, c); + Cell &cell = m_cells[i]; + if (cell.val != -1) { + m_inserted = false; + return cell.val; + } + cell.qx = a; cell.qy = b; cell.qz = c; + cell.val = value; + m_inserted = true; + if (++m_size > size_t(double(m_cap) * 0.7)) + grow(); + return value; + } + +private: + struct Cell + { + int64_t qx = 0, qy = 0, qz = 0; + int32_t val = -1; + }; + + void alloc(size_t cap) + { + m_cap = cap; + m_mask = cap - 1; + m_cells.assign(cap, Cell{}); + } + + size_t slot(int64_t qx, int64_t qy, int64_t qz) const + { + // Unsigned multiplies: the signed versions overflowed on nearly every key, which is undefined + // behaviour. The resulting bits are identical on every target OrcaSlicer builds for. + // + // A stronger 64-bit finalizer was tried and measured no faster - the probing that shows up in a + // profile is subdivide's parallel mark count, spread over every core, not long probe chains. + uint32_t h = (uint32_t(qx) * 0x9E3779B1u) ^ (uint32_t(qy) * 0x85EBCA77u) ^ (uint32_t(qz) * 0xC2B2AE3Du); + h ^= h >> 15; + size_t i = size_t(h) & m_mask; + // Equality is checked against the stored 64-bit keys, so truncating to 32 bits for the hash + // costs collisions at worst, never a wrong answer. + while (m_cells[i].val != -1) { + const Cell &c = m_cells[i]; + if (c.qx == qx && c.qy == qy && c.qz == qz) + return i; + i = (i + 1) & m_mask; + } + return i; + } + + void grow() + { + std::vector old = std::move(m_cells); + alloc(m_cap * 2); + for (const Cell &c : old) + if (c.val != -1) + m_cells[slot(c.qx, c.qy, c.qz)] = c; + } + + double m_quant; + size_t m_cap = 0, m_mask = 0, m_size = 0; + bool m_inserted = false; + std::vector m_cells; +}; + +// Three consecutive entries per triangle. The indexers turn this into shared vertices where a stage +// needs adjacency. +struct TriSoup +{ + std::vector pos; + std::vector nrm; // parallel to pos + std::vector exclude_weight; // parallel to pos; empty when nothing is excluded + + size_t triangle_count() const { return pos.size() / 3; } + bool empty() const { return pos.empty(); } +}; + +// Assign each vertex the sequential id of its quantised position, first occurrence winning. +struct WeldResult +{ + std::vector vertex_id; + int unique_count = 0; +}; +WeldResult weld_vertices(const std::vector &positions, double quant); + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeMesh.cpp b/src/libslic3r/TextureBake/TextureBakeMesh.cpp new file mode 100644 index 0000000000..6f6d72218d --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeMesh.cpp @@ -0,0 +1,62 @@ +#include "TextureBakeMesh.hpp" + +#include + +namespace Slic3r { +namespace TextureBake { + +TriSoup to_soup(const indexed_triangle_set &its, const std::vector &face_excluded) +{ + TriSoup out; + const size_t n = its.indices.size(); + out.pos.resize(n * 3); + out.nrm.resize(n * 3); + const bool have_excl = face_excluded.size() == n; + if (have_excl) + out.exclude_weight.resize(n * 3); + + for (size_t t = 0; t < n; ++t) { + const stl_triangle_vertex_indices &tri = its.indices[t]; + const Vec3f a = its.vertices[size_t(tri[0])]; + const Vec3f b = its.vertices[size_t(tri[1])]; + const Vec3f c = its.vertices[size_t(tri[2])]; + Vec3f nrm = (b - a).cross(c - a); + const float len = nrm.norm(); + nrm = (len > 0.f) ? Vec3f(nrm / len) : Vec3f(0.f, 0.f, 1.f); + out.pos[t * 3] = a; + out.pos[t * 3 + 1] = b; + out.pos[t * 3 + 2] = c; + // Per-face on purpose: the accurate indexer derives smooth normals and splits at sharp edges + // itself, so averaged ones would pre-empt that. + out.nrm[t * 3] = out.nrm[t * 3 + 1] = out.nrm[t * 3 + 2] = nrm; + if (have_excl) { + const float w = face_excluded[t] ? 1.f : 0.f; + out.exclude_weight[t * 3] = out.exclude_weight[t * 3 + 1] = out.exclude_weight[t * 3 + 2] = w; + } + } + return out; +} + +indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup) +{ + indexed_triangle_set out; + const size_t n = soup.pos.size(); + out.indices.reserve(n / 3); + QuantizedPointMap map(WELD_GRID_GEOMETRY, std::min(n, size_t(1) << 22)); + std::vector id(n); + for (size_t i = 0; i < n; ++i) { + id[i] = map.get_or_set(soup.pos[i], int(out.vertices.size())); + if (map.inserted()) + out.vertices.push_back(soup.pos[i]); + } + for (size_t t = 0; t + 2 < n; t += 3) { + // Welded-together corners carry no area. + if (id[t] == id[t + 1] || id[t + 1] == id[t + 2] || id[t] == id[t + 2]) + continue; + out.indices.emplace_back(id[t], id[t + 1], id[t + 2]); + } + return out; +} + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeMesh.hpp b/src/libslic3r/TextureBake/TextureBakeMesh.hpp new file mode 100644 index 0000000000..b4e3ced5fe --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeMesh.hpp @@ -0,0 +1,19 @@ +#pragma once + +// Conversion between the pipeline's triangle soup and the indexed mesh used elsewhere. The pipeline +// stays on soup because each stage welds on its own grid, and those differences are load-bearing. + +#include "TextureBakeIndex.hpp" +#include "../TriangleMesh.hpp" + +namespace Slic3r { +namespace TextureBake { + +// `face_excluded`: one entry per input triangle, becoming the soup's per-corner exclusion weight. +TriSoup to_soup(const indexed_triangle_set &its, const std::vector &face_excluded = {}); + +// Welds at the geometry grid. +indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup); + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakePipeline.cpp b/src/libslic3r/TextureBake/TextureBakePipeline.cpp new file mode 100644 index 0000000000..2b678905b1 --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakePipeline.cpp @@ -0,0 +1,375 @@ +#include "TextureBakePipeline.hpp" + +#include +#include + +#include "TextureBakeDebug.hpp" + +#include +#include +#include +#include + +#include + +namespace Slic3r { +namespace TextureBake { + +void clamp_below_bottom(TriSoup &geometry, float bottom_z) +{ + for (size_t t = 0; t + 2 < geometry.pos.size(); t += 3) { + bool dirty = false; + for (int k = 0; k < 3; ++k) + if (geometry.pos[t + size_t(k)].z() < bottom_z) { + geometry.pos[t + size_t(k)].z() = bottom_z; + dirty = true; + } + if (!dirty) + continue; + Vec3f n = (geometry.pos[t + 1] - geometry.pos[t]).cross(geometry.pos[t + 2] - geometry.pos[t]); + const float len = n.norm(); + n = (len > 0.f) ? Vec3f(n / len) : Vec3f(0.f, 0.f, 1.f); + geometry.nrm[t] = geometry.nrm[t + 1] = geometry.nrm[t + 2] = n; + } +} + +size_t snap_bottom_to_flat(TriSoup &geometry, float bottom_z, double tol) +{ + const size_t vert_count = geometry.pos.size(); + const size_t tri_count = vert_count / 3; + if (tri_count == 0 || tol <= 0.0) + return 0; + + // Weld at the finest grid: by this point copies of one position are bit-identical, because every + // earlier stage moved them by the same vector. + QuantizedPointMap weld(WELD_GRID_DECIMATION, std::min(vert_count, size_t(1) << 22)); + std::vector vid(vert_count); + int unique = 0; + for (size_t i = 0; i < vert_count; ++i) { + vid[i] = weld.get_or_set(geometry.pos[i], unique); + if (weld.inserted()) + ++unique; + } + // Incident corners per position, CSR style. + std::vector start(size_t(unique) + 1, 0); + for (size_t i = 0; i < vert_count; ++i) + ++start[size_t(vid[i]) + 1]; + for (size_t id = 0; id < size_t(unique); ++id) + start[id + 1] += start[id]; + std::vector inc(vert_count), cursor(size_t(unique), 0); + for (size_t i = 0; i < vert_count; ++i) + inc[start[size_t(vid[i])] + cursor[size_t(vid[i])]++] = uint32_t(i); + + const double fold_cos = std::cos(75.0 * M_PI / 180.0); + std::vector dirty_tri(tri_count, 0); + + for (size_t id = 0; id < size_t(unique); ++id) { + const float z = geometry.pos[inc[start[id]]].z(); + if (z == bottom_z || std::abs(double(z) - double(bottom_z)) > tol) + continue; + + // Simulate the move: every incident triangle must keep positive area and must not fold. + bool ok = true; + for (uint32_t k = start[id]; k < start[id + 1] && ok; ++k) { + const size_t t = size_t(inc[k]) / 3; + Vec3f p[3]; + for (int v = 0; v < 3; ++v) { + p[v] = geometry.pos[t * 3 + size_t(v)]; + if (vid[t * 3 + size_t(v)] == int(id)) + p[v].z() = bottom_z; + } + const Vec3d on = (geometry.pos[t * 3 + 1] - geometry.pos[t * 3]) + .cross(geometry.pos[t * 3 + 2] - geometry.pos[t * 3]).cast(); + const Vec3d nn = (p[1] - p[0]).cross(p[2] - p[0]).cast(); + const double o2 = on.squaredNorm(), n2 = nn.squaredNorm(); + if (n2 < 1e-20) { ok = false; break; } // would collapse to zero area + if (o2 < 1e-20) continue; // already degenerate, cannot judge a rotation + const double dot = on.dot(nn); + if (dot < 0.0 || dot * dot < fold_cos * fold_cos * o2 * n2) + ok = false; + } + if (!ok) + continue; + + for (uint32_t k = start[id]; k < start[id + 1]; ++k) { + geometry.pos[inc[k]].z() = bottom_z; + dirty_tri[size_t(inc[k]) / 3] = 1; + } + } + + size_t dirty = 0; + for (size_t t = 0; t < tri_count; ++t) { + if (!dirty_tri[t]) + continue; + ++dirty; + Vec3f n = (geometry.pos[t * 3 + 1] - geometry.pos[t * 3]) + .cross(geometry.pos[t * 3 + 2] - geometry.pos[t * 3]); + const float len = n.norm(); + n = (len > 0.f) ? Vec3f(n / len) : Vec3f(0.f, 0.f, 1.f); + geometry.nrm[t * 3] = geometry.nrm[t * 3 + 1] = geometry.nrm[t * 3 + 2] = n; + } + return dirty; +} + +PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample, + const PipelineSettings &settings, const DisplaceBounds &bounds, + PipelineMode mode, const std::vector &face_excluded, + const PipelineProgressFn &on_progress, BakeStageRecorder *debug, + const ColorSampleFn &color_sample) +{ + PipelineResult result; + const auto report = [&](const char *stage, double f) { + return !on_progress || on_progress(stage, f); + }; + // Per-stage wall time. The stages differ in cost by orders of magnitude depending on the model, so + // without this it is guesswork which one to attack. + auto clock_now = [] { return std::chrono::steady_clock::now(); }; + auto t_stage = clock_now(); + // One call site for both the log line and the debug capture, so a stage cannot appear in one and + // be missing from the other. The capture happens after the elapsed time is read: welding the soup + // and scanning its edges costs more than some of the stages do, and must not land inside the + // measurement it is reporting. + const auto lap = [&](const char *stage, const TriSoup &geometry, const std::string &detail = {}) { + const double ms = std::chrono::duration(clock_now() - t_stage).count(); + BOOST_LOG_TRIVIAL(info) << "TextureBake " << stage << ": " << ms << " ms, " + << geometry.triangle_count() << " tris"; + if (debug != nullptr) + debug->capture(stage, geometry, ms, detail); + t_stage = clock_now(); + }; + + if (input.empty() || !sample) { + result.geometry = input; + return result; + } + if (debug != nullptr) + debug->capture("input", input, 0.0, "as handed to the pipeline"); + t_stage = clock_now(); // the capture above is not part of the first stage + + // 1. Refine to the target edge length. + SubdivideResult sub = subdivide( + input, settings.refine_length, face_excluded, /* fast */ false, settings.safety_cap, + [&](double f, size_t, double) { return report("subdivide", f); }, settings.paint_within); + result.safety_cap_hit = sub.safety_cap_hit; + lap("subdivide", sub.geometry); + if (!report("subdivide", 1.0)) { + result.canceled = true; + return result; + } + + // 2. Dissolve the slivers refinement inherited, then recover the edges that lengthened. + if (settings.regularize) { + RegularizeOptions ropts = settings.regularize_opts; + ropts.preserve_excluded = settings.preserve_untextured; + RegularizeResult reg = regularize_mesh(sub.geometry, sub.face_parent_id, + settings.refine_length, ropts); + result.collapse_count = reg.collapse_count; + lap("regularize", reg.geometry, std::to_string(reg.collapse_count) + " collapses"); + if (!report("regularize", 1.0)) { + result.canceled = true; + return result; + } + if (reg.collapse_count > 0) { + // Excluded faces are carried on the soup itself, so the flag is re-derived rather than + // indexed across the collapse. + std::vector excl; + if (!reg.geometry.exclude_weight.empty()) { + excl.assign(reg.geometry.triangle_count(), 0); + for (size_t t = 0; t < excl.size(); ++t) + excl[t] = reg.geometry.exclude_weight[t * 3] > 0.99f ? 1 : 0; + } + sub = subdivide(reg.geometry, settings.refine_length * settings.regularize_second_pass_mul, + excl, false, settings.safety_cap, + [&](double f, size_t, double) { return report("re-subdivide", f); }, + settings.paint_within); + result.safety_cap_hit = result.safety_cap_hit || sub.safety_cap_hit; + // The second pass renumbers faces, so the parent map has to be composed through it. + std::vector composed(sub.face_parent_id.size()); + for (size_t i = 0; i < composed.size(); ++i) { + const int mid = sub.face_parent_id[i]; + composed[i] = (mid >= 0 && size_t(mid) < reg.face_parent_id.size()) + ? reg.face_parent_id[size_t(mid)] : -1; + } + sub.face_parent_id = std::move(composed); + lap("re-subdivide", sub.geometry); + } else { + sub.geometry = std::move(reg.geometry); + sub.face_parent_id = std::move(reg.face_parent_id); + } + } + + // 2b. Paint finer than the input triangles. The caller includes a source triangle when any part of + // it is painted; now that the faces are small, ask once more per face and switch the unpainted + // ones off. They are pinned like the excluded region from here on: their own corners at weight 1, + // and the displacement's boundary sealing pins the stroke's rim on the painted side. + if (settings.painted) { + const size_t nf = sub.geometry.triangle_count(); + const bool have_w = !sub.geometry.exclude_weight.empty(); + std::vector unpainted(nf, 0); + tbb::parallel_for(tbb::blocked_range(0, nf), [&](const tbb::blocked_range &r) { + for (size_t t = r.begin(); t < r.end(); ++t) { + if (have_w && sub.geometry.exclude_weight[t * 3] > 0.99f) + continue; // excluded from the start, never asked + const Vec3f &a = sub.geometry.pos[t * 3], &b = sub.geometry.pos[t * 3 + 1], &c = sub.geometry.pos[t * 3 + 2]; + if (!settings.painted((a + b + c) / 3.f)) + unpainted[t] = 1; + } + }); + size_t switched = 0; + for (size_t t = 0; t < nf; ++t) + switched += unpainted[t]; + if (switched > 0) { + if (sub.geometry.exclude_weight.empty()) + sub.geometry.exclude_weight.assign(sub.geometry.pos.size(), 0.f); + for (size_t t = 0; t < nf; ++t) + if (unpainted[t]) + sub.geometry.exclude_weight[t * 3] = sub.geometry.exclude_weight[t * 3 + 1] = + sub.geometry.exclude_weight[t * 3 + 2] = 1.f; + } + lap("paint", sub.geometry, std::to_string(switched) + " faces switched off"); + if (!report("paint", 1.0)) { + result.canceled = true; + return result; + } + } + + // 3. Align the mesh to the height field's edges, then displace. + if (settings.relocate) { + std::vector locked; + if (settings.preserve_untextured && !sub.geometry.exclude_weight.empty()) { + locked.assign(sub.geometry.triangle_count(), 0); + for (size_t t = 0; t < locked.size(); ++t) + locked[t] = sub.geometry.exclude_weight[t * 3] > 0.99f ? 1 : 0; + } + RelocateResult rel = relocate_to_contours(sub.geometry, sample, settings.relocate_opts, locked); + BOOST_LOG_TRIVIAL(info) << "TextureBake relocate: moved=" << rel.moved + << " rejected=" << rel.rejected; + sub.geometry = std::move(rel.geometry); + lap("relocate", sub.geometry, + "moved " + std::to_string(rel.moved) + ", rejected " + std::to_string(rel.rejected)); + } + + // 3b. Diagonals along the height field's steps, so they displace into straight walls. + if (settings.flip_edges) { + std::vector locked; + if (settings.preserve_untextured && !sub.geometry.exclude_weight.empty()) { + locked.assign(sub.geometry.triangle_count(), 0); + for (size_t t = 0; t < locked.size(); ++t) + locked[t] = sub.geometry.exclude_weight[t * 3] > 0.99f ? 1 : 0; + } + FlipResult fl = flip_edges_to_height(sub.geometry, sub.face_parent_id, sample, settings.flip_opts, locked); + sub.geometry = std::move(fl.geometry); + sub.face_parent_id = std::move(fl.face_parent_id); + lap("align edges", sub.geometry, std::to_string(fl.flipped) + " flips"); + if (!report("align edges", 1.0)) { + result.canceled = true; + return result; + } + } + + TriSoup displaced = apply_displacement(sub.geometry, sample, settings.displace, bounds, + [&](double f) { return report("displace", f); }); + lap("displace", displaced); + if (!report("displace", 1.0)) { + result.canceled = true; + return result; + } + + // 4. Decimate - export only. A bake needs the face-parent map, which a collapse destroys. + std::vector parent = std::move(sub.face_parent_id); + const size_t displaced_before_decimate = displaced.triangle_count(); + if (mode == PipelineMode::Export) { + std::vector locked; + size_t preserved = 0; + { + if (settings.preserve_untextured && !displaced.exclude_weight.empty()) { + locked.assign(displaced.triangle_count(), 0); + // The corner average, as the displacement stage judges it: after the flip stage's + // per-vertex merge an included face touching the excluded region carries one corner + // at weight 1, and must stay free to collapse and to take colour. + for (size_t t = 0; t < locked.size(); ++t) + locked[t] = (displaced.exclude_weight[t * 3] + displaced.exclude_weight[t * 3 + 1] + + displaced.exclude_weight[t * 3 + 2]) / 3.f > 0.99f ? 1 : 0; + // That includes the faces the paint test switched off: the harvest would re-triangulate + // them into long slivers, which a later bake painted there would refine instead of the + // grid the graded refinement left. + preserved = size_t(std::count(locked.begin(), locked.end(), uint8_t(1))); + } + } + // The budget is what this bake may spend on what it refines. Geometry it only preserves - the + // unpainted surface, and on it the relief of an earlier bake - is counted on top of it: charged + // against the same budget, a second bake over a fresh area had to evict the first one's + // triangles to fit, so every bake after the first came out coarser than the one before. + const size_t target = settings.max_triangles + preserved; + const bool over_budget = displaced.triangle_count() > target; + // Flat faces are harvested whether or not the budget bites. Refinement is driven by the target + // edge length alone, so it leaves as fine a mesh over the flat parts of a texture as over its + // detail, and nothing else removes those: under its budget a bake kept every redundant triangle + // unless the budget was lowered until decimation had to run. Only collapses costing less than + // harvest_tol are taken, so this does not reach the relief. + const bool harvest_only = !over_budget && settings.harvest_flat && displaced.triangle_count() > 0; + if (over_budget || harvest_only) { + // Colour per face on the fine mesh, so colour boundaries become creases the collapse + // respects. Excluded (unpainted) faces take no colour. + std::vector face_color; + if (color_sample) { + const size_t nf = displaced.triangle_count(); + face_color.assign(nf, -1); + const bool have_w = !displaced.exclude_weight.empty(); + tbb::parallel_for(tbb::blocked_range(0, nf), [&](const tbb::blocked_range &r) { + for (size_t t = r.begin(); t < r.end(); ++t) { + if (have_w && (displaced.exclude_weight[t * 3] + displaced.exclude_weight[t * 3 + 1] + + displaced.exclude_weight[t * 3 + 2]) / 3.f > 0.99f) + continue; + const Vec3f &a = displaced.pos[t * 3], &b = displaced.pos[t * 3 + 1], &c = displaced.pos[t * 3 + 2]; + face_color[t] = color_sample((a + b + c) / 3.f, displaced.nrm[t * 3]); + } + }); + } + // Harvesting alone is asked for by handing it the count it already has: nothing is then + // over the target, so the loop only ever pops collapses under the tolerance. + const size_t before = displaced.triangle_count(); + DecimateResult dec = decimate(displaced, over_budget ? target : before, settings.harvest_flat, + settings.harvest_tol, locked, + [&](double f) { return report("decimate", f); }, face_color); + result.locked_over_budget = dec.locked_over_budget; + result.budget_limited = result.simplified = dec.target_cost_detail; + displaced = std::move(dec.geometry); + lap("decimate", displaced, over_budget ? "over budget, simplified" : "flat faces harvested"); + BOOST_LOG_TRIVIAL(info) << "TextureBake decimate: " << before << " -> " << displaced.triangle_count() + << (over_budget ? " (budget " : " (flat harvest, budget ") << target << ")"; + parent.clear(); // no longer meaningful + } + result.triangles_refined = displaced_before_decimate; + result.triangles_budget = target; + if (!report("decimate", 1.0)) { + result.canceled = true; + return result; + } + } + + // 5. Flatten the bed-contact surface. + { + const bool clamped = settings.clamp_below_plate || settings.displace.bottom_angle_limit > 0.f; + if (clamped) + clamp_below_bottom(displaced, bounds.min.z()); + size_t snapped = 0; + if (settings.bottom_snap_tol > 0.0) + snapped = snap_bottom_to_flat(displaced, bounds.min.z(), settings.bottom_snap_tol); + if (clamped || settings.bottom_snap_tol > 0.0) + lap("bottom clamp + snap", displaced, std::to_string(snapped) + " triangles snapped flat"); + } + + // 6. Close the T-junctions decimation left behind. Only meaningful when it ran. + if (mode == PipelineMode::Export && parent.empty()) { + displaced = resolve_t_junctions(displaced); + lap("repair", displaced); + } + + result.geometry = std::move(displaced); + result.face_parent_id = std::move(parent); + return result; +} + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakePipeline.hpp b/src/libslic3r/TextureBake/TextureBakePipeline.hpp new file mode 100644 index 0000000000..e230cf0d49 --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakePipeline.hpp @@ -0,0 +1,156 @@ +#pragma once + +// The bake pipeline: +// +// subdivide -> [regularize -> re-subdivide] -> [paint test] -> [relocate] -> [flip edges] +// -> displace -> [decimate] -> bottom clamp -> bottom snap -> [resolve T-junctions] +// +// Regularization sits between two subdivisions on purpose: it dissolves the slivers refinement +// inherited, which lengthens some edges past the target, and the second pass brings those back. +// Before any subdivision it would have nothing to work on, since the slivers come from refining a +// needle; after a single pass it would leave the mesh coarser than asked for. +// +// Decimation and repair are export-only - decimation drops the output-to-input face mapping a bake +// needs to carry per-face data forward. + +#include +#include +#include + +#include "TextureBakeDecimate.hpp" +#include "TextureBakeDisplace.hpp" +#include "TextureBakeIndex.hpp" +#include "TextureBakeRegularize.hpp" +#include "TextureBakeFlip.hpp" +#include "TextureBakeRelocate.hpp" +#include "TextureBakeRepair.hpp" +#include "TextureBakeSubdivide.hpp" + +namespace Slic3r { + +// Optional step-by-step capture; see TextureBakeDebug.hpp. A pointer, and forward declared, so the +// pipeline header stays free of the mesh types the recorder converts into. +class BakeStageRecorder; + +namespace TextureBake { + +enum class PipelineMode +{ + // Keeps the face-parent mapping; skips decimation and repair. + Bake, + // The full sequence, including decimation and repair. + Export, +}; + +struct PipelineSettings +{ + // Target edge length for the refinement, in mm. + double refine_length = 1.0; + + // Sliver removal between the two subdivision passes. + bool regularize = true; + RegularizeOptions regularize_opts; + // Slightly above the first pass, so it recovers the edges regularization lengthened instead of + // re-refining what it just merged. + double regularize_second_pass_mul = 1.1; + + // Slide vertices onto the height map's own edges before displacing, so a step lands on a mesh + // edge instead of being quantised to wherever the grid fell. + bool relocate = false; + RelocateSettings relocate_opts; + + // Choose each quad's diagonal to follow the height field before displacing, so a step that crosses + // the grid at an angle comes out as a straight wall instead of a sawtooth. See TextureBakeFlip.hpp. + bool flip_edges = true; + FlipSettings flip_opts; + + DisplaceSettings displace; + + // Optional. Asked once per refined face (its centroid, in the soup's coordinates) after the + // refinement stages and before displacement, for faces whose source triangle was included: + // false marks the face as unpainted (no displacement), so paint finer than the input triangles + // is honoured. Faces excluded from the start are never asked. Called from several threads at + // once, so it must be safe to call concurrently. + std::function painted; + + // Optional. Whether the paint comes within a radius of a point, which grades the refinement (see + // subdivide()): only the paint and its surroundings reach refine_length, and the unpainted surface + // further out stays in whole pieces of its own grid. That is what a later bake painted there + // refines, so it meets the same grid a first bake does. Called from several threads at once. + SubdivideWithinFn paint_within; + + // Export mode only. + // What this bake may spend on what it refines. Geometry it only preserves (see preserve_untextured), + // the unpainted surface and an earlier bake's relief on it, is counted on top of it, so that relief + // does not have to be evicted to fit this one. + size_t max_triangles = 750'000; + // Keep removing zero-cost flat faces past the target. Only applies when decimation runs, i.e. when + // the displaced mesh is over the budget - an under-budget mesh is never decimated. + bool harvest_flat = true; + double harvest_tol = DECIMATE_DEFAULT_HARVEST_TOL; + // Lock the untextured region against both regularization and decimation. + bool preserve_untextured = true; + + // Push anything that displaced below the plate back up to it. Downward movement is otherwise left + // alone, so relief on the underside is kept - only what would sink through the plate is stopped. + bool clamp_below_plate = false; + + // Snap vertices within this of the bottom plane onto it. 0 disables. + double bottom_snap_tol = 0.1; + + int safety_cap = SUBDIVIDE_SAFETY_CAP; +}; + +// Stage name and a fraction within it. Returning false cancels the run. +using PipelineProgressFn = std::function; +// Colour class of a point of the surface (a palette index, -1 for none), for the decimation's +// colour-boundary creases. Only consulted when the mesh is over budget. +using ColorSampleFn = std::function; + +struct PipelineResult +{ + TriSoup geometry; + // Output face -> input face. Empty in Export mode, where decimation invalidates it. + std::vector face_parent_id; + bool safety_cap_hit = false; + bool locked_over_budget = false; + size_t collapse_count = 0; + bool canceled = false; + // What the refinement produced, before decimation, and the count it had to fit into (the budget + // plus the preserved geometry). budget_limited says the refined mesh did not fit without collapsing + // more than flat faces: the result carries less of the texture than the resolution asked for, + // which is what a caller warns about. + size_t triangles_refined = 0; + size_t triangles_budget = 0; + bool budget_limited = false; + // The budget coarsened the mesh, so its triangles are no longer the fine, even ones the refinement + // laid down - they can be as large as the features on them, which a caller that post-processes per + // triangle (the colour despeckle) has to know. Harvesting flat faces does not count: what it + // removes carried nothing. Not the same question as "did a collapse run", which is why + // face_parent_id being empty is no longer used to answer it. + bool simplified = false; +}; + +// `debug`, when given and enabled, receives the mesh after every stage that ran - which is the only +// way to tell which stage a bad result came from, since each one rewrites the whole mesh. +PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample, + const PipelineSettings &settings, const DisplaceBounds &bounds, + PipelineMode mode, const std::vector &face_excluded = {}, + const PipelineProgressFn &on_progress = {}, + BakeStageRecorder *debug = nullptr, const ColorSampleFn &color_sample = {}); + +// Snap anything that ended below the model's original bottom back up to it. +void clamp_below_bottom(TriSoup &geometry, float bottom_z); + +// Flatten the bed-contact surface by snapping positions within `tol` of the bottom plane onto it. +// +// Gated, not unconditional: an unconditional band snap also flattens the undersides of texture relief +// near the base, folding them coplanar into the bottom face. Folded faces overlap the plate, so edges +// there pick up four incident faces - non-manifold edges and phantom shells on re-import. All copies +// of a position move together, and the move is rejected if any incident triangle would go degenerate +// or rotate more than about 75 degrees. A real bed-contact sliver rotates by a fraction of a degree +// and still snaps. Returns how many triangles moved. +size_t snap_bottom_to_flat(TriSoup &geometry, float bottom_z, double tol = 0.1); + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeRegularize.cpp b/src/libslic3r/TextureBake/TextureBakeRegularize.cpp new file mode 100644 index 0000000000..3028d8bee9 --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeRegularize.cpp @@ -0,0 +1,393 @@ +#include "TextureBakeRegularize.hpp" + +#include +#include +#include + +#include +#include + +namespace Slic3r { +namespace TextureBake { + +namespace { + +// Vertex-to-triangle lists as intrusive doubly linked lists of corner slots over flat arrays. Slot s +// is corner (triangle * 3 + k), owned by corners[s]. Deleted and moved corners are unlinked, so a +// collapse costs no allocation. +struct SlotLists +{ + std::vector head, next, prev; + + void init(size_t vertex_count, size_t slot_count) + { + head.assign(vertex_count, -1); + next.assign(slot_count, -1); + prev.assign(slot_count, -1); + } + void link(int s, const std::vector &corners) + { + const int v = corners[size_t(s)]; + const int h = head[size_t(v)]; + prev[size_t(s)] = -1; + next[size_t(s)] = h; + if (h != -1) + prev[size_t(h)] = s; + head[size_t(v)] = s; + } + void unlink(int s, const std::vector &corners) + { + const int p = prev[size_t(s)], n = next[size_t(s)]; + if (p != -1) next[size_t(p)] = n; + else head[size_t(corners[size_t(s)])] = n; + if (n != -1) prev[size_t(n)] = p; + } +}; + +} // namespace + +RegularizeResult regularize_mesh(const TriSoup &geometry, const std::vector &face_parent_id, + double max_edge_length, const RegularizeOptions &opts) +{ + RegularizeResult result; + const size_t tri_count = geometry.triangle_count(); + if (tri_count == 0 || max_edge_length <= 0.0) { + result.geometry = geometry; + result.face_parent_id = face_parent_id; + return result; + } + + const double base_max_len_sq = (max_edge_length * opts.slack) * (max_edge_length * opts.slack); + const double aggr_max_len_sq = + (max_edge_length * opts.aggressive_slack) * (max_edge_length * opts.aggressive_slack); + const double extreme_aspect2 = opts.extreme_sliver_aspect * opts.extreme_sliver_aspect; + const double aspect_thr2 = opts.aspect_threshold * opts.aspect_threshold; + + // Double precision: a collapse writes a midpoint back and later collapses read it, so rounding + // would accumulate. + QuantizedPointMap pos_map(WELD_GRID_GEOMETRY, std::min(tri_count * 3, size_t(1) << 22)); + std::vector vert; + std::vector corners(tri_count * 3); + vert.reserve(tri_count); + for (size_t i = 0; i < tri_count * 3; ++i) { + const Vec3f &p = geometry.pos[i]; + const int id = pos_map.get_or_set(p, int(vert.size())); + if (pos_map.inserted()) + vert.push_back(p.cast()); + corners[i] = id; + } + const size_t vert_count = vert.size(); + + std::vector tri_nrm(tri_count, Vec3d::Zero()); + std::vector tri_deleted(tri_count, 0); + result.face_parent_id = face_parent_id; + if (result.face_parent_id.size() != tri_count) + result.face_parent_id.assign(tri_count, 0); + + const auto sq_dist = [&](int a, int b) { return (vert[size_t(a)] - vert[size_t(b)]).squaredNorm(); }; + + const auto recompute_face_normal = [&](size_t t) { + const Vec3d &a = vert[size_t(corners[t * 3])]; + const Vec3d n = (vert[size_t(corners[t * 3 + 1])] - a).cross(vert[size_t(corners[t * 3 + 2])] - a); + const double len = n.norm(); + tri_nrm[t] = (len > 0.0) ? Vec3d(n / len) : Vec3d::Zero(); + }; + for (size_t t = 0; t < tri_count; ++t) + recompute_face_normal(t); + + // Never updated - the normal gate measures against these, so drift cannot compound across rounds. + const std::vector orig_nrm = tri_nrm; + + // Squared thinness, the longest edge over the shortest altitude: + // thinness = lmax / hmin = lmax^2 / (2 * area), so thinness^2 = lmax^4 / |AB x AC|^2 + // + // Not lmax/lmin, which misses what matters here: three near-collinear points can have all edges + // similar, so an edge ratio reports about 2 and the gate skips a triangle with near-zero area + // whose corners sample three unrelated texels. An equilateral scores about 1.15. + const auto tri_aspect_sq = [&](size_t t) -> double { + const Vec3d &a = vert[size_t(corners[t * 3])]; + const Vec3d ab = vert[size_t(corners[t * 3 + 1])] - a; + const Vec3d ac = vert[size_t(corners[t * 3 + 2])] - a; + const Vec3d bc = vert[size_t(corners[t * 3 + 2])] - vert[size_t(corners[t * 3 + 1])]; + const double lmax2 = std::max({ ab.squaredNorm(), ac.squaredNorm(), bc.squaredNorm() }); + const double cross2 = ab.cross(ac).squaredNorm(); + return cross2 > 0.0 ? lmax2 * lmax2 / cross2 : std::numeric_limits::infinity(); + }; + + SlotLists slots; + slots.init(vert_count, tri_count * 3); + for (size_t s = 0; s < tri_count * 3; ++s) + slots.link(int(s), corners); + + // O(1) membership without clearing a set per collapse. + std::vector vert_stamp(vert_count, 0), tri_stamp(tri_count, 0); + uint32_t stamp_gen = 0; + + // Both endpoints of a hard edge are barred from being collapse endpoints, preserving such + // corners exactly while leaving flat-face interiors free. + // + // Skipped when either triangle is an extreme sliver: a sliver's normal is dominated by where its + // far apex sits, so noise pivots it tens of degrees with no feature behind it, and freezing on + // that would lock the very chains this pass exists to dissolve. Genuine features are bordered by + // well-shaped triangles and are unaffected. + std::vector frozen_vert(vert_count, 0); + { + std::vector tri_thin2(tri_count); + for (size_t t = 0; t < tri_count; ++t) + tri_thin2[t] = tri_aspect_sq(t); + QuantizedPointMap edge_seen(1.0, std::min(tri_count * 3, size_t(1) << 22)); + for (size_t t = 0; t < tri_count; ++t) + for (int e = 0; e < 3; ++e) { + const int u = corners[t * 3 + size_t(e)]; + const int v = corners[t * 3 + size_t((e + 1) % 3)]; + const int lo = std::min(u, v), hi = std::max(u, v); + const int other = edge_seen.get_or_set_key(lo, hi, 0, int(t)); + if (edge_seen.inserted()) + continue; + if (tri_thin2[t] > extreme_aspect2 || tri_thin2[size_t(other)] > extreme_aspect2) + continue; + if (tri_nrm[t].dot(tri_nrm[size_t(other)]) < opts.sharp_edge_cos) { + frozen_vert[size_t(u)] = 1; + frozen_vert[size_t(v)] = 1; + } + } + } + + // Exclusion freeze. The weight is constant across a face's corners, so the first one answers. + if (opts.preserve_excluded && !geometry.exclude_weight.empty()) + for (size_t t = 0; t < tri_count; ++t) + if (geometry.exclude_weight[t * 3] > 0.99f) + for (int k = 0; k < 3; ++k) + frozen_vert[size_t(corners[t * 3 + size_t(k)])] = 1; + + std::vector wing_scratch, affected_scratch; + + const auto third_vertex = [&](size_t t, int u, int v) { + const int a = corners[t * 3], b = corners[t * 3 + 1], c = corners[t * 3 + 2]; + if (a != u && a != v) return a; + if (b != u && b != v) return b; + return c; + }; + const auto triangles_sharing_edge = [&](int u, int v) -> std::vector & { + wing_scratch.clear(); + for (int s = slots.head[size_t(u)]; s != -1; s = slots.next[size_t(s)]) { + const size_t t = size_t(s) / 3; + if (tri_deleted[t]) + continue; + if (corners[t * 3] == v || corners[t * 3 + 1] == v || corners[t * 3 + 2] == v) + wing_scratch.push_back(int(t)); + } + return wing_scratch; + }; + + RegularizeRejectStats &stats = result.reject_stats; + + const auto try_collapse = [&](int u, int v) -> bool { + if (u == v) + return false; + if (frozen_vert[size_t(u)] || frozen_vert[size_t(v)]) { ++stats.frozen; return false; } + + // Two wings means a manifold interior edge. + std::vector &wings = triangles_sharing_edge(u, v); + if (wings.size() != 2) { ++stats.wing_count; return false; } + const size_t w0 = size_t(wings[0]), w1 = size_t(wings[1]); + const int apex1 = third_vertex(w0, u, v), apex2 = third_vertex(w1, u, v); + if (apex1 == apex2) { ++stats.folded_apex; return false; } + + // The edge cap loosens if *either* wing is extreme, since the re-subdivision recovers an + // over-long edge. The normal cap needs *both*, which is what protects fillets. + const double w1a = tri_aspect_sq(w0), w2a = tri_aspect_sq(w1); + const bool either_extreme = w1a > extreme_aspect2 || w2a > extreme_aspect2; + const bool both_extreme = w1a > extreme_aspect2 && w2a > extreme_aspect2; + const double eff_max_len_sq = either_extreme ? aggr_max_len_sq : base_max_len_sq; + const double eff_normal_cos = + both_extreme ? opts.aggressive_normal_delta_cos : opts.max_normal_delta_cos; + + // A vertex sharing a triangle with both endpoints, other than the wing apexes, would go + // non-manifold. Stamp one side's neighbours, scan the other against them. + ++stamp_gen; + for (int s = slots.head[size_t(v)]; s != -1; s = slots.next[size_t(s)]) { + const size_t t = size_t(s) / 3; + if (tri_deleted[t]) + continue; + for (int k = 0; k < 3; ++k) + if (const int x = corners[t * 3 + size_t(k)]; x != v) + vert_stamp[size_t(x)] = stamp_gen; + } + for (int s = slots.head[size_t(u)]; s != -1; s = slots.next[size_t(s)]) { + const size_t t = size_t(s) / 3; + if (tri_deleted[t]) + continue; + for (int k = 0; k < 3; ++k) { + const int x = corners[t * 3 + size_t(k)]; + if (x != u && x != v && x != apex1 && x != apex2 && vert_stamp[size_t(x)] == stamp_gen) { + ++stats.link_condition; + return false; + } + } + } + + const Vec3d m = (vert[size_t(u)] + vert[size_t(v)]) * 0.5; + + // Everything using either endpoint; the wings are being deleted. + ++stamp_gen; + affected_scratch.clear(); + for (const int endpoint : { u, v }) + for (int s = slots.head[size_t(endpoint)]; s != -1; s = slots.next[size_t(s)]) { + const size_t t = size_t(s) / 3; + if (tri_deleted[t] || t == w0 || t == w1) + continue; + if (tri_stamp[t] != stamp_gen) { + tri_stamp[t] = stamp_gen; + affected_scratch.push_back(int(t)); + } + } + + // Validate every affected triangle before touching anything. + for (const int ti : affected_scratch) { + const size_t t = size_t(ti); + Vec3d p[3]; + for (int k = 0; k < 3; ++k) { + const int x = corners[t * 3 + size_t(k)]; + p[k] = (x == u || x == v) ? m : vert[size_t(x)]; + } + const double ab2 = (p[1] - p[0]).squaredNorm(); + const double bc2 = (p[2] - p[1]).squaredNorm(); + const double ca2 = (p[0] - p[2]).squaredNorm(); + if (ab2 > eff_max_len_sq || bc2 > eff_max_len_sq || ca2 > eff_max_len_sq) { + ++stats.edge_cap; + return false; + } + const Vec3d n = (p[1] - p[0]).cross(p[2] - p[0]); + const double nlen = n.norm(); + if (nlen <= 0.0) { ++stats.degenerate; return false; } + if ((n / nlen).dot(orig_nrm[t]) < eff_normal_cos) { ++stats.normal_change; return false; } + } + + // Apply: move u to the merged position and redirect every reference to v. + vert[size_t(u)] = m; + for (const size_t w : { w0, w1 }) { + tri_deleted[w] = 1; + for (int k = 0; k < 3; ++k) + slots.unlink(int(w * 3) + k, corners); + } + // A non-wing triangle contains v exactly once, so moving its slots suffices. + for (int s = slots.head[size_t(v)]; s != -1;) { + const int ns = slots.next[size_t(s)]; + slots.unlink(s, corners); + corners[size_t(s)] = u; + slots.link(s, corners); + recompute_face_normal(size_t(s) / 3); + s = ns; + } + for (int s = slots.head[size_t(u)]; s != -1; s = slots.next[size_t(s)]) { + const size_t t = size_t(s) / 3; + if (!tri_deleted[t]) + recompute_face_normal(t); + } + return true; + }; + + // Per-triangle thinness for a round's candidate scan; -1 marks "not a candidate". Scored in + // parallel, since the scan only reads the mesh, then gathered serially in index order. + std::vector round_aspect(tri_count); + for (int round = 0; round < opts.maxrounds; ++round) { + // Rebuilt each round so earlier collapses inform the priorities. + tbb::parallel_for(tbb::blocked_range(0, tri_count), [&](const tbb::blocked_range &r) { + for (size_t t = r.begin(); t < r.end(); ++t) { + round_aspect[t] = -1.0; + if (tri_deleted[t]) + continue; + const int a = corners[t * 3], b = corners[t * 3 + 1], c = corners[t * 3 + 2]; + if (std::min({ sq_dist(a, b), sq_dist(b, c), sq_dist(c, a) }) <= 0.0) + continue; + const double aspect2 = tri_aspect_sq(t); + if (aspect2 >= aspect_thr2) + round_aspect[t] = aspect2; + } + }); + // Worst first; ties keep ascending triangle order so the pass is deterministic. Sorting the + // (thinness, triangle) pairs themselves gives exactly the order the index sort with its + // indirect comparator did, without that comparator's extra lookup on every comparison. + std::vector> cand; + for (size_t t = 0; t < tri_count; ++t) + if (round_aspect[t] >= 0.0) + cand.emplace_back(round_aspect[t], int(t)); + std::sort(cand.begin(), cand.end(), [](const std::pair &x, const std::pair &y) { + return x.first != y.first ? x.first > y.first : x.second < y.second; + }); + + size_t round_collapses = 0; + for (const std::pair &entry : cand) { + const size_t t = size_t(entry.second); + if (tri_deleted[t]) + continue; + const int a = corners[t * 3], b = corners[t * 3 + 1], c = corners[t * 3 + 2]; + // All three edges, shortest first: a sliver straddling a seam has its shortest edge + // crossing it, which the normal gate refuses, while a long edge along one surface + // collapses safely. Trying only the shortest would leave those stuck. + struct Cand { double len2; int u, v; }; + Cand e[3] = { { sq_dist(a, b), a, b }, { sq_dist(b, c), b, c }, { sq_dist(c, a), c, a } }; + std::stable_sort(std::begin(e), std::end(e), + [](const Cand &x, const Cand &y) { return x.len2 < y.len2; }); + if (try_collapse(e[0].u, e[0].v) || try_collapse(e[1].u, e[1].v) || + try_collapse(e[2].u, e[2].v)) + ++round_collapses; + } + result.collapse_count += round_collapses; + if (round_collapses == 0) + break; + } + + // Drop deleted triangles and rebuild the soup. + const bool have_weights = !geometry.exclude_weight.empty(); + const size_t survivors = tri_count - size_t(std::count(tri_deleted.begin(), tri_deleted.end(), uint8_t(1))); + std::vector out_parent; + TriSoup &out = result.geometry; + out_parent.reserve(survivors); + out.pos.reserve(survivors * 3); + if (have_weights) + out.exclude_weight.reserve(survivors * 3); + for (size_t t = 0; t < tri_count; ++t) { + if (tri_deleted[t]) + continue; + for (int k = 0; k < 3; ++k) + out.pos.push_back(vert[size_t(corners[t * 3 + size_t(k)])].cast()); + if (have_weights) { + // Constant across a face's corners. + const float w = geometry.exclude_weight[t * 3]; + out.exclude_weight.insert(out.exclude_weight.end(), { w, w, w }); + } + out_parent.push_back(result.face_parent_id[t]); + } + result.face_parent_id = std::move(out_parent); + + // Rebuilt from the compacted geometry - the collapses moved vertices. + out.nrm.assign(out.pos.size(), Vec3f::Zero()); + { + QuantizedPointMap weld(WELD_GRID_GEOMETRY, out.pos.size()); + std::vector vid(out.pos.size()); + int next = 0; + for (size_t i = 0; i < out.pos.size(); ++i) { + vid[i] = weld.get_or_set(out.pos[i], next); + if (weld.inserted()) + ++next; + } + std::vector vn(size_t(next), Vec3d::Zero()); + for (size_t t = 0; t * 3 < out.pos.size(); ++t) { + const Vec3d a = out.pos[t * 3].cast(); + const Vec3d n = (out.pos[t * 3 + 1].cast() - a).cross(out.pos[t * 3 + 2].cast() - a); + for (int k = 0; k < 3; ++k) + vn[size_t(vid[t * 3 + size_t(k)])] += n; + } + for (size_t i = 0; i < out.pos.size(); ++i) { + const Vec3d &n = vn[size_t(vid[i])]; + const double l = n.norm(); + out.nrm[i] = (l > 0.0) ? Vec3d(n / l).cast() : Vec3f(0.f, 0.f, 1.f); + } + } + return result; +} + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeRegularize.hpp b/src/libslic3r/TextureBake/TextureBakeRegularize.hpp new file mode 100644 index 0000000000..350666783a --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeRegularize.hpp @@ -0,0 +1,73 @@ +#pragma once + +// Sliver removal by short-edge collapse. +// +// Subdivision turns tessellation needles into chains of slivers that are within the edge-length +// budget but still poor triangles. A sliver's three vertices land on three unrelated texels, so the +// relief picks up noise that is an artifact of the tessellation rather than of the image. +// +// A candidate's edge is collapsed to its midpoint only if it passes three gates: no affected +// triangle may exceed the target edge times a slack factor; every affected triangle must keep its +// face normal within a bound of its *original* direction (which is what stops curved surfaces being +// flattened); and the link condition must hold, or the result would be non-manifold. Boundary and +// non-manifold edges are skipped outright. Rounds repeat until one achieves nothing. + +#include +#include + +#include "TextureBakeIndex.hpp" + +namespace Slic3r { +namespace TextureBake { + +struct RegularizeOptions +{ + // Candidate threshold. Set to catch real slivers - chains measure in the hundreds - without + // sweeping up moderate fillet triangles, which sit between 2 and 5. + double aspect_threshold = 5.0; + + // The base tier is loose on purpose: non-sliver boundary collapses must keep succeeding, since + // those give a chain the room to dissolve. A tight base leaves chains worse than before. The + // aggressive tier applies when at least one wing is an extreme sliver. + double slack = 3.0; + double aggressive_slack = 8.0; + + // Thinness above which a wing counts as extreme: longest edge over shortest altitude. + double extreme_sliver_aspect = 8.0; + + // Measured against each triangle's normal from before any collapse ran, so rounds of small + // allowed drift cannot compound into corner damage. Asymmetric two-tier: the loose bound needs + // *both* wings extreme, which matches a needle chain on a curved face but not a sliver beside a + // fillet, so fillets keep the tight bound. + double max_normal_delta_cos = 0.965925826289; // cos(15 degrees) + double aggressive_normal_delta_cos = 0.906307787037; // cos(25 degrees) + + // Vertices on edges sharper than this are frozen, so hard features keep every original vertex. + double sharp_edge_cos = 0.866025403784; // cos(30 degrees) + + int maxrounds = 8; + + // Freeze excluded faces entirely, so untextured geometry is never modified. + bool preserve_excluded = false; +}; + +// Which gate blocked a collapse - the only practical way to tell why a region failed to merge. +struct RegularizeRejectStats +{ + size_t frozen = 0, wing_count = 0, link_condition = 0, edge_cap = 0, normal_change = 0, + degenerate = 0, folded_apex = 0; +}; + +struct RegularizeResult +{ + TriSoup geometry; + std::vector face_parent_id; + size_t collapse_count = 0; + RegularizeRejectStats reject_stats; +}; + +RegularizeResult regularize_mesh(const TriSoup &geometry, const std::vector &face_parent_id, + double max_edge_length, const RegularizeOptions &opts = {}); + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeRelocate.cpp b/src/libslic3r/TextureBake/TextureBakeRelocate.cpp new file mode 100644 index 0000000000..5ea33b353b --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeRelocate.cpp @@ -0,0 +1,205 @@ +#include "TextureBakeRelocate.hpp" + +#include +#include +#include + +#include +#include + +namespace Slic3r { +namespace TextureBake { + +namespace { + +// Any two unit vectors orthogonal to n. Which two does not matter - the gradient is expressed in this +// basis and converted straight back, so the result is basis independent. +void tangent_basis(const Vec3f &n, Vec3f &t1, Vec3f &t2) +{ + const Vec3f a = (std::abs(n.x()) < 0.9f) ? Vec3f(1.f, 0.f, 0.f) : Vec3f(0.f, 1.f, 0.f); + t1 = n.cross(a).normalized(); + t2 = n.cross(t1).normalized(); +} + +} // namespace + +RelocateResult relocate_to_contours(const TriSoup &geometry, const HeightSampleFn &sample, + const RelocateSettings &settings, const std::vector &locked) +{ + RelocateResult result; + result.geometry = geometry; + const size_t count = geometry.pos.size(); + const size_t tri_ct = count / 3; + if (count == 0 || !sample || settings.iterations <= 0) + return result; + + // Weld, so every copy of a position moves together and the mesh cannot come apart. + QuantizedPointMap weld(WELD_GRID_GEOMETRY, std::min(count, size_t(1) << 22)); + std::vector vid(count); + std::vector pos; + for (size_t i = 0; i < count; ++i) { + vid[i] = weld.get_or_set(geometry.pos[i], int(pos.size())); + if (weld.inserted()) + pos.push_back(geometry.pos[i]); + } + const size_t nv = pos.size(); + + // Incident corners per position, CSR style, plus the mean incident edge length that sets the scale + // for both the finite difference and the move limit. + std::vector start(nv + 1, 0); + for (size_t i = 0; i < count; ++i) + ++start[size_t(vid[i]) + 1]; + for (size_t v = 0; v < nv; ++v) + start[v + 1] += start[v]; + std::vector inc(count), cursor(nv, 0); + for (size_t i = 0; i < count; ++i) + inc[start[size_t(vid[i])] + cursor[size_t(vid[i])]++] = uint32_t(i); + + std::vector frozen(nv, 0); + if (!locked.empty()) + for (size_t t = 0; t < tri_ct && t < locked.size(); ++t) + if (locked[t]) + for (int k = 0; k < 3; ++k) + frozen[size_t(vid[t * 3 + size_t(k)])] = 1; + + std::vector edge_len(nv, 0.f), normal_len(nv, 0.f); + std::vector nrm(nv, Vec3f::Zero()); + const auto rebuild_frames = [&]() { + std::fill(nrm.begin(), nrm.end(), Vec3f::Zero()); + std::fill(edge_len.begin(), edge_len.end(), 0.f); + std::vector deg(nv, 0); + for (size_t t = 0; t < tri_ct; ++t) { + const int a = vid[t * 3], b = vid[t * 3 + 1], c = vid[t * 3 + 2]; + const Vec3f fn = (pos[size_t(b)] - pos[size_t(a)]).cross(pos[size_t(c)] - pos[size_t(a)]); + for (int k = 0; k < 3; ++k) { + const int u = vid[t * 3 + size_t(k)], w = vid[t * 3 + size_t((k + 1) % 3)]; + nrm[size_t(u)] += fn; + edge_len[size_t(u)] += (pos[size_t(w)] - pos[size_t(u)]).norm(); + ++deg[size_t(u)]; + } + } + for (size_t v = 0; v < nv; ++v) { + const float l = nrm[v].norm(); + nrm[v] = (l > 0.f) ? Vec3f(nrm[v] / l) : Vec3f(0.f, 0.f, 1.f); + edge_len[v] = deg[v] > 0 ? edge_len[v] / float(deg[v]) : 0.f; + } + }; + rebuild_frames(); + + // The level to snap onto, taken from the height actually present on this patch rather than assumed. + // A texture that never reaches full black or white would otherwise be measured against a range it + // does not occupy. + double h_lo = std::numeric_limits::max(), h_hi = -h_lo; + { + std::vector h0(nv, 0.f); + tbb::parallel_for(tbb::blocked_range(0, nv), [&](const tbb::blocked_range &r) { + for (size_t v = r.begin(); v < r.end(); ++v) + h0[v] = sample(pos[v], nrm[v], nrm[v]); + }); + for (const float h : h0) { + h_lo = std::min(h_lo, double(h)); + h_hi = std::max(h_hi, double(h)); + } + } + const double h_range = h_hi - h_lo; + if (!(h_range > 0.0)) + return result; // a flat height field has no contour to snap to + const double target = h_lo + h_range * settings.contour_level; + // A gradient is worth acting on when the height changes by this much across one edge length. + const double min_grad = h_range * settings.min_gradient_fraction; + + std::vector ever_moved(nv, 0); + + for (int iter = 0; iter < settings.iterations; ++iter) { + std::vector proposal(nv); + std::vector want(nv, 0); + + tbb::parallel_for(tbb::blocked_range(0, nv), [&](const tbb::blocked_range &r) { + for (size_t v = r.begin(); v < r.end(); ++v) { + if (frozen[v] || edge_len[v] <= 0.f) + continue; + const Vec3f n = nrm[v]; + Vec3f t1, t2; + tangent_basis(n, t1, t2); + const float eps = edge_len[v] * float(settings.gradient_step_fraction); + if (eps <= 0.f) + continue; + + // Central differences in the tangent plane. Sampling the field itself, not the mesh, + // so the gradient is the image's, at whatever resolution the mesh happens to have. + const double h = double(sample(pos[v], n, n)); + const double gx = (double(sample(pos[v] + t1 * eps, n, n)) - + double(sample(pos[v] - t1 * eps, n, n))) / (2.0 * double(eps)); + const double gy = (double(sample(pos[v] + t2 * eps, n, n)) - + double(sample(pos[v] - t2 * eps, n, n))) / (2.0 * double(eps)); + const double g2 = gx * gx + gy * gy; + if (g2 <= 0.0) + continue; + // Scale-free test: how much the height changes across one edge, versus the patch range. + if (std::sqrt(g2) * double(edge_len[v]) < min_grad) + continue; + + // Newton step onto the level set h = target, expressed back in 3D. + const double s = -(h - target) / g2; + Vec3f d = t1 * float(s * gx) + t2 * float(s * gy); + const float cap = edge_len[v] * float(settings.max_move_fraction); + const float len = d.norm(); + if (len <= 0.f) + continue; + if (len > cap) + d *= cap / len; + proposal[v] = pos[v] + d; + want[v] = 1; + } + }); + + // Apply one at a time: a move is only valid against the neighbourhood as it stands, and two + // adjacent vertices moving together can invert a triangle neither would have on its own. + size_t applied = 0; + for (size_t v = 0; v < nv; ++v) { + if (!want[v]) + continue; + const Vec3f old = pos[v]; + pos[v] = proposal[v]; + bool ok = true; + for (uint32_t k = start[v]; k < start[v + 1] && ok; ++k) { + const size_t t = size_t(inc[k]) / 3; + const Vec3f &a = pos[size_t(vid[t * 3])]; + const Vec3f n2 = (pos[size_t(vid[t * 3 + 1])] - a).cross(pos[size_t(vid[t * 3 + 2])] - a); + // Compared against the frame this vertex carried before the move: a triangle that + // flips or collapses means the move crossed a neighbour. + if (n2.squaredNorm() <= 0.f || n2.normalized().dot(nrm[v]) < 0.f) + ok = false; + } + if (ok) { + ++applied; + ever_moved[v] = 1; + } else { + pos[v] = old; + ++result.rejected; + } + } + if (applied == 0) + break; + rebuild_frames(); + } + + for (size_t v = 0; v < nv; ++v) + if (ever_moved[v]) + ++result.moved; + + // Write the relocated positions back to every copy, and rebuild the per-face normals. + for (size_t i = 0; i < count; ++i) + result.geometry.pos[i] = pos[size_t(vid[i])]; + for (size_t t = 0; t < tri_ct; ++t) { + Vec3f n = (result.geometry.pos[t * 3 + 1] - result.geometry.pos[t * 3]) + .cross(result.geometry.pos[t * 3 + 2] - result.geometry.pos[t * 3]); + const float len = n.norm(); + n = (len > 0.f) ? Vec3f(n / len) : Vec3f(0.f, 0.f, 1.f); + result.geometry.nrm[t * 3] = result.geometry.nrm[t * 3 + 1] = result.geometry.nrm[t * 3 + 2] = n; + } + return result; +} + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeRelocate.hpp b/src/libslic3r/TextureBake/TextureBakeRelocate.hpp new file mode 100644 index 0000000000..cd674d03ae --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeRelocate.hpp @@ -0,0 +1,72 @@ +#pragma once + +// Tangential relocation: slide vertices along the surface so triangle edges land on the height map's +// own edges, before any displacement happens. +// +// Displacement moves vertices along the normal only, so a step in the height map - the wall of a +// mortar groove, the rim of an embossed shape - is reproduced wherever the triangle grid happens to +// fall, as a staircase quantised to triangle boundaries. Refining further only makes the steps +// smaller; it never straightens them, because the edge in the image still does not coincide with any +// edge in the mesh. +// +// This pass fixes the cause rather than the symptom. For a vertex sitting near a step it takes a +// Newton step onto the contour: with h the sampled height and g its tangential gradient, the move +// +// d = -(h - target) * g / |g|^2 +// +// lands on the level set h = target to first order. The ring of vertices nearest each step therefore +// snaps onto it, the triangle edges between them follow the step, and the displaced result has a +// straight wall instead of a sawtooth. +// +// Vertices in flat regions have no gradient to speak of and are left alone, so the pass costs nothing +// where there is nothing to align. + +#include +#include +#include + +#include "TextureBakeDisplace.hpp" +#include "TextureBakeIndex.hpp" + +namespace Slic3r { +namespace TextureBake { + +struct RelocateSettings +{ + // Passes. Each is a Newton step, so a couple converge for vertices that start reasonably close; + // more mainly helps ones that begin further away. + int iterations = 3; + + // How far a vertex may move in one pass, as a fraction of the mean length of its incident edges. + // Below a half it cannot pass a neighbour, which is what keeps the triangulation valid without + // needing a full topological check. + double max_move_fraction = 0.35; + + // A vertex is only pulled when the height varies enough across its own footprint to mean + // something - as a fraction of the height range over the whole patch. Below this the gradient is + // noise, and chasing it would scramble flat regions. + double min_gradient_fraction = 0.05; + + // The level to snap onto, as a position in the sampled height range: 0.5 is midway between the + // lowest and highest point of the relief, which is where the wall of a step is steepest. + double contour_level = 0.5; + + // Sampling offset for the finite-difference gradient, as a fraction of the local edge length. + double gradient_step_fraction = 0.25; +}; + +struct RelocateResult +{ + TriSoup geometry; + size_t moved = 0; // positions that were relocated at least once + size_t rejected = 0; // moves refused because a triangle would have inverted +}; + +// `locked`, when non-empty, has one entry per input triangle; a vertex touching a locked triangle is +// never moved, so an excluded region keeps its exact vertex positions. +RelocateResult relocate_to_contours(const TriSoup &geometry, const HeightSampleFn &sample, + const RelocateSettings &settings = {}, + const std::vector &locked = {}); + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeRepair.cpp b/src/libslic3r/TextureBake/TextureBakeRepair.cpp new file mode 100644 index 0000000000..438e0ea25d --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeRepair.cpp @@ -0,0 +1,215 @@ +#include "TextureBakeRepair.hpp" + +#include +#include +#include +#include +#include + +namespace Slic3r { +namespace TextureBake { + +namespace { + +inline uint64_t edge_key(int a, int b) +{ + const uint32_t lo = uint32_t(std::min(a, b)), hi = uint32_t(std::max(a, b)); + return (uint64_t(lo) << 32) | uint64_t(hi); +} + +// On the export grid a squared cross product is either 0 (collinear) or at least about 1e-16, the +// smallest real triangle being one grid unit per leg, so this separates the two cleanly. +constexpr double DEGENERATE_AREA_SQ = 1e-18; + +} // namespace + +EdgeDefects count_edge_defects(const TriSoup &geometry, double quant) +{ + EdgeDefects out; + const size_t n = geometry.pos.size(); + out.triangles = n / 3; + QuantizedPointMap vmap(quant, std::min(n, size_t(1) << 22)); + std::vector id(n); + int next = 0; + for (size_t i = 0; i < n; ++i) { + id[i] = vmap.get_or_set(geometry.pos[i], next); + if (vmap.inserted()) + ++next; + } + std::unordered_map counts; + for (size_t t = 0; t + 2 < n; t += 3) { + const int a = id[t], b = id[t + 1], c = id[t + 2]; + if (a == b || b == c || a == c) + continue; + const int tri[3] = { a, b, c }; + for (int e = 0; e < 3; ++e) + ++counts[edge_key(tri[e], tri[(e + 1) % 3])]; + } + for (const auto &[key, c] : counts) { + (void) key; + if (c == 1) ++out.open; + else if (c > 2) ++out.non_manifold; + } + return out; +} + +size_t count_area_slivers(const TriSoup &geometry) +{ + size_t n = 0; + for (size_t t = 0; t + 2 < geometry.pos.size(); t += 3) { + const Vec3d u = (geometry.pos[t + 1] - geometry.pos[t]).cast(); + const Vec3d v = (geometry.pos[t + 2] - geometry.pos[t]).cast(); + // The threshold a slicer applies: area below 1e-12 mm^2. + if (u.cross(v).squaredNorm() < 1e-24) + ++n; + } + return n; +} + +TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts) +{ + const size_t n_tri = geometry.triangle_count(); + const double on_tol2 = opts.on_seg_tol * opts.on_seg_tol; + const double Q = opts.weld_quant; + + // Snapped, not just welded: keeping unrounded coordinates lets a thin triangle pass the + // degeneracy test here and then collapse to collinear once the file is written, punching the very + // hole this pass prevents. Snapping makes the check see what will be written. + QuantizedPointMap vmap(Q, std::min(n_tri * 3, size_t(1) << 22)); + std::vector vert; + std::vector vid(n_tri * 3); + for (size_t i = 0; i < n_tri * 3; ++i) { + const Vec3f &p = geometry.pos[i]; + const int id = vmap.get_or_set(p, int(vert.size())); + if (vmap.inserted()) + vert.emplace_back(double(grid_round(double(p.x()) * Q)) / Q, + double(grid_round(double(p.y()) * Q)) / Q, + double(grid_round(double(p.z()) * Q)) / Q); + vid[i] = id; + } + + // Dropped: faces whose corners welded together, and needles - distinct but collinear on this + // grid. A needle reads as watertight yet is deleted downstream, and dropping it leaves exactly + // the on-edge-vertex topology the pass below closes. + std::vector> faces; + faces.reserve(n_tri); + for (size_t t = 0; t < n_tri; ++t) { + const int a = vid[t * 3], b = vid[t * 3 + 1], c = vid[t * 3 + 2]; + if (a == b || b == c || a == c) + continue; + const Vec3d u = vert[size_t(b)] - vert[size_t(a)]; + const Vec3d w = vert[size_t(c)] - vert[size_t(a)]; + if (u.cross(w).squaredNorm() < DEGENERATE_AREA_SQ) + continue; + faces.push_back({ a, b, c }); + } + + for (int iter = 0; iter < opts.max_iters; ++iter) { + std::unordered_map e_count; + for (const auto &f : faces) + for (int e = 0; e < 3; ++e) + ++e_count[edge_key(f[size_t(e)], f[size_t((e + 1) % 3)])]; + + std::unordered_set bverts; + for (const auto &[key, c] : e_count) { + if (c != 1) + continue; + bverts.insert(int(uint32_t(key >> 32))); + bverts.insert(int(uint32_t(key & 0xFFFFFFFFu))); + } + if (bverts.empty()) + break; + const std::vector bv(bverts.begin(), bverts.end()); + + struct Split { int a, b; std::vector mids; }; + std::unordered_map splits; + for (size_t fi = 0; fi < faces.size(); ++fi) { + const auto &f = faces[fi]; + for (int e = 0; e < 3; ++e) { + const int a = f[size_t(e)], b = f[size_t((e + 1) % 3)]; + if (e_count[edge_key(a, b)] != 1) + continue; // only a boundary edge carries an unresolved T-junction + const Vec3d A = vert[size_t(a)]; + const Vec3d ev = vert[size_t(b)] - A; + const double elen2 = ev.squaredNorm(); + if (elen2 < 1e-20) + continue; + std::vector> found; + for (const int c : bv) { + if (c == a || c == b) + continue; + const Vec3d cv = vert[size_t(c)] - A; + const double tp = cv.dot(ev) / elen2; + if (tp <= 1e-4 || tp >= 1.0 - 1e-4) + continue; // strictly between the ends + if ((cv - ev * tp).squaredNorm() < on_tol2) + found.emplace_back(tp, c); + } + if (!found.empty()) { + std::sort(found.begin(), found.end(), + [](const auto &x, const auto &y) { return x.first < y.first; }); + Split sp{ a, b, {} }; + for (const auto &m : found) + sp.mids.push_back(m.second); + splits.emplace(fi, std::move(sp)); + break; // one site per face per pass; iteration handles cascades + } + } + } + if (splits.empty()) + break; + + std::vector> next; + next.reserve(faces.size() + splits.size() * 2); + for (size_t fi = 0; fi < faces.size(); ++fi) { + const auto it = splits.find(fi); + if (it == splits.end()) { + next.push_back(faces[fi]); + continue; + } + const auto &f = faces[fi]; + const auto &sp = it->second; + const int apex = (f[0] != sp.a && f[0] != sp.b) ? f[0] + : (f[1] != sp.a && f[1] != sp.b) ? f[1] + : f[2]; + // Walk the base the way the face already traverses it, so the winding survives. + bool dir_ab = false; + for (int e = 0; e < 3; ++e) + if (f[size_t(e)] == sp.a && f[size_t((e + 1) % 3)] == sp.b) { + dir_ab = true; + break; + } + std::vector seq; + if (dir_ab) { + seq.push_back(sp.a); + seq.insert(seq.end(), sp.mids.begin(), sp.mids.end()); + seq.push_back(sp.b); + } else { + seq.push_back(sp.b); + seq.insert(seq.end(), sp.mids.rbegin(), sp.mids.rend()); + seq.push_back(sp.a); + } + for (size_t s = 0; s + 1 < seq.size(); ++s) + next.push_back({ seq[s], seq[s + 1], apex }); + } + faces.swap(next); + } + + TriSoup out; + out.pos.reserve(faces.size() * 3); + out.nrm.reserve(faces.size() * 3); + for (const auto &f : faces) { + const Vec3f a = vert[size_t(f[0])].cast(); + const Vec3f b = vert[size_t(f[1])].cast(); + const Vec3f c = vert[size_t(f[2])].cast(); + Vec3f nrm = (b - a).cross(c - a); + const float len = nrm.norm(); + nrm = (len > 0.f) ? Vec3f(nrm / len) : Vec3f(0.f, 0.f, 1.f); + out.pos.insert(out.pos.end(), { a, b, c }); + out.nrm.insert(out.nrm.end(), { nrm, nrm, nrm }); + } + return out; +} + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeRepair.hpp b/src/libslic3r/TextureBake/TextureBakeRepair.hpp new file mode 100644 index 0000000000..35e9f50b06 --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeRepair.hpp @@ -0,0 +1,48 @@ +#pragma once + +// T-junction resolution and edge-defect accounting. +// +// Decimation can collapse a long edge whose interior still carries neighbouring triangles' vertices. +// Those then sit *on* an edge rather than at an end: watertight vertex-for-vertex, but the edge has +// one incident face on one side, which a slicer reads as an open boundary. This splits the offending +// face into a fan so every on-edge vertex becomes a real corner. + +#include +#include + +#include "TextureBakeIndex.hpp" + +namespace Slic3r { +namespace TextureBake { + +struct EdgeDefects +{ + size_t open = 0, non_manifold = 0, triangles = 0; +}; + +// Welds at the export grid first: counting on the un-snapped mesh reports defects the file does not +// have and misses ones it does. +EdgeDefects count_edge_defects(const TriSoup &geometry, double quant = WELD_GRID_EXPORT); + +// Triangles a slicer would drop as degenerate. Each one, removed, punches a hole - so a non-zero +// count means watertight only on paper. +size_t count_area_slivers(const TriSoup &geometry); + +struct RepairOptions +{ + // Coordinates are snapped onto this grid, matching the precision files are written with. + double weld_quant = WELD_GRID_EXPORT; + + // How far off an edge a vertex may sit and still count as on it. Well above the harvest + // tolerance, since harvesting leaves a region flat only to within that, making a collapsed edge a + // chord the on-edge vertices deviate from by about as much. Still far below the weld grid. + double on_seg_tol = 0.02; + + // Splitting one face can expose another behind it, so the pass cascades. + int max_iters = 16; +}; + +TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts = {}); + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeSubdivide.cpp b/src/libslic3r/TextureBake/TextureBakeSubdivide.cpp new file mode 100644 index 0000000000..6b882b1fc8 --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeSubdivide.cpp @@ -0,0 +1,484 @@ +#include "TextureBakeSubdivide.hpp" + +#include +#include +#include + +#include +#include +#include + +namespace Slic3r { +namespace TextureBake { + +namespace { + +double edge_len_sq(const VertStore &v, int a, int b) +{ + return (v.pos[size_t(a)] - v.pos[size_t(b)]).squaredNorm(); +} + +// Both indexers accumulate raw, area-weighted cross products and normalise once at the end. +void normalize_store_normals(VertStore &verts) +{ + for (Vec3d &n : verts.nrm) { + const double len = n.norm(); + n = (len > 0.0) ? Vec3d(n / len) : Vec3d(0.0, 0.0, 1.0); + } +} + +// Keyed by the raw parent-vertex pair rather than by position: two sharp-edge copies of one point +// need their own midpoints, since their normals differ even though the position does not. +int get_midpoint(VertStore &verts, QuantizedPointMap &cache, int a, int b, + QuantizedPointMap *pos_canon_map) +{ + const int lo = std::min(a, b), hi = std::max(a, b); + if (const int cached = cache.get_key(lo, hi, 0); cached != -1) + return cached; + + const Vec3d m = (verts.pos[size_t(a)] + verts.pos[size_t(b)]) * 0.5; + Vec3d n = verts.nrm[size_t(a)] + verts.nrm[size_t(b)]; + const double nl = n.norm(); + n = (nl > 0.0) ? Vec3d(n / nl) : verts.nrm[size_t(a)]; + + const int idx = verts.push(m, n); + if (!verts.wgt.empty()) + verts.wgt.push_back((verts.wgt[size_t(a)] + verts.wgt[size_t(b)]) * 0.5); + if (!verts.canon.empty() && pos_canon_map != nullptr) + verts.canon.push_back(pos_canon_map->get_or_set(float(m.x()), float(m.y()), float(m.z()), idx)); + + cache.get_or_set_key(lo, hi, 0, idx); + return idx; +} + +struct PassResult +{ + std::vector indices; + std::vector face_excluded; + std::vector face_parent_id; + bool changed = false; + bool capped = false; +}; + +// Three steps, so that no T-junction can appear: +// 1. Mark every too-long edge globally, so both triangles on a shared edge decide alike. +// 1.5 Predict the exact resulting count from the marks (0->1, 1->2, 2->3, 3->4) and abort the +// *whole* pass if it exceeds the cap - a partial pass leaves split parents beside unsplit +// neighbours, the very crack step 1 prevents. +// 2. Rebuild, allocating once at the now-known size. +PassResult subdivide_pass(VertStore &verts, const std::vector &indices, double max_edge_length, + int safety_cap, const std::vector &face_excluded, + QuantizedPointMap *pos_canon_map, const std::vector &face_parent_id, + const SubdivideWithinFn &within) +{ + PassResult out; + const double max_sq = max_edge_length * max_edge_length; + const size_t tri_count = indices.size() / 3; + const bool have_canon = !verts.canon.empty(); + + // Sized from the pass rather than grown from 64 k: a fine pass marks on the order of one edge per + // triangle, and growing to that by doubling rehashes the whole table a dozen times. + const size_t expect = std::max(size_t(1) << 16, tri_count); + QuantizedPointMap mid_cache(1.0, expect); + QuantizedPointMap split_edges(1.0, expect); + + // With canonical ids the key is the canonical *position* id, so split copies either side of a + // sharp edge see one another's decision; without them the vertex index serves. + const auto key_of = [&](int v) -> int64_t { return have_canon ? verts.canon[size_t(v)] : v; }; + const auto mark_edge = [&](int a, int b) { + const int64_t u = key_of(a), v = key_of(b); + if (u < v) split_edges.get_or_set_key(u, v, 0, 1); + else split_edges.get_or_set_key(v, u, 0, 1); + }; + const auto is_marked = [&](int a, int b) { + const int64_t u = key_of(a), v = key_of(b); + return (u < v ? split_edges.get_key(u, v, 0) : split_edges.get_key(v, u, 0)) != -1; + }; + + // Graded, an edge splits while it is longer than max_edge_length + SUBDIVIDE_GRADE * (d - len/2), + // d its midpoint's distance from the region; less half the length bounds the edge's own distance + // from below, so the test errs toward splitting. Solved for d, that is the region coming within the + // radius below. It reads the positions alone, so two triangles sharing the edge agree. + const auto too_long = [&](int a, int b) { + const double len_sq = edge_len_sq(verts, a, b); + if (len_sq <= max_sq) + return false; + if (!within) + return true; + const double len = std::sqrt(len_sq); + return within((verts.pos[size_t(a)] + verts.pos[size_t(b)]) * 0.5, + 0.5 * len + (len - max_edge_length) / SUBDIVIDE_GRADE); + }; + + // Step 1. An excluded triangle marks none of its own edges while left alone, so its interior never + // refines; its boundary edges are still marked by an included neighbour, and ungraded it follows + // that split. Graded, one the refinement reaches marks its own too-long edges too, and so on + // outward. An earlier bake's relief meets the paint along edges already at the target length, so + // it is never reached and stays as it was. + std::vector want(tri_count, 0); + tbb::parallel_for(tbb::blocked_range(0, tri_count), [&](const tbb::blocked_range &range) { + for (size_t t = range.begin(); t < range.end(); ++t) { + const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2]; + want[t] = uint8_t(too_long(a, b)) | uint8_t(too_long(b, c) << 1) | uint8_t(too_long(c, a) << 2); + } + }); + const auto mark_wanted = [&](size_t t) { + const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2]; + if (want[t] & 1) mark_edge(a, b); + if (want[t] & 2) mark_edge(b, c); + if (want[t] & 4) mark_edge(c, a); + }; + std::vector unreached; + for (size_t t = 0; t < tri_count; ++t) + if (face_excluded.empty() || !face_excluded[t]) + mark_wanted(t); + else if (within && want[t] != 0) + unreached.push_back(t); + // Marks only accumulate, so the order the reached triangles are taken in does not matter. + for (size_t n = 0; n != unreached.size();) { + n = unreached.size(); + unreached.erase(std::remove_if(unreached.begin(), unreached.end(), [&](size_t t) { + const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2]; + const bool reached = is_marked(a, b) || is_marked(b, c) || is_marked(c, a); + if (reached) + mark_wanted(t); + return reached; + }), unreached.end()); + } + if (split_edges.size() == 0) { + out.indices = indices; + out.face_excluded = face_excluded; + out.face_parent_id = face_parent_id; + return out; // changed stays false: nothing left to refine + } + + // Step 1.5. Read-only against the finished mark set, so it runs in parallel - and it keeps each + // triangle's three marks (bit 0 = ab, 1 = bc, 2 = ca), so the serial rebuild below reads a byte + // instead of probing the hash map three more times per triangle. + std::vector tri_marks(tri_count); + const size_t predicted = tbb::parallel_reduce( + tbb::blocked_range(0, tri_count), size_t(0), + [&](const tbb::blocked_range &range, size_t acc) { + for (size_t t = range.begin(); t < range.end(); ++t) { + const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2]; + const uint8_t m = uint8_t(is_marked(a, b)) | uint8_t(is_marked(b, c) << 1) | + uint8_t(is_marked(c, a) << 2); + tri_marks[t] = m; + const int n = (m & 1) + ((m >> 1) & 1) + ((m >> 2) & 1); + acc += (n == 0) ? 1 : size_t(n + 1); + } + return acc; + }, + std::plus()); + if (predicted > size_t(safety_cap)) { + out.indices = indices; + out.face_excluded = face_excluded; + out.face_parent_id = face_parent_id; + out.capped = true; + return out; // coarser than asked for, but watertight + } + + // Step 2. + out.indices.resize(predicted * 3); + if (!face_excluded.empty()) + out.face_excluded.resize(predicted); + if (!face_parent_id.empty()) + out.face_parent_id.resize(predicted); + size_t wi = 0, fi = 0; + const auto emit_face_data = [&](uint8_t excl, int pid, int times) { + for (int k = 0; k < times; ++k) { + if (!out.face_excluded.empty()) out.face_excluded[fi] = excl; + if (!out.face_parent_id.empty()) out.face_parent_id[fi] = pid; + ++fi; + } + }; + const auto emit = [&](int x, int y, int z) { + out.indices[wi++] = x; out.indices[wi++] = y; out.indices[wi++] = z; + }; + + for (size_t t = 0; t < tri_count; ++t) { + const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2]; + const uint8_t excl = face_excluded.empty() ? uint8_t(0) : face_excluded[t]; + const int pid = face_parent_id.empty() ? 0 : face_parent_id[t]; + const bool s_ab = (tri_marks[t] & 1) != 0, s_bc = (tri_marks[t] & 2) != 0, s_ca = (tri_marks[t] & 4) != 0; + const int n = int(s_ab) + int(s_bc) + int(s_ca); + + if (n == 0) { + emit(a, b, c); + emit_face_data(excl, pid, 1); + } else if (n == 3) { + // a + // / \ + // mCA-mAB + // / \ / \ + // c--mBC--b + const int m_ab = get_midpoint(verts, mid_cache, a, b, pos_canon_map); + const int m_bc = get_midpoint(verts, mid_cache, b, c, pos_canon_map); + const int m_ca = get_midpoint(verts, mid_cache, c, a, pos_canon_map); + emit(a, m_ab, m_ca); + emit(m_ab, b, m_bc); + emit(m_ca, m_bc, c); + emit(m_ab, m_bc, m_ca); + emit_face_data(excl, pid, 4); + } else if (n == 1) { + if (s_ab) { + const int m = get_midpoint(verts, mid_cache, a, b, pos_canon_map); + emit(a, m, c); + emit(m, b, c); + } else if (s_bc) { + const int m = get_midpoint(verts, mid_cache, b, c, pos_canon_map); + emit(a, b, m); + emit(a, m, c); + } else { + const int m = get_midpoint(verts, mid_cache, c, a, pos_canon_map); + emit(a, b, m); + emit(m, b, c); + } + emit_face_data(excl, pid, 2); + } else { + // A corner triangle on the untouched-edge vertex, then the remaining quadrilateral split + // along the midpoint-to-midpoint diagonal, which keeps the winding consistent. + // + // A sliver parent propagates: that inner diagonal inherits half the short edge and hands + // the sliver to two children per pass. No better diagonal exists - one avoiding the + // midpoints must pass through one of them, giving a zero-area triangle. Regularization + // removes such slivers before the mesh reaches here. + if (!s_ab) { // fan from c + const int m_bc = get_midpoint(verts, mid_cache, b, c, pos_canon_map); + const int m_ca = get_midpoint(verts, mid_cache, c, a, pos_canon_map); + emit(a, b, m_bc); + emit(a, m_bc, m_ca); + emit(c, m_ca, m_bc); + } else if (!s_bc) { // fan from a + const int m_ab = get_midpoint(verts, mid_cache, a, b, pos_canon_map); + const int m_ca = get_midpoint(verts, mid_cache, c, a, pos_canon_map); + emit(a, m_ab, m_ca); + emit(m_ab, b, c); + emit(m_ab, c, m_ca); + } else { // fan from b + const int m_ab = get_midpoint(verts, mid_cache, a, b, pos_canon_map); + const int m_bc = get_midpoint(verts, mid_cache, b, c, pos_canon_map); + emit(b, m_bc, m_ab); + emit(a, m_ab, m_bc); + emit(a, m_bc, c); + } + emit_face_data(excl, pid, 3); + } + } + + out.changed = true; + return out; +} + +} // namespace + +IndexedMesh to_indexed_fast(const TriSoup &geometry) +{ + // Preview path: a plain position merge - no clustering, no sharp-edge splitting, no canonical ids. + IndexedMesh out; + const size_t n = geometry.pos.size(); + QuantizedPointMap vert_map(WELD_GRID_GEOMETRY, std::min(n, size_t(1) << 22)); + out.indices.resize(n); + const bool has_w = !geometry.exclude_weight.empty(); + + for (size_t i = 0; i < n; ++i) { + const Vec3f &p = geometry.pos[i]; + const Vec3f nf = geometry.nrm.empty() ? Vec3f(0.f, 0.f, 1.f) : geometry.nrm[i]; + const int idx = vert_map.get_or_set(p, int(out.verts.count())); + if (vert_map.inserted()) { + out.verts.push(p.cast(), nf.cast()); + if (has_w) + out.verts.wgt.push_back(double(geometry.exclude_weight[i])); + } else { + out.verts.nrm[size_t(idx)] += nf.cast(); + // Merge exclusion by maximum: any excluded face marks the shared vertex. + if (has_w && double(geometry.exclude_weight[i]) > out.verts.wgt[size_t(idx)]) + out.verts.wgt[size_t(idx)] = double(geometry.exclude_weight[i]); + } + out.indices[i] = idx; + } + normalize_store_normals(out.verts); + return out; +} + +IndexedMesh to_indexed(const TriSoup &geometry) +{ + // Export path. Two vertices at one position merge only when their face normals agree to within + // SUBDIVIDE_SHARP_ANGLE_DEG, which keeps a cylinder from faceting while stopping a cube's edge + // normal from leaking into the flat face interiors as subdivision carries it inward. + IndexedMesh out; + out.has_canon = true; + const size_t n = geometry.pos.size(); + const bool has_w = !geometry.exclude_weight.empty(); + const double sharp_cos = std::cos(SUBDIVIDE_SHARP_ANGLE_DEG * M_PI / 180.0); + + // Per-face normals: unit for the angle test, raw for the area-weighted accumulation. + std::vector face_unit(n), face_raw(n); + tbb::parallel_for(tbb::blocked_range(0, n / 3), [&](const tbb::blocked_range &range) { + for (size_t f = range.begin(); f < range.end(); ++f) { + const size_t t = f * 3; + const Vec3d a = geometry.pos[t].cast(); + const Vec3d r = (geometry.pos[t + 1].cast() - a).cross( + geometry.pos[t + 2].cast() - a); + const double len = r.norm(); + const Vec3d u = (len > 0.0) ? Vec3d(r / len) : Vec3d(0.0, 0.0, 1.0); + for (int v = 0; v < 3; ++v) { + face_unit[t + size_t(v)] = u; + face_raw[t + size_t(v)] = r; + } + } + }); + + out.indices.resize(n); + out.pos_canon_map = QuantizedPointMap(WELD_GRID_GEOMETRY, std::min(n, size_t(1) << 22)); + struct Cluster { int idx; Vec3d fn_unit; }; + std::unordered_map> clusters_by_canon; + + for (size_t i = 0; i < n; ++i) { + const Vec3f &p = geometry.pos[i]; + // The first vertex at a position becomes its canonical id; later split copies share it. + const int canon_id = out.pos_canon_map.get_or_set(p, int(out.verts.count())); + const bool fresh_position = out.pos_canon_map.inserted(); + + const auto add_vertex = [&](int canon) { + const int idx = out.verts.push(p.cast(), face_raw[i]); + if (has_w) + out.verts.wgt.push_back(double(geometry.exclude_weight[i])); + out.verts.canon.push_back(canon); + return idx; + }; + + if (fresh_position) { + const int idx = add_vertex(canon_id); + clusters_by_canon[canon_id].push_back({ idx, face_unit[i] }); + out.indices[i] = idx; + continue; + } + + std::vector &clusters = clusters_by_canon[canon_id]; + bool matched = false; + for (Cluster &cl : clusters) { + if (cl.fn_unit.dot(face_unit[i]) < sharp_cos) + continue; + out.verts.nrm[size_t(cl.idx)] += face_raw[i]; + if (has_w && double(geometry.exclude_weight[i]) > out.verts.wgt[size_t(cl.idx)]) + out.verts.wgt[size_t(cl.idx)] = double(geometry.exclude_weight[i]); + // Track the running average, so gradual curvature stays in one cluster instead of + // fragmenting when a distant face exceeds the threshold against the seed's fixed normal. + cl.fn_unit += face_unit[i]; + if (const double rl = cl.fn_unit.norm(); rl > 0.0) + cl.fn_unit /= rl; + out.indices[i] = cl.idx; + matched = true; + break; + } + if (!matched) { + // A sharp-edge split: a new vertex at the same position, sharing its canonical id. + const int idx = add_vertex(canon_id); + clusters.push_back({ idx, face_unit[i] }); + out.indices[i] = idx; + } + } + + normalize_store_normals(out.verts); + return out; +} + +TriSoup to_non_indexed(const VertStore &verts, const std::vector &indices, + const std::vector &face_excluded) +{ + TriSoup out; + const size_t tri_count = indices.size() / 3; + out.pos.resize(tri_count * 3); + out.nrm.resize(tri_count * 3); + const bool want_weights = !face_excluded.empty() || !verts.wgt.empty(); + if (want_weights) + out.exclude_weight.resize(tri_count * 3); + + // Each triangle writes only its own three slots, so this is a plain parallel gather. + tbb::parallel_for(tbb::blocked_range(0, tri_count), [&](const tbb::blocked_range &r) { + for (size_t t = r.begin(); t < r.end(); ++t) { + // The per-face flag, not the interpolated weight: merging by maximum can push an *included* + // face's corners to 1 when it borders two excluded neighbours, wrongly excluding it. + const bool have_face_flag = !face_excluded.empty(); + const float face_w = have_face_flag ? (face_excluded[t] ? 1.f : 0.f) : 0.f; + for (int v = 0; v < 3; ++v) { + const size_t vidx = size_t(indices[t * 3 + size_t(v)]); + out.pos[t * 3 + size_t(v)] = verts.pos[vidx].cast(); + out.nrm[t * 3 + size_t(v)] = verts.nrm[vidx].cast(); + if (want_weights) + out.exclude_weight[t * 3 + size_t(v)] = + have_face_flag ? face_w : float(verts.wgt[vidx]); + } + } + }); + return out; +} + +SubdivideResult subdivide(const TriSoup &geometry, double max_edge_length, + const std::vector &face_excluded, bool fast, int safety_cap, + const SubdivideProgressFn &on_progress, const SubdivideWithinFn &within) +{ + SubdivideResult result; + if (geometry.empty() || max_edge_length <= 0.0) { + result.geometry = geometry; + return result; + } + + IndexedMesh indexed = fast ? to_indexed_fast(geometry) : to_indexed(geometry); + QuantizedPointMap *canon_map = indexed.has_canon ? &indexed.pos_canon_map : nullptr; + + const size_t initial_tris = indexed.indices.size() / 3; + std::vector current_indices = std::move(indexed.indices); // nothing reads it again + std::vector current_excluded = face_excluded; + std::vector current_parent(initial_tris); + for (size_t i = 0; i < initial_tris; ++i) + current_parent[i] = int(i); + + for (int iter = 0; iter < SUBDIVIDE_MAX_ITERATIONS; ++iter) { + if (current_indices.size() / 3 >= size_t(safety_cap)) { + result.safety_cap_hit = true; + break; + } + + PassResult pass = subdivide_pass(indexed.verts, current_indices, max_edge_length, safety_cap, + current_excluded, canon_map, current_parent, within); + current_indices = std::move(pass.indices); + if (!pass.face_excluded.empty()) + current_excluded = std::move(pass.face_excluded); + if (!pass.face_parent_id.empty()) + current_parent = std::move(pass.face_parent_id); + if (pass.capped || current_indices.size() / 3 >= size_t(safety_cap)) + result.safety_cap_hit = true; + + if (on_progress) { + // Reported after the pass, so the value falls each iteration instead of lagging a step. + // A max is order-independent, so the scan reduces in parallel to the same value. + const double max_edge_sq = tbb::parallel_reduce( + tbb::blocked_range(0, current_indices.size() / 3), 0.0, + [&](const tbb::blocked_range &r, double acc) { + for (size_t f = r.begin(); f < r.end(); ++f) { + const int a = current_indices[f * 3], b = current_indices[f * 3 + 1], + c = current_indices[f * 3 + 2]; + acc = std::max({ acc, edge_len_sq(indexed.verts, a, b), edge_len_sq(indexed.verts, b, c), + edge_len_sq(indexed.verts, c, a) }); + } + return acc; + }, + [](double x, double y) { return std::max(x, y); }); + if (!on_progress(std::min(0.95, double(iter + 1) / SUBDIVIDE_MAX_ITERATIONS), + current_indices.size() / 3, std::sqrt(max_edge_sq))) + break; // whole passes only, so what we have is still crack-free + } + + if (!pass.changed || result.safety_cap_hit) + break; + } + + result.geometry = to_non_indexed(indexed.verts, current_indices, current_excluded); + result.face_parent_id = std::move(current_parent); + return result; +} + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeSubdivide.hpp b/src/libslic3r/TextureBake/TextureBakeSubdivide.hpp new file mode 100644 index 0000000000..743c74eb49 --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeSubdivide.hpp @@ -0,0 +1,96 @@ +#pragma once + +// Adaptive subdivision to a target edge length, by global marked-edge (red-green) refinement rather +// than longest-edge bisection. Marking is global, so two triangles sharing an edge always agree and +// the result is crack-free by construction. A triangle is rebuilt from its marked-edge count: 0 +// keeps, 1 bisects, 2 fans into three, 3 does the regular 1->4 split. +// +// The 1->4 case is what keeps the tessellation regular - its children are similar to the parent. An +// irregular one shows up after displacement as adjacent triangles tilting alternately, i.e. noise. + +#include +#include + +#include "TextureBakeIndex.hpp" + +namespace Slic3r { +namespace TextureBake { + +// Memory guard for the stages downstream. At roughly 145 bytes per triangle this is about 2.9 GB. +static constexpr int SUBDIVIDE_SAFETY_CAP = 16'000'000; + +// Vertices at one position stay separate when their faces disagree by more than this: a cube keeps +// hard edges, a cylinder keeps averaged ones. +static constexpr double SUBDIVIDE_SHARP_ANGLE_DEG = 30.0; + +// A depth bound, not a work bound: the loop stops as soon as a pass changes nothing. +static constexpr int SUBDIVIDE_MAX_ITERATIONS = 12; + +// Graded refinement: millimetres of edge length allowed per millimetre of distance from the region. +static constexpr double SUBDIVIDE_GRADE = 1.0; + +// Built by the indexers, appended to by the passes. Double precision so repeated midpointing does +// not drift. +struct VertStore +{ + std::vector pos; + std::vector nrm; + std::vector wgt; // exclusion weights; empty when the caller supplied none + std::vector canon; // canonical position ids; empty in fast mode + + size_t count() const { return pos.size(); } + int push(const Vec3d &p, const Vec3d &n) + { + const int idx = int(pos.size()); + pos.push_back(p); + nrm.push_back(n); + return idx; + } +}; + +struct IndexedMesh +{ + VertStore verts; + std::vector indices; // 3 per triangle + QuantizedPointMap pos_canon_map{ WELD_GRID_GEOMETRY, 256 }; + bool has_canon = false; +}; + +// Fraction, triangle count, longest remaining edge. Returning false cancels; what comes back is +// still watertight, because passes apply whole or not at all. +using SubdivideProgressFn = std::function; +// Whether the region being refined comes within `radius` mm of `point`. Called from several threads +// at once. +using SubdivideWithinFn = std::function; + +struct SubdivideResult +{ + TriSoup geometry; + // Output triangle -> input triangle it descends from, so per-face data survives with no remap. + std::vector face_parent_id; + bool safety_cap_hit = false; +}; + +// `face_excluded`: one entry per input triangle; non-zero means its interior is not refined unless the +// graded refinement reaches it (see `within`). Its edges still split when an included neighbour marks +// them, so no T-junction appears at the boundary. +// `fast` selects the cheap position-only indexer for previews. +// `within`, when given, grades the refinement: an edge splits only while it is longer than +// max_edge_length plus SUBDIVIDE_GRADE times its distance from the region. Away from the region the +// triangles are then left as whole pieces of the input's own 1->4 grid rather than refined throughout. +// An excluded triangle the refinement reaches - one of its edges splits - splits the same graded way, +// instead of fanning from its far corner to follow the edge it shares with an included one. +SubdivideResult subdivide(const TriSoup &geometry, double max_edge_length, + const std::vector &face_excluded = {}, bool fast = false, + int safety_cap = SUBDIVIDE_SAFETY_CAP, + const SubdivideProgressFn &on_progress = {}, + const SubdivideWithinFn &within = {}); + +// Displacement needs the same welding and sharp-edge clustering. +IndexedMesh to_indexed(const TriSoup &geometry); +IndexedMesh to_indexed_fast(const TriSoup &geometry); +TriSoup to_non_indexed(const VertStore &verts, const std::vector &indices, + const std::vector &face_excluded); + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureDisplacement.cpp b/src/libslic3r/TextureDisplacement.cpp new file mode 100644 index 0000000000..862c6c9044 --- /dev/null +++ b/src/libslic3r/TextureDisplacement.cpp @@ -0,0 +1,5044 @@ +#include "TextureDisplacement.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include + +#include "AABBTreeIndirect.hpp" +#include "MeshBoolean.hpp" +#include "Model.hpp" +#include "PNGReadWrite.hpp" +#include "TriangleSelector.hpp" +#include "TextureBake/TextureBakeDebug.hpp" +#include "TextureBake/TextureBakeMesh.hpp" +#include "TextureBake/TextureBakePipeline.hpp" + +namespace Slic3r { + +bool DecodedHeightTexture::texel_tap(const Vec2f &uv, bool tile_enabled, TextureTileMethod tile_method, + TexelTap &tap) const +{ + if (empty()) + return false; + + auto repeat01 = [](float x) { + x = std::fmod(x, 1.f); + return x < 0.f ? x + 1.f : x; + }; + // Standard mirrored-repeat: reflect back and forth every other unit, so tile edges always + // line up with themselves instead of jumping from one edge of the image to the other. + auto mirrored_repeat01 = [](float x) { + x = std::fmod(std::abs(x), 2.f); + return x > 1.f ? 2.f - x : x; + }; + + if (!tile_enabled && (uv.x() < 0.f || uv.x() >= 1.f || uv.y() < 0.f || uv.y() >= 1.f)) + // Outside the single, non-repeating placement entirely: no texture there, not "smeared + // edge pixel" - clamping the *coordinate* to [0, 1] would otherwise keep returning the + // border row/column's height forever in every direction, stretching it out to infinity. + return false; + + float u, v; + if (!tile_enabled) { + u = uv.x(); + v = uv.y(); + } else if (tile_method == TextureTileMethod::MirroredRepeat) { + u = mirrored_repeat01(uv.x()); + v = mirrored_repeat01(uv.y()); + } else { + u = repeat01(uv.x()); + v = repeat01(uv.y()); + } + + const float fx = u * float(width); + const float fy = v * float(height); + const int x0 = std::clamp(int(std::floor(fx)), 0, width - 1); + const int y0 = std::clamp(int(std::floor(fy)), 0, height - 1); + // Neighbour for bilinear filtering: wrap for tiling methods, clamp at the edge otherwise (a + // repeating neighbour would incorrectly blend against the opposite edge of the image). + tap.x0 = x0; + tap.y0 = y0; + tap.x1 = tile_enabled ? (x0 + 1) % width : std::min(x0 + 1, width - 1); + tap.y1 = tile_enabled ? (y0 + 1) % height : std::min(y0 + 1, height - 1); + tap.tx = fx - std::floor(fx); + tap.ty = fy - std::floor(fy); + return true; +} + +float DecodedHeightTexture::sample(const Vec2f &uv, bool tile_enabled, TextureTileMethod tile_method) const +{ + TexelTap tap; + if (!texel_tap(uv, tile_enabled, tile_method, tap)) + return 0.f; + const int x0 = tap.x0, y0 = tap.y0, x1 = tap.x1, y1 = tap.y1; + const float tx = tap.tx, ty = tap.ty; + + auto at = [this](int x, int y) { return float(pixels[size_t(y) * size_t(width) + size_t(x)]) / 255.f; }; + const float top = at(x0, y0) * (1.f - tx) + at(x1, y0) * tx; + const float bottom = at(x0, y1) * (1.f - tx) + at(x1, y1) * tx; + return top * (1.f - ty) + bottom * ty; +} + +Vec3f DecodedHeightTexture::sample_color(const Vec2f &uv, bool tile_enabled, TextureTileMethod tile_method) const +{ + TexelTap tap; + if (!has_color() || !texel_tap(uv, tile_enabled, tile_method, tap)) + return Vec3f::Zero(); + + auto at = [this](int x, int y) { + const size_t i = (size_t(y) * size_t(width) + size_t(x)) * 3; + return Vec3f(float(rgb[i]) / 255.f, float(rgb[i + 1]) / 255.f, float(rgb[i + 2]) / 255.f); + }; + const Vec3f top = at(tap.x0, tap.y0) * (1.f - tap.tx) + at(tap.x1, tap.y0) * tap.tx; + const Vec3f bottom = at(tap.x0, tap.y1) * (1.f - tap.tx) + at(tap.x1, tap.y1) * tap.tx; + return top * (1.f - tap.ty) + bottom * tap.ty; +} + +namespace { +// Decoding a PNG (zlib inflate + defilter) is real work, and image_data never changes in place +// once assigned to a layer (a new texture always gets a brand new image_data), so the decoded +// result can be cached for the lifetime of that specific image_data allocation. This matters +// because the GUI's live preview calls decode_height_texture() again on every rebuild (every +// paint stroke / parameter tweak), which would otherwise re-decode the same unchanged bytes over +// and over. Keyed by a weak_ptr (not just the raw pointer) so a freed image_data's address being +// reused by an unrelated later allocation can never alias a stale cache entry: a weak_ptr to a +// destroyed object always fails to lock, forcing a correct re-decode instead of a false hit. +struct DecodedTextureCache +{ + std::mutex mutex; + std::unordered_map>, DecodedHeightTexture>> entries; +}; +DecodedTextureCache g_decoded_texture_cache; + +// The smoothed copy is cached too, one per image: the thumbnail, the shaded-preview height texture, the +// colour texture, the projector texture, the UV editor's background and the preview job all ask for +// the same (image, smoothing) pair in the same frame while the Smoothing slider moves, and each of them +// blurring its own copy is what froze the UI. Keyed like the raw cache; a different smoothing value +// simply replaces the entry. +struct SmoothedTextureCache +{ + struct Entry + { + std::weak_ptr> source; + float smoothing = 0.f; + DecodedHeightTexture texture; + }; + std::mutex mutex; + std::unordered_map entries; +}; +SmoothedTextureCache g_smoothed_texture_cache; +} // namespace + +namespace { +// A few passes of a separable box blur approximate a Gaussian, cheaply. `radius` is in whole texels; +// 0 is a no-op. Wraps at the edges so a tiling height map stays seamless after smoothing. Operates on +// the grayscale byte buffer in place. +// +// Each pass is a sliding window - one add and one subtract per texel - so the cost is the image size, +// not the image size times the radius. The Smoothing slider drives this on every frame it moves, for +// every consumer of the layer, and the radius goes up to 48 texels: the per-window loop this replaces +// stalled the UI for seconds on a large map. Rows (and column blocks) run in parallel. The rounding is +// the old code's exactly, so a blur gives the same bytes as before. +void smooth_height_pixels_box(std::vector &pixels, int width, int height, int radius) +{ + if (radius <= 0 || width <= 0 || height <= 0 || pixels.size() != size_t(width) * size_t(height)) + return; + + const int window = 2 * radius + 1; + const float inv = 1.f / float(window); + const auto wrap = [](int i, int n) { return (i % n + n) % n; }; + std::vector tmp(pixels.size()); + + for (int pass = 0; pass < 2; ++pass) { // two passes -> smoother than a single box + // Horizontal: a running sum per row. + tbb::parallel_for(tbb::blocked_range(0, height), [&](const tbb::blocked_range &r) { + for (int y = r.begin(); y < r.end(); ++y) { + const uint8_t *src = pixels.data() + size_t(y) * size_t(width); + uint8_t *dst = tmp.data() + size_t(y) * size_t(width); + uint32_t sum = 0; + for (int k = -radius; k <= radius; ++k) + sum += src[wrap(k, width)]; + for (int x = 0; x < width; ++x) { + dst[x] = uint8_t(std::lround(float(sum) * inv)); + sum += src[wrap(x + radius + 1, width)]; + sum -= src[wrap(x - radius, width)]; + } + } + }); + // Vertical: one running sum per column, advanced row by row so the reads stay row-major. + tbb::parallel_for(tbb::blocked_range(0, width, 256), [&](const tbb::blocked_range &r) { + std::vector sum(size_t(r.size()), 0); + for (int k = -radius; k <= radius; ++k) { + const uint8_t *row = tmp.data() + size_t(wrap(k, height)) * size_t(width); + for (int x = r.begin(); x < r.end(); ++x) + sum[size_t(x - r.begin())] += row[x]; + } + for (int y = 0; y < height; ++y) { + uint8_t *dst = pixels.data() + size_t(y) * size_t(width); + const uint8_t *add = tmp.data() + size_t(wrap(y + radius + 1, height)) * size_t(width); + const uint8_t *sub = tmp.data() + size_t(wrap(y - radius, height)) * size_t(width); + for (int x = r.begin(); x < r.end(); ++x) { + uint32_t &sx = sum[size_t(x - r.begin())]; + dst[x] = uint8_t(std::lround(float(sx) * inv)); + sx += add[x]; + sx -= sub[x]; + } + } + }); + } +} + +// The same blur with a *continuous* radius, which is what the Smoothing slider drives. +// +// A box blur can only work in whole texels, so mapping the slider straight onto a rounded radius +// made it move in visible jumps - and its very first step off zero was a full one-texel blur rather +// than a hint of one, which is what made the control feel like it switched on rather than ramped up. +// Blur at the next whole texel up and cross-fade the raw image back in by the fraction left over: +// below one texel that fade *is* the sub-texel kernel, and above it it turns each integer step into +// a continuous ramp. +void smooth_height_pixels(std::vector &pixels, int width, int height, float radius) +{ + if (radius <= 0.f || width <= 0 || height <= 0 || pixels.size() != size_t(width) * size_t(height)) + return; + + const int whole = std::max(1, int(std::ceil(radius))); + const float mix = std::clamp(radius / float(whole), 0.f, 1.f); + const std::vector raw = (mix < 0.999f) ? pixels : std::vector{}; + smooth_height_pixels_box(pixels, width, height, whole); + if (!raw.empty()) + for (size_t i = 0; i < pixels.size(); ++i) + pixels[i] = uint8_t(std::lround(float(raw[i]) + (float(pixels[i]) - float(raw[i])) * mix)); +} +} // namespace + +DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer) +{ + DecodedHeightTexture result; + if (layer.empty()) + return result; + + // The raw (unsmoothed) decode is what gets cached, keyed by the image_data allocation - decoding + // a PNG is the expensive part and never changes for a given image. Smoothing is applied afterwards + // to a throwaway copy, so moving the smoothing slider never invalidates the decode cache. + const void *key = layer.image_data.get(); + bool have_raw = false; + { + std::lock_guard lock(g_decoded_texture_cache.mutex); + auto it = g_decoded_texture_cache.entries.find(key); + if (it != g_decoded_texture_cache.entries.end() && it->second.first.lock() == layer.image_data) { + result = it->second.second; + have_raw = true; + } + } + + if (!have_raw) { + const png::ReadBuf rbuf{ layer.image_data->data(), layer.image_data->size() }; + if (!png::is_png(rbuf)) + // PNG only. The GUI converts any other imported format (jpg, bmp, ...) on import, so this + // code needs no dependency on wxWidgets/libjpeg to read arbitrary user images. + return result; + + png::ImageGreyscale img; + if (png::decode_png(rbuf, img) && img.cols > 0 && img.rows > 0) { + // The shipped library, and anything imported before colour was kept. + result.width = int(img.cols); + result.height = int(img.rows); + result.pixels = std::move(img.buf); + } else { + // A colour source: keep the colour, and take the height from its luminance. The + // coefficients are wxImage::ConvertToGreyscale()'s, which is what the importer used to + // apply on the way in - so a texture that used to be flattened to grey at import time + // displaces identically now that its colour is preserved. + // The loop below steps bytes_per_pixel per texel, which only holds at 8 bits per channel: + // decode_colored_png() does not narrow a 16-bit image, so that would read as noise. + png::ImageColorscale col; + if (!png::decode_colored_png(rbuf, col) || col.cols == 0 || col.rows == 0 || + col.bytes_per_pixel < 3 || col.buf.size() != col.cols * col.rows * size_t(col.bytes_per_pixel)) + return result; + + const int w = int(col.cols); + const int h = int(col.rows); + const size_t bpp = size_t(col.bytes_per_pixel); + result.width = w; + result.height = h; + result.pixels.resize(size_t(w) * size_t(h)); + result.rgb.resize(size_t(w) * size_t(h) * 3); + // decode_colored_png() hands its buffer back bottom-up - it is shared with the CLI's + // plate-thumbnail loader, which expects that - while this type, and decode_png()'s + // grayscale path above, are top-to-bottom. Reverse the rows on the way in so a colour + // height map displaces the same way up as a grayscale one. + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const uint8_t *src = col.buf.data() + (size_t(h - 1 - y) * size_t(w) + size_t(x)) * bpp; + const size_t dst = size_t(y) * size_t(w) + size_t(x); + const uint8_t r = src[0], g = src[1], b = src[2]; + result.rgb[dst * 3] = r; + result.rgb[dst * 3 + 1] = g; + result.rgb[dst * 3 + 2] = b; + result.pixels[dst] = uint8_t(std::lround(0.299 * r + 0.587 * g + 0.114 * b)); + } + } + + std::lock_guard lock(g_decoded_texture_cache.mutex); + // Opportunistically drop entries for image_data that no longer exists anywhere, so the + // cache doesn't grow without bound across many add/remove-texture cycles in a long session. + auto &entries = g_decoded_texture_cache.entries; + for (auto it = entries.begin(); it != entries.end();) + it = it->second.first.expired() ? entries.erase(it) : std::next(it); + entries[key] = { std::weak_ptr>(layer.image_data), result }; + } + + // Smoothing radius scales with the texture so the same slider value blurs the same *fraction* of + // the image whatever resolution it came in at, and stays continuous in the slider - see + // smooth_height_pixels(). The cap is a cost limit, not part of the mapping: the blur is + // O(width * height * radius) per pass, so a large map with the slider at the top would otherwise + // stall every preview rebuild. + if (layer.smoothing > 0.f) { + { + std::lock_guard lock(g_smoothed_texture_cache.mutex); + auto it = g_smoothed_texture_cache.entries.find(key); + if (it != g_smoothed_texture_cache.entries.end() && it->second.smoothing == layer.smoothing && + it->second.source.lock() == layer.image_data) + return it->second.texture; + } + const float span = 0.05f * float(std::min(result.width, result.height)); + const float radius = std::clamp(layer.smoothing, 0.f, 1.f) * std::min(span, 48.f); + smooth_height_pixels(result.pixels, result.width, result.height, radius); + // Colour gets the same blur, per channel. It is the same knob for the same reason: detail in + // the image finer than the mesh can carry is noise either way, and low-passing it here is the + // cheapest place to remove it - one blur of the texture, rather than a fight per triangle. + if (result.has_color()) { + const size_t n = size_t(result.width) * size_t(result.height); + std::vector channel(n); + for (int c = 0; c < 3; ++c) { + for (size_t i = 0; i < n; ++i) + channel[i] = result.rgb[i * 3 + size_t(c)]; + smooth_height_pixels(channel, result.width, result.height, radius); + for (size_t i = 0; i < n; ++i) + result.rgb[i * 3 + size_t(c)] = channel[i]; + } + } + std::lock_guard lock(g_smoothed_texture_cache.mutex); + auto &entries = g_smoothed_texture_cache.entries; + for (auto it = entries.begin(); it != entries.end();) + it = it->second.source.expired() ? entries.erase(it) : std::next(it); + entries[key] = { std::weak_ptr>(layer.image_data), layer.smoothing, result }; + } + return result; +} + +namespace { +struct TextureDetailCache +{ + std::mutex mutex; + std::unordered_map>, TextureDetail>> entries; +}; +TextureDetailCache g_texture_detail_cache; +} // namespace + +TextureDetail analyze_texture_detail(const TextureDisplacementLayer &layer) +{ + TextureDetail out; + if (layer.empty()) + return out; + const void *key = layer.image_data.get(); + { + std::lock_guard lock(g_texture_detail_cache.mutex); + auto it = g_texture_detail_cache.entries.find(key); + if (it != g_texture_detail_cache.entries.end() && it->second.first.lock() == layer.image_data) + return it->second.second; + } + // On the image as imported: the Smoothing slider must not move the recommendation around. + TextureDisplacementLayer raw = layer; + raw.smoothing = 0.f; + const DecodedHeightTexture tex = decode_height_texture(raw); + const int w = tex.width, h = tex.height; + if (w >= 3 && h >= 3) { + double sum = 0.0; + size_t sharp = 0, n = 0; + for (int y = 1; y < h - 1; ++y) { + const uint8_t *row = tex.pixels.data() + size_t(y) * size_t(w); + for (int x = 1; x < w - 1; ++x) { + const float dx = 0.5f * (float(row[x + 1]) - float(row[x - 1])); + const float dy = 0.5f * (float(row[x + w]) - float(row[x - w])); + const float mag = std::sqrt(dx * dx + dy * dy); + sum += mag; + sharp += mag > 30.f; + ++n; + } + } + out.mean_gradient = float(sum / double(n)); + out.sharp_fraction = float(sharp) / float(n); + if (out.sharp_fraction > 0.15f || out.mean_gradient > 50.f) out.pixels_per_edge = 1.f; + else if (out.sharp_fraction > 0.05f || out.mean_gradient > 20.f) out.pixels_per_edge = 1.5f; + else if (out.mean_gradient > 8.f) out.pixels_per_edge = 2.5f; + else out.pixels_per_edge = 4.f; + + // Colour spread: a coarse histogram (8 levels per channel, 64 levels for a grey image) and + // the share of the eight fullest bins. Tiles, logos and camouflage put nearly everything in a + // handful of bins even with some texture noise; a photograph spreads across hundreds. + std::vector bins(size_t(8 * 8 * 8), 0); + const size_t npx = size_t(w) * size_t(h); + if (tex.has_color()) + for (size_t i = 0; i < npx; ++i) + ++bins[size_t(tex.rgb[i * 3] >> 5) * 64 + size_t(tex.rgb[i * 3 + 1] >> 5) * 8 + size_t(tex.rgb[i * 3 + 2] >> 5)]; + else + for (size_t i = 0; i < npx; ++i) + ++bins[size_t(tex.pixels[i] >> 2) * 8]; // 64 grey levels, spread over distinct bins + std::partial_sort(bins.begin(), bins.begin() + 8, bins.end(), std::greater()); + uint64_t top = 0; + for (int i = 0; i < 8; ++i) + top += bins[size_t(i)]; + out.flat_share = float(double(top) / double(npx)); + out.flat_colors = out.flat_share >= 0.85f; + } + std::lock_guard lock(g_texture_detail_cache.mutex); + auto &entries = g_texture_detail_cache.entries; + for (auto it = entries.begin(); it != entries.end();) + it = it->second.first.expired() ? entries.erase(it) : std::next(it); + entries[key] = { std::weak_ptr>(layer.image_data), out }; + return out; +} + +V2Resolution recommend_v2_resolution(const indexed_triangle_set &mesh, + const std::vector &layers, + const Transform3d &volume_to_world) +{ + // The defaults on model load: edge = diagonal / 250 in [0.05, 5] mm, budget 750 k. A texture-driven + // variant (resolution from the texture's own detail) was measured to give better walls on step + // textures at 2-10x the bake time and up to 2 M output triangles, and was not worth that; these + // defaults stayed. The texel size and sharpness are still reported for the panel. + constexpr double EDGE_MIN = 0.05, EDGE_MAX = 5.0, DIAG_DIVISOR = 250.0; + constexpr int BUDGET_K = 750; + + V2Resolution out; + if (mesh.vertices.empty()) + return out; + for (const TextureDisplacementLayer &layer : layers) { + if (layer.empty() || layer.tiling_scale <= 0.f) + continue; + const DecodedHeightTexture &tex = decode_height_texture(layer); + if (tex.width <= 0) + continue; + const float texel = layer.tiling_scale / float(tex.width); + if (out.texel_mm <= 0.f || texel < out.texel_mm) { + out.texel_mm = texel; + out.pixels_per_edge = analyze_texture_detail(layer).pixels_per_edge; + } + } + Vec3d bmin = Vec3d::Constant(std::numeric_limits::max()), bmax = -bmin; + for (const Vec3f &v : mesh.vertices) { + const Vec3d w = volume_to_world * v.cast(); + bmin = bmin.cwiseMin(w); + bmax = bmax.cwiseMax(w); + } + const double diag = (bmax - bmin).norm(); + double edge = std::clamp(diag / DIAG_DIVISOR, EDGE_MIN, EDGE_MAX); + edge = std::max(EDGE_MIN, std::ceil(edge * 100.0) / 100.0); + out.edge_mm = float(edge); + out.budget_k = BUDGET_K; + out.budget_bound = false; + return out; +} + +Vec2f project_planar(const Vec3f &position, const Vec3f &normal) +{ + // Planar-project onto the two axes orthogonal to the dominant component of `normal`. Called + // with each vertex's *own* normal (TextureProjectionMethod::Triplanar), this is a standard + // tri-planar/cube projection; a patch spanning several differently-oriented faces gets each + // face projected along its own best-fit axis instead of all faces sharing one axis picked + // from a single averaged normal (which looks correct on one face but visibly distorts on any + // other face in the same patch - exactly the bug an earlier version of this feature had). + const Vec3f n = normal.cwiseAbs(); + if (n.x() >= n.y() && n.x() >= n.z()) + return Vec2f(position.y(), position.z()); + if (n.y() >= n.x() && n.y() >= n.z()) + return Vec2f(position.x(), position.z()); + return Vec2f(position.x(), position.y()); +} + +namespace { +// Wrapped around patch_axis, centered at patch_center. u is the arc length (mm) around the axis at +// this point's own radius, v is the signed distance along the axis - a reasonable approximation +// for roughly cylindrical selections, not an exact fit for arbitrary geometry. +Vec2f project_cylindrical(const Vec3f &position, const Vec3f &patch_center, const Vec3f &patch_axis) +{ + Vec3f up = patch_axis; + up = (up.norm() > 1e-8f) ? Vec3f(up.normalized()) : Vec3f::UnitZ(); + const Vec3f arbitrary = (std::abs(up.dot(Vec3f::UnitZ())) < 0.9f) ? Vec3f::UnitZ() : Vec3f::UnitX(); + const Vec3f right = Vec3f(up.cross(arbitrary).normalized()); + const Vec3f fwd = Vec3f(right.cross(up).normalized()); + + const Vec3f rel = position - patch_center; + const float along_axis = rel.dot(up); + const float x = rel.dot(right); + const float y = rel.dot(fwd); + const float radius = std::sqrt(x * x + y * y); + const float angle = std::atan2(y, x); + + return Vec2f(angle * radius, along_axis); +} + +// Longitude/latitude around patch_center. u/v are scaled by this point's own distance from the +// center so the result is in roughly the same mm-ish units tiling_scale expects, rather than bare +// radians - again an approximation, not an exact geodesic parametrization. +Vec2f project_spherical(const Vec3f &position, const Vec3f &patch_center) +{ + const Vec3f rel = position - patch_center; + const float radius = rel.norm(); + if (radius < 1e-8f) + return Vec2f::Zero(); + + const Vec3f dir = rel / radius; + const float longitude = std::atan2(dir.y(), dir.x()); + const float latitude = std::asin(std::clamp(dir.z(), -1.f, 1.f)); + return Vec2f(longitude, latitude) * radius; +} + +// CGAL's LSCM parameterizer expects a clean mesh with no isolated (unreferenced) vertices - but +// `patch` here (from TriangleSelector::get_facets_strict()) carries the *entire* mesh's vertex +// array, only its `indices` filtered to the painted triangles. Build a compacted copy referencing +// only the vertices `patch.indices` actually uses, plus a map back to the original vertex index so +// the resulting per-vertex UVs can be looked up by the caller's own (uncompacted) indexing. +indexed_triangle_set compact_patch_with_map(const indexed_triangle_set &patch, std::vector &original_to_compact) +{ + original_to_compact.assign(patch.vertices.size(), -1); + indexed_triangle_set compact; + compact.vertices.reserve(patch.vertices.size()); + compact.indices.reserve(patch.indices.size()); + for (const stl_triangle_vertex_indices &tri : patch.indices) { + stl_triangle_vertex_indices new_tri; + for (int i = 0; i < 3; ++i) { + const int vi = tri[i]; + if (original_to_compact[vi] < 0) { + original_to_compact[vi] = int(compact.vertices.size()); + compact.vertices.push_back(patch.vertices[vi]); + } + new_tri[i] = original_to_compact[vi]; + } + compact.indices.push_back(new_tri); + } + return compact; +} + +} // namespace + +namespace { + +// Union-find over triangles, used to grow charts. +struct UnionFind +{ + std::vector parent; + explicit UnionFind(size_t n) : parent(n) { std::iota(parent.begin(), parent.end(), 0); } + int find(int x) + { + while (parent[size_t(x)] != x) { + parent[size_t(x)] = parent[size_t(parent[size_t(x)])]; // path halving + x = parent[size_t(x)]; + } + return x; + } + void unite(int a, int b) + { + a = find(a); + b = find(b); + if (a != b) + parent[size_t(b)] = a; + } +}; + +uint64_t undirected_edge_key(int a, int b) +{ + if (a > b) + std::swap(a, b); + return (uint64_t(uint32_t(a)) << 32) | uint32_t(b); +} + +Vec3f face_normal(const indexed_triangle_set &mesh, const stl_triangle_vertex_indices &tri) +{ + const Vec3f n = (mesh.vertices[tri[1]] - mesh.vertices[tri[0]]).cross(mesh.vertices[tri[2]] - mesh.vertices[tri[0]]); + const float len = n.norm(); + return (len > 1e-12f) ? Vec3f(n / len) : Vec3f::UnitZ(); +} + +// Whether a chart can be laid flat as one island: a topological disk (V - E + F = 1 with a single boundary loop, +// which LSCM requires) whose faces all point within ~100 degrees of their average - past that even a disk folds +// over itself when flattened. Degenerate faces carry no direction and are left out of the normal test. +bool chart_is_flattenable(const indexed_triangle_set &mesh, const std::vector &normals, const std::vector &faces) +{ + constexpr float MIN_NORMAL_COS = -0.17f; // cos(100 deg) + + std::unordered_map edge_use; + std::unordered_map local_vertex; + edge_use.reserve(faces.size() * 2); + local_vertex.reserve(faces.size()); + Vec3f normal_sum = Vec3f::Zero(); + for (const int f : faces) { + const stl_triangle_vertex_indices &tri = mesh.indices[size_t(f)]; + for (int i = 0; i < 3; ++i) { + ++edge_use[undirected_edge_key(tri[i], tri[(i + 1) % 3])]; + local_vertex.emplace(tri[i], int(local_vertex.size())); + } + normal_sum += normals[size_t(f)]; + } + if (int(local_vertex.size()) - int(edge_use.size()) + int(faces.size()) != 1) + return false; + + UnionFind loops(local_vertex.size()); + int boundary_seed = -1; + for (const auto &[key, uses] : edge_use) { + if (uses > 2) + return false; // non-manifold + if (uses == 1) { + const int a = local_vertex[int(key >> 32)], b = local_vertex[int(uint32_t(key))]; + loops.unite(a, b); + boundary_seed = a; + } + } + if (boundary_seed < 0) + return false; // closed + const int loop = loops.find(boundary_seed); + for (const auto &[key, uses] : edge_use) + if (uses == 1 && loops.find(local_vertex[int(key >> 32)]) != loop) + return false; // a second boundary loop: a ring, or a disk with a hole + + const float len = normal_sum.norm(); + if (len < 1e-6f) + return false; + const Vec3f mean = normal_sum / len; + for (const int f : faces) { + const stl_triangle_vertex_indices &tri = mesh.indices[size_t(f)]; + const bool degenerate = (mesh.vertices[tri[1]] - mesh.vertices[tri[0]]).cross(mesh.vertices[tri[2]] - mesh.vertices[tri[0]]) + .squaredNorm() < 1e-20f; + if (!degenerate && normals[size_t(f)].dot(mean) < MIN_NORMAL_COS) + return false; + } + return true; +} + +// Which side of a cut each face of an unflattenable chart goes to (true / false, parallel to `faces`). The cut +// separates the faces' normals along their widest spread, so a tube splits lengthwise into two half-tubes and a +// closed sphere into two hemispheres. Where the normals barely vary (a flat ring) it splits the face centroids +// along their longest axis instead. +std::vector split_chart_sides(const indexed_triangle_set &mesh, const std::vector &normals, const std::vector &faces) +{ + const auto principal_axis = [](const std::vector &samples, Vec3f &mean, Vec3f &axis) { + mean = Vec3f::Zero(); + for (const Vec3f &s : samples) + mean += s; + mean /= float(samples.size()); + Eigen::Matrix3f covariance = Eigen::Matrix3f::Zero(); + for (const Vec3f &s : samples) { + const Vec3f d = s - mean; + covariance += d * d.transpose(); + } + const Eigen::SelfAdjointEigenSolver solver(covariance); + axis = solver.eigenvectors().col(2); // eigenvalues come sorted ascending + return solver.eigenvalues()(2) / float(samples.size()); + }; + + std::vector samples(faces.size()); + for (size_t i = 0; i < faces.size(); ++i) + samples[i] = normals[size_t(faces[i])]; + Vec3f mean, axis; + if (principal_axis(samples, mean, axis) < 1e-3f) { + for (size_t i = 0; i < faces.size(); ++i) { + const stl_triangle_vertex_indices &tri = mesh.indices[size_t(faces[i])]; + samples[i] = (mesh.vertices[tri[0]] + mesh.vertices[tri[1]] + mesh.vertices[tri[2]]) / 3.f; + } + principal_axis(samples, mean, axis); + } + std::vector side(faces.size()); + for (size_t i = 0; i < faces.size(); ++i) + side[i] = (samples[i] - mean).dot(axis) > 0.f; + return side; +} + +// Groups triangles into charts. First two triangles sharing an edge join the same chart only if the angle between +// their normals is below `seam_angle_deg` (and the edge is not a marked seam): everything sharper is a seam. The +// test uses each face's own normal - averaging in its neighbours, as this once did, cuts low-poly flat faces apart, +// because the two triangles of one cube face average in different neighbouring faces. +// +// Then every chart that cannot be laid flat as one island (see chart_is_flattenable()) is cut in two and each +// connected piece checked again: a cylinder's smooth side is one chart by angle but a ring, which no flattening +// can open without a cut, and a sphere is closed. +std::vector segment_into_charts(const indexed_triangle_set &mesh, const std::vector &normals, + float seam_angle_deg, const std::unordered_set &seam_keys, + int &chart_count) +{ + constexpr int MAX_SPLIT_DEPTH = 8; + const int n_faces = int(mesh.indices.size()); + const float cos_threshold = std::cos(std::clamp(seam_angle_deg, 0.f, 180.f) * float(M_PI) / 180.f); + + // Each shared edge, with the (up to two) faces on it. + std::unordered_map> edge_faces; + edge_faces.reserve(size_t(n_faces) * 3); + for (int f = 0; f < n_faces; ++f) { + const stl_triangle_vertex_indices &tri = mesh.indices[size_t(f)]; + for (int i = 0; i < 3; ++i) { + const uint64_t key = undirected_edge_key(tri[i], tri[(i + 1) % 3]); + const auto [it, inserted] = edge_faces.emplace(key, std::make_pair(f, -1)); + if (!inserted) + it->second.second = f; + } + } + + // Faces joined across each edge that does not cut. A face has three edges, so at most three such neighbours. + std::vector> adjacent(static_cast(n_faces), { -1, -1, -1 }); + const auto link = [&adjacent](int f, int nb) { + for (int &slot : adjacent[size_t(f)]) + if (slot < 0) { + slot = nb; + return; + } + }; + for (const auto &[key, fp] : edge_faces) { + if (fp.second < 0 || fp.first == fp.second) + continue; // a boundary edge of the patch, nothing on the far side to join + // A manually/auto marked seam always cuts, whatever the dihedral angle - that is exactly + // what lets "mark seam" / "cut island" split a chart that is otherwise flat enough to merge. + if (!seam_keys.empty() && seam_keys.count(key)) + continue; + if (normals[size_t(fp.first)].dot(normals[size_t(fp.second)]) >= cos_threshold) { + link(fp.first, fp.second); + link(fp.second, fp.first); + } + } + + // Connected pieces of `faces` over `adjacent`, staying within each face's current `group`. + std::vector group(static_cast(n_faces), 0), visited(static_cast(n_faces), -1); + int visit_pass = 0; + const auto connected_pieces = [&](const std::vector &faces, std::vector> &out) { + const int pass = visit_pass++; + std::vector stack; + for (const int seed : faces) { + if (visited[size_t(seed)] == pass) + continue; + visited[size_t(seed)] = pass; + std::vector piece{ seed }; + stack.assign(1, seed); + while (!stack.empty()) { + const int f = stack.back(); + stack.pop_back(); + for (const int nb : adjacent[size_t(f)]) + if (nb >= 0 && visited[size_t(nb)] != pass && group[size_t(nb)] == group[size_t(f)]) { + visited[size_t(nb)] = pass; + piece.push_back(nb); + stack.push_back(nb); + } + } + out.push_back(std::move(piece)); + } + }; + + std::vector all_faces(static_cast(n_faces)); + std::iota(all_faces.begin(), all_faces.end(), 0); + std::vector> pieces; + connected_pieces(all_faces, pieces); + std::vector, int>> pending; // a piece, and how many cuts produced it + for (std::vector &piece : pieces) + pending.emplace_back(std::move(piece), 0); + + std::vector> charts; + int next_group = 1; + while (!pending.empty()) { + std::vector faces = std::move(pending.back().first); + const int depth = pending.back().second; + pending.pop_back(); + if (faces.size() < 2 || depth >= MAX_SPLIT_DEPTH || chart_is_flattenable(mesh, normals, faces)) { + charts.push_back(std::move(faces)); + continue; + } + const std::vector side = split_chart_sides(mesh, normals, faces); + std::vector halves[2]; + for (size_t i = 0; i < faces.size(); ++i) + halves[side[i] ? 1 : 0].push_back(faces[i]); + if (halves[0].empty() || halves[1].empty()) { + charts.push_back(std::move(faces)); + continue; + } + for (std::vector &half : halves) { + for (const int f : half) + group[size_t(f)] = next_group; + ++next_group; + pieces.clear(); + connected_pieces(half, pieces); + for (std::vector &piece : pieces) + pending.emplace_back(std::move(piece), depth + 1); + } + } + + // Chart ids in first-encountered-triangle order, which TextureIsland indexing documents. + std::vector> first_face(charts.size()); + for (size_t c = 0; c < charts.size(); ++c) + first_face[c] = { *std::min_element(charts[c].begin(), charts[c].end()), c }; + std::sort(first_face.begin(), first_face.end()); + + std::vector chart_of(static_cast(n_faces), -1); + for (size_t id = 0; id < first_face.size(); ++id) + for (const int f : charts[first_face[id].second]) + chart_of[size_t(f)] = int(id); + chart_count = int(charts.size()); + return chart_of; +} + +// Projects a chart onto an orthonormal basis of its own average normal. This is *isometric* for a +// flat chart - lengths and angles come out exactly right - which is why a flat chart never needs a +// solve at all, and why this also serves as the fallback for a chart LSCM cannot handle. +std::vector project_to_tangent_plane(const indexed_triangle_set &chart, const Vec3f &normal) +{ + const Vec3f seed = (std::abs(normal.z()) < 0.9f) ? Vec3f::UnitZ() : Vec3f::UnitX(); + const Vec3f u = Vec3f(seed.cross(normal).normalized()); + const Vec3f v = Vec3f(normal.cross(u).normalized()); + + std::vector uvs(chart.vertices.size()); + for (size_t i = 0; i < chart.vertices.size(); ++i) + uvs[i] = Vec2f(chart.vertices[i].dot(u), chart.vertices[i].dot(v)); + return uvs; +} + +float area_3d(const indexed_triangle_set &mesh) +{ + float area = 0.f; + for (const stl_triangle_vertex_indices &t : mesh.indices) + area += 0.5f * (mesh.vertices[t[1]] - mesh.vertices[t[0]]).cross(mesh.vertices[t[2]] - mesh.vertices[t[0]]).norm(); + return area; +} + +float area_2d(const std::vector &uvs, const std::vector &indices) +{ + float area = 0.f; + for (const stl_triangle_vertex_indices &t : indices) { + const Vec2f e0 = uvs[size_t(t[1])] - uvs[size_t(t[0])]; + const Vec2f e1 = uvs[size_t(t[2])] - uvs[size_t(t[0])]; + area += 0.5f * std::abs(e0.x() * e1.y() - e0.y() * e1.x()); + } + return area; +} + +// FNV-1a over the patch's geometry plus the seam angle. The unwrap depends on nothing else about a +// layer - not depth, tiling, rotation, offset or even which texture is on it - so keying the cache +// on just this is what lets every one of those sliders be dragged without paying for a re-solve. +uint64_t unwrap_cache_key(const indexed_triangle_set &patch, float seam_angle_deg, float padding_mm, + const std::vector> &seam_edges) +{ + uint64_t h = 1469598103934665603ull; + const auto mix = [&h](const void *data, size_t bytes) { + const unsigned char *p = static_cast(data); + for (size_t i = 0; i < bytes; ++i) { + h ^= p[i]; + h *= 1099511628211ull; + } + }; + mix(patch.vertices.data(), patch.vertices.size() * sizeof(Vec3f)); + mix(patch.indices.data(), patch.indices.size() * sizeof(stl_triangle_vertex_indices)); + mix(&seam_angle_deg, sizeof(seam_angle_deg)); + mix(&padding_mm, sizeof(padding_mm)); + mix(seam_edges.data(), seam_edges.size() * sizeof(std::pair)); + return h; +} + +struct UnwrapCache +{ + std::mutex mutex; + std::unordered_map entries; +}; +UnwrapCache g_unwrap_cache; +const size_t UNWRAP_CACHE_MAX_ENTRIES = 8; + +} // namespace + +PatchUnwrap compute_patch_unwrap(const indexed_triangle_set &patch, float seam_angle_deg, float padding_mm, + const std::vector> &seam_edges) +{ + PatchUnwrap result; + if (patch.indices.empty()) + return result; + + const uint64_t key = unwrap_cache_key(patch, seam_angle_deg, padding_mm, seam_edges); + { + std::lock_guard lock(g_unwrap_cache.mutex); + if (auto it = g_unwrap_cache.entries.find(key); it != g_unwrap_cache.entries.end()) + return it->second; + } + + // CGAL wants a mesh with no unreferenced vertices; `patch` carries the whole mesh's vertex array + // (see get_facets_strict()), so compact it and keep the map back to the caller's numbering. + std::vector patch_to_compact; + const indexed_triangle_set compact = compact_patch_with_map(patch, patch_to_compact); + std::vector compact_to_patch(compact.vertices.size(), -1); + for (size_t vi = 0; vi < patch_to_compact.size(); ++vi) + if (patch_to_compact[vi] >= 0) + compact_to_patch[size_t(patch_to_compact[vi])] = int(vi); + + std::vector normals(compact.indices.size()); + for (size_t f = 0; f < compact.indices.size(); ++f) + normals[f] = face_normal(compact, compact.indices[f]); + + // Seam edges arrive in patch (== mesh) vertex space; translate to the compact numbering the + // segmentation runs in. An edge whose endpoints didn't both survive compaction is simply ignored. + std::unordered_set seam_keys; + seam_keys.reserve(seam_edges.size() * 2); + for (const auto &[a, b] : seam_edges) { + if (a < 0 || b < 0 || size_t(a) >= patch_to_compact.size() || size_t(b) >= patch_to_compact.size()) + continue; + const int ca = patch_to_compact[size_t(a)]; + const int cb = patch_to_compact[size_t(b)]; + if (ca >= 0 && cb >= 0) + seam_keys.insert(undirected_edge_key(ca, cb)); + } + + // Once the user has marked seams by hand, those seams define the islands: the automatic + // sharp-angle cutting must not keep splitting faces the user left un-seamed, or islands the user + // meant to be one piece never merge (the "seam angle overrides it" the user hit). So when any + // manual seam exists, the angle threshold is dropped to 180 degrees - nothing is cut except the + // marked seams. With no manual seams, the seam angle behaves exactly as before. + const float effective_seam_angle = seam_keys.empty() ? seam_angle_deg : 180.f; + int chart_count = 0; + const std::vector chart_of = segment_into_charts(compact, normals, effective_seam_angle, seam_keys, chart_count); + + struct FlatChart + { + std::vector uvs; + std::vector to_patch; // chart vertex -> patch vertex + std::vector indices; // chart-local + // Parallel to `indices`: the patch triangle each one came from. compact_patch_with_map() keeps + // the patch's triangle count *and* order, so a compact face index is already a patch face index. + std::vector faces; + Vec2f min = Vec2f::Zero(); + Vec2f size = Vec2f::Zero(); + }; + // static_cast, not size_t(...): the latter is a most-vexing-parse and declares a function. + std::vector charts(static_cast(chart_count)); + + for (int c = 0; c < chart_count; ++c) { + FlatChart &chart = charts[size_t(c)]; + + // Build the chart's own sub-mesh. Each chart gets its *own* compact->local vertex map, which + // is exactly what duplicates a seam vertex: it appears once in each chart that touches it, + // free to hold a different UV in each. + indexed_triangle_set chart_mesh; + std::vector compact_to_local(compact.vertices.size(), -1); + Vec3f normal_sum = Vec3f::Zero(); + for (size_t f = 0; f < compact.indices.size(); ++f) { + if (chart_of[f] != c) + continue; + const stl_triangle_vertex_indices &tri = compact.indices[f]; + // Area-weighted, so a chart's average normal isn't dragged around by slivers. + normal_sum += (compact.vertices[tri[1]] - compact.vertices[tri[0]]).cross(compact.vertices[tri[2]] - compact.vertices[tri[0]]); + + stl_triangle_vertex_indices local_tri; + for (int i = 0; i < 3; ++i) { + const int cv = tri[i]; + if (compact_to_local[size_t(cv)] < 0) { + compact_to_local[size_t(cv)] = int(chart_mesh.vertices.size()); + chart_mesh.vertices.push_back(compact.vertices[size_t(cv)]); + chart.to_patch.push_back(compact_to_patch[size_t(cv)]); + } + local_tri[i] = compact_to_local[size_t(cv)]; + } + chart_mesh.indices.push_back(local_tri); + chart.faces.push_back(int(f)); + } + if (chart_mesh.indices.empty()) + continue; + + const Vec3f chart_normal = (normal_sum.norm() > 1e-12f) ? Vec3f(normal_sum.normalized()) : Vec3f::UnitZ(); + + // Is the chart flat? Charts are grown by a *pairwise* angle threshold, so a chart can still + // curve gradually across many triangles - being merged is not the same as being planar. But + // when it is planar (a cube face, and after seam-cutting that is the common case), the + // tangent-plane projection is already the exact answer, and skipping the solve is the single + // biggest speed-up here. + bool planar = true; + for (size_t f = 0; f < compact.indices.size() && planar; ++f) + if (chart_of[f] == c) + planar = normals[f].dot(chart_normal) >= 0.9998f; // ~1 degree + + // Measured before chart_mesh.indices is moved out from under it, below - area_3d() iterates + // those indices, so taking it afterwards silently measures an empty mesh and returns 0. + const float mesh_area_3d = area_3d(chart_mesh); + + std::optional> uvs; + if (!planar) + uvs = MeshBoolean::cgal::parameterize_lscm(chart_mesh); + // Flat chart, or one LSCM refused (not a topological disk - closed, or holed). + chart.uvs = uvs ? std::move(*uvs) : project_to_tangent_plane(chart_mesh, chart_normal); + chart.indices = std::move(chart_mesh.indices); + + // LSCM's output is only defined up to a similarity, so charts come back at arbitrary and + // mutually inconsistent scales. Rescale each to its true surface area, so `Tile size (mm)` + // means the same thing on every chart and the texture doesn't change density across a seam. + const float uv_area = area_2d(chart.uvs, chart.indices); + if (uv_area > 1e-12f && mesh_area_3d > 1e-12f) { + const float scale = std::sqrt(mesh_area_3d / uv_area); + for (Vec2f &uv : chart.uvs) + uv *= scale; + } + + Vec2f lo(std::numeric_limits::max(), std::numeric_limits::max()); + Vec2f hi(std::numeric_limits::lowest(), std::numeric_limits::lowest()); + for (const Vec2f &uv : chart.uvs) { + lo = lo.cwiseMin(uv); + hi = hi.cwiseMax(uv); + } + chart.min = lo; + chart.size = hi - lo; + } + + // Shelf-pack the charts side by side so they don't overlap. Overlap would be harmless for a + // repeating texture but wrong for a non-tiled (decal) one, and it makes the UV editor unreadable. + float total_area = 0.f; + float widest = 0.f; + for (const FlatChart &chart : charts) { + total_area = total_area + chart.size.x() * chart.size.y(); + widest = std::max(widest, chart.size.x()); + } + const float shelf_width = std::max(widest, std::sqrt(std::max(total_area, 0.f)) * 1.4f); + // Negative padding means auto: a small fraction of the packed size, which is scale-independent + // and so does something sensible for a 5 mm patch and a 500 mm one alike. + const float margin = (padding_mm >= 0.f) ? padding_mm : std::max(shelf_width * 0.02f, 1e-4f); + + std::vector order(charts.size()); + std::iota(order.begin(), order.end(), 0); + std::sort(order.begin(), order.end(), // tallest first: the usual way to keep shelves tight + [&charts](int a, int b) { return charts[size_t(a)].size.y() > charts[size_t(b)].size.y(); }); + + float cursor_x = 0.f, cursor_y = 0.f, row_height = 0.f; + for (const int c : order) { + FlatChart &chart = charts[size_t(c)]; + if (chart.uvs.empty()) + continue; + if (cursor_x > 0.f && cursor_x + chart.size.x() > shelf_width) { + cursor_x = 0.f; + cursor_y += row_height + margin; + row_height = 0.f; + } + const Vec2f translation = Vec2f(cursor_x, cursor_y) - chart.min; + for (Vec2f &uv : chart.uvs) + uv += translation; + + cursor_x += chart.size.x() + margin; + row_height = std::max(row_height, chart.size.y()); + } + + // Concatenate the packed charts into the flat, duplicated-vertex form PatchUnwrap describes. + result.chart_centroid.assign(size_t(chart_count), Vec2f::Zero()); + for (int c = 0; c < chart_count; ++c) { + FlatChart &chart = charts[size_t(c)]; + const int base = int(result.uvs.size()); + result.uvs.insert(result.uvs.end(), chart.uvs.begin(), chart.uvs.end()); + result.source_vertex.insert(result.source_vertex.end(), chart.to_patch.begin(), chart.to_patch.end()); + result.vertex_chart.insert(result.vertex_chart.end(), chart.uvs.size(), c); + for (const stl_triangle_vertex_indices &tri : chart.indices) + result.indices.emplace_back(tri[0] + base, tri[1] + base, tri[2] + base); + result.source_face.insert(result.source_face.end(), chart.faces.begin(), chart.faces.end()); + + if (!chart.uvs.empty()) { + Vec2f sum = Vec2f::Zero(); + for (const Vec2f &uv : chart.uvs) + sum += uv; + result.chart_centroid[size_t(c)] = sum / float(chart.uvs.size()); + } + } + result.chart_count = chart_count; + + // Island outlines: an edge used by exactly one triangle. Charts have disjoint vertex sets (seam + // vertices are duplicated), so an edge along a seam shows up once in each chart and is correctly + // reported as a boundary of both. + { + std::unordered_map edge_use; + edge_use.reserve(result.indices.size() * 3); + for (const stl_triangle_vertex_indices &tri : result.indices) + for (int i = 0; i < 3; ++i) + ++edge_use[undirected_edge_key(tri[i], tri[(i + 1) % 3])]; + for (const stl_triangle_vertex_indices &tri : result.indices) + for (int i = 0; i < 3; ++i) { + const int a = tri[i], b = tri[(i + 1) % 3]; + if (edge_use[undirected_edge_key(a, b)] == 1) + result.boundary_edges.emplace_back(a, b); + } + } + + { + std::lock_guard lock(g_unwrap_cache.mutex); + // A patch changes on every paint stroke, so this would grow without bound over a session. + // Nothing here is worth an LRU: the working set is "the patch I am editing right now". + if (g_unwrap_cache.entries.size() >= UNWRAP_CACHE_MAX_ENTRIES) + g_unwrap_cache.entries.clear(); + g_unwrap_cache.entries.emplace(key, result); + } + return result; +} + +Eigen::Matrix island_transform_matrix(int chart, const PatchUnwrap &unwrap, const std::vector &islands) +{ + Eigen::Matrix m; + m << 1.f, 0.f, 0.f, + 0.f, 1.f, 0.f; + if (chart < 0 || size_t(chart) >= islands.size() || size_t(chart) >= unwrap.chart_centroid.size()) + return m; // no hand placement for this island: leave it where the packing put it + + const TextureIsland &island = islands[size_t(chart)]; + const Vec2f centre = unwrap.chart_centroid[size_t(chart)]; + + const float rad = island.rotation_deg * float(M_PI) / 180.f; + const float s = island.scale; + const float cs = std::cos(rad) * s; + const float sn = std::sin(rad) * s; + + // uv -> centre + R*S*(uv - centre) + offset, with the linear part and the constant part split + // out so this can be handed to a shader as-is. + Eigen::Matrix2f linear; + linear << cs, -sn, + sn, cs; + const Vec2f translation = centre + island.offset - linear * centre; + + m.block<2, 2>(0, 0) = linear; + m.col(2) = translation; + return m; +} + +Vec2f apply_island_transform(const Vec2f &uv, int chart, const PatchUnwrap &unwrap, const std::vector &islands) +{ + const Eigen::Matrix m = island_transform_matrix(chart, unwrap, islands); + return m.block<2, 2>(0, 0) * uv + m.col(2); +} + +namespace { +// One chart's copy of a shared mesh edge: the uv-vertex indices of its two endpoints, ordered so that +// .first is always the lower-numbered base vertex (so two charts' copies line up by base vertex). +struct ChartEdge { int chart = -1; int uv_lo = -1; int uv_hi = -1; }; + +// base edge (lo,hi mesh vertex) -> every chart that has it on its boundary. A mesh edge shared by two +// charts is a seam between them and appears once in each chart's boundary (charts have disjoint uv +// vertices), so its entry lists both charts. +std::map, std::vector> build_shared_edges(const PatchUnwrap &u) +{ + std::map, std::vector> edges; + for (const auto &[ua, ub] : u.boundary_edges) { + if (ua < 0 || ub < 0 || size_t(ua) >= u.source_vertex.size() || size_t(ub) >= u.source_vertex.size()) + continue; + const int ba = u.source_vertex[size_t(ua)], bb = u.source_vertex[size_t(ub)]; + if (ba < 0 || bb < 0 || ba == bb) + continue; + const int chart = (size_t(ua) < u.vertex_chart.size()) ? u.vertex_chart[size_t(ua)] : -1; + if (chart < 0) + continue; + const std::pair key{ std::min(ba, bb), std::max(ba, bb) }; + // Order the uv endpoints to match the base-vertex order in the key. + ChartEdge e{ chart, ua, ub }; + if (u.source_vertex[size_t(ua)] != key.first) + std::swap(e.uv_lo, e.uv_hi); + edges[key].push_back(e); + } + return edges; +} + +// Solve the placement that maps child's raw uv edge (rc0->rc1) onto the parent's placed edge +// (pp0->pp1): a rigid rotation about the child chart's centroid plus an offset, scale kept at 1 so the +// texel density is unchanged. cen is the child chart's centroid. +TextureIsland solve_edge_alignment(const Vec2f &rc0, const Vec2f &rc1, const Vec2f &pp0, const Vec2f &pp1, const Vec2f &cen) +{ + const Vec2f dP = pp1 - pp0, dC = rc1 - rc0; + const float rho = std::atan2(dP.y(), dP.x()) - std::atan2(dC.y(), dC.x()); + const float cs = std::cos(rho), sn = std::sin(rho); + const auto rot = [&](const Vec2f &v) { return Vec2f(v.x() * cs - v.y() * sn, v.x() * sn + v.y() * cs); }; + + TextureIsland island; + island.scale = 1.f; + island.rotation_deg = rho * 180.f / float(M_PI); + island.offset = pp0 - (rot(rc0 - cen) + cen); // island_transform_matrix places rc0 exactly here + return island; +} +} // namespace + +bool join_chart_placement(const PatchUnwrap &unwrap, const std::vector &islands, int child, int parent, + TextureIsland &out_child) +{ + if (child < 0 || parent < 0 || child == parent || size_t(child) >= unwrap.chart_centroid.size()) + return false; + const auto edges = build_shared_edges(unwrap); + for (const auto &[base_edge, list] : edges) { + const ChartEdge *pe = nullptr; + const ChartEdge *ce = nullptr; + for (const ChartEdge &e : list) { + if (e.chart == parent) pe = &e; + if (e.chart == child) ce = &e; + } + if (pe == nullptr || ce == nullptr) + continue; + // Parent's *placed* edge; child's *raw* edge, matched endpoint-to-endpoint by base vertex. + const Vec2f pp0 = apply_island_transform(unwrap.uvs[size_t(pe->uv_lo)], parent, unwrap, islands); + const Vec2f pp1 = apply_island_transform(unwrap.uvs[size_t(pe->uv_hi)], parent, unwrap, islands); + out_child = solve_edge_alignment(unwrap.uvs[size_t(ce->uv_lo)], unwrap.uvs[size_t(ce->uv_hi)], pp0, pp1, + unwrap.chart_centroid[size_t(child)]); + return true; + } + return false; +} + +namespace { +using Tri2 = std::array; + +// Whether two triangles overlap by more than `eps` (separating axis test). Triangles that merely share an edge or +// a corner, as neighbours in a net do, do not. +bool triangles_overlap(const Tri2 &a, const Tri2 &b, float eps) +{ + for (const Tri2 *t : { &a, &b }) + for (int i = 0; i < 3; ++i) { + const Vec2f edge = (*t)[(i + 1) % 3] - (*t)[i]; + const float len = edge.norm(); + if (len < 1e-12f) + continue; + const Vec2f axis(-edge.y() / len, edge.x() / len); + float a_min = std::numeric_limits::max(), a_max = std::numeric_limits::lowest(); + float b_min = a_min, b_max = a_max; + for (int k = 0; k < 3; ++k) { + const float pa = axis.dot(a[k]), pb = axis.dot(b[k]); + a_min = std::min(a_min, pa); + a_max = std::max(a_max, pa); + b_min = std::min(b_min, pb); + b_max = std::max(b_max, pb); + } + if (a_max <= b_min + eps || b_max <= a_min + eps) + return false; + } + return true; +} + +// The triangles already placed in one net, bucketed in a uniform grid so a candidate chart is only tested against +// its neighbourhood. Triangles spanning many cells go into a list that is tested against everything instead. +struct NetGrid +{ + static constexpr int BIG_SPAN = 16; + float cell; + float eps; + std::unordered_map> cells; + std::vector big; + + static uint64_t key(int x, int y) { return (uint64_t(uint32_t(x)) << 32) | uint32_t(y); } + bool range(const Tri2 &t, int &x0, int &y0, int &x1, int &y1) const + { + const Vec2f lo = t[0].cwiseMin(t[1]).cwiseMin(t[2]), hi = t[0].cwiseMax(t[1]).cwiseMax(t[2]); + x0 = int(std::floor(lo.x() / cell)); + y0 = int(std::floor(lo.y() / cell)); + x1 = int(std::floor(hi.x() / cell)); + y1 = int(std::floor(hi.y() / cell)); + return x1 - x0 <= BIG_SPAN && y1 - y0 <= BIG_SPAN; + } + bool overlaps(const Tri2 &t) const + { + for (const Tri2 &b : big) + if (triangles_overlap(t, b, eps)) + return true; + int x0, y0, x1, y1; + if (!range(t, x0, y0, x1, y1)) { + for (const auto &[k, tris] : cells) + for (const Tri2 &b : tris) + if (triangles_overlap(t, b, eps)) + return true; + return false; + } + for (int x = x0; x <= x1; ++x) + for (int y = y0; y <= y1; ++y) + if (const auto it = cells.find(key(x, y)); it != cells.end()) + for (const Tri2 &b : it->second) + if (triangles_overlap(t, b, eps)) + return true; + return false; + } + void insert(const Tri2 &t) + { + int x0, y0, x1, y1; + if (!range(t, x0, y0, x1, y1)) { + big.push_back(t); + return; + } + for (int x = x0; x <= x1; ++x) + for (int y = y0; y <= y1; ++y) + cells[key(x, y)].push_back(t); + } +}; +} // namespace + +std::vector compute_connected_net(const PatchUnwrap &unwrap) +{ + const int n = std::max(unwrap.chart_count, 0); + std::vector islands(static_cast(n)); + if (n <= 1) + return islands; + + // Chart adjacency, with one representative shared edge per adjacent pair. + const auto edges = build_shared_edges(unwrap); + struct PairEdge { ChartEdge a, b; }; + std::map, PairEdge> pair_edge; + std::vector> adj(static_cast(n)); + for (const auto &[base_edge, list] : edges) { + for (size_t i = 0; i < list.size(); ++i) + for (size_t j = i + 1; j < list.size(); ++j) { + const int c1 = list[i].chart, c2 = list[j].chart; + if (c1 == c2 || c1 < 0 || c2 < 0 || c1 >= n || c2 >= n) + continue; + const std::pair pk{ std::min(c1, c2), std::max(c1, c2) }; + if (pair_edge.count(pk)) + continue; // keep the first shared edge as the fold line for this pair + pair_edge[pk] = (c1 < c2) ? PairEdge{ list[i], list[j] } : PairEdge{ list[j], list[i] }; + adj[size_t(pk.first)].push_back(pk.second); + adj[size_t(pk.second)].push_back(pk.first); + } + } + + // Per chart: its vertices, its triangles and its flattened area. + std::vector> chart_verts(static_cast(n)), chart_tris(static_cast(n)); + std::vector chart_area(static_cast(n), 0.f); + for (size_t i = 0; i < unwrap.uvs.size(); ++i) + if (const int c = unwrap.vertex_chart[i]; c >= 0 && c < n) + chart_verts[size_t(c)].push_back(int(i)); + float extent_sum = 0.f, extent = 0.f; + for (size_t t = 0; t < unwrap.indices.size(); ++t) { + const stl_triangle_vertex_indices &tri = unwrap.indices[t]; + const int c = unwrap.vertex_chart[size_t(tri[0])]; + if (c < 0 || c >= n) + continue; + chart_tris[size_t(c)].push_back(int(t)); + const Vec2f &p0 = unwrap.uvs[size_t(tri[0])], &p1 = unwrap.uvs[size_t(tri[1])], &p2 = unwrap.uvs[size_t(tri[2])]; + const Vec2f e0 = p1 - p0, e1 = p2 - p0; + chart_area[size_t(c)] += 0.5f * std::abs(e0.x() * e1.y() - e0.y() * e1.x()); + const Vec2f size = p0.cwiseMax(p1).cwiseMax(p2) - p0.cwiseMin(p1).cwiseMin(p2); + extent_sum += std::max(size.x(), size.y()); + extent = std::max({ extent, p0.cwiseAbs().maxCoeff(), p1.cwiseAbs().maxCoeff(), p2.cwiseAbs().maxCoeff() }); + } + const float cell = std::max(extent_sum / float(std::max(unwrap.indices.size(), 1)), 1e-6f); + // Touching neighbours may overlap by rounding: a fraction of a typical triangle, but at least what float + // rounding of a placement at this distance from the origin can produce. + const float eps = std::max(0.02f * cell, 4e-6f * extent); + + const auto placed = [&](const Eigen::Matrix &m, int t) { + const stl_triangle_vertex_indices &tri = unwrap.indices[size_t(t)]; + Tri2 out; + for (int k = 0; k < 3; ++k) + out[size_t(k)] = m.block<2, 2>(0, 0) * unwrap.uvs[size_t(tri[k])] + m.col(2); + return out; + }; + + // Grow a net from the largest chart not yet in one, unfolding each neighbour onto the chart it was reached from + // (bigger neighbours first, so slivers don't claim the good spots) unless its triangles would overlap the net. + // A chart that doesn't fit stays out and roots a net of its own later, so nothing is left in a random spot. + std::vector by_area(static_cast(n)); + std::iota(by_area.begin(), by_area.end(), 0); + std::stable_sort(by_area.begin(), by_area.end(), [&chart_area](int a, int b) { return chart_area[size_t(a)] > chart_area[size_t(b)]; }); + + std::vector net_of(static_cast(n), -1); + int net_count = 0; + for (const int root : by_area) { + if (net_of[size_t(root)] >= 0 || chart_tris[size_t(root)].empty()) + continue; + const int net = net_count++; + NetGrid grid{ cell, eps, {}, {} }; + net_of[size_t(root)] = net; + { + const Eigen::Matrix m = island_transform_matrix(root, unwrap, islands); + for (const int t : chart_tris[size_t(root)]) + grid.insert(placed(m, t)); + } + std::queue q; + q.push(root); + while (!q.empty()) { + const int p = q.front(); + q.pop(); + std::vector neighbours = adj[size_t(p)]; + std::stable_sort(neighbours.begin(), neighbours.end(), + [&chart_area](int a, int b) { return chart_area[size_t(a)] > chart_area[size_t(b)]; }); + for (const int c : neighbours) { + if (net_of[size_t(c)] >= 0 || chart_tris[size_t(c)].empty()) + continue; + const auto it = pair_edge.find({ std::min(p, c), std::max(p, c) }); + if (it == pair_edge.end()) + continue; + const ChartEdge &pe = (p < c) ? it->second.a : it->second.b; + const ChartEdge &ce = (p < c) ? it->second.b : it->second.a; + const Vec2f pp0 = apply_island_transform(unwrap.uvs[size_t(pe.uv_lo)], p, unwrap, islands); + const Vec2f pp1 = apply_island_transform(unwrap.uvs[size_t(pe.uv_hi)], p, unwrap, islands); + islands[size_t(c)] = solve_edge_alignment(unwrap.uvs[size_t(ce.uv_lo)], unwrap.uvs[size_t(ce.uv_hi)], pp0, + pp1, unwrap.chart_centroid[size_t(c)]); + + const Eigen::Matrix m = island_transform_matrix(c, unwrap, islands); + std::vector tris; + tris.reserve(chart_tris[size_t(c)].size()); + bool fits = true; + for (const int t : chart_tris[size_t(c)]) { + tris.push_back(placed(m, t)); + if (grid.overlaps(tris.back())) { + fits = false; + break; + } + } + if (!fits) { + islands[size_t(c)] = TextureIsland{}; + continue; + } + for (const Tri2 &t : tris) + grid.insert(t); + net_of[size_t(c)] = net; + q.push(c); + } + } + } + + // Shelf-pack the nets side by side, tallest first, the way compute_patch_unwrap() packs charts. + std::vector lo(static_cast(net_count), Vec2f::Constant(std::numeric_limits::max())); + std::vector hi(static_cast(net_count), Vec2f::Constant(std::numeric_limits::lowest())); + for (int c = 0; c < n; ++c) { + const int net = net_of[size_t(c)]; + if (net < 0) + continue; + const Eigen::Matrix m = island_transform_matrix(c, unwrap, islands); + for (const int v : chart_verts[size_t(c)]) { + const Vec2f p = m.block<2, 2>(0, 0) * unwrap.uvs[size_t(v)] + m.col(2); + lo[size_t(net)] = lo[size_t(net)].cwiseMin(p); + hi[size_t(net)] = hi[size_t(net)].cwiseMax(p); + } + } + float total_area = 0.f, widest = 0.f; + for (int k = 0; k < net_count; ++k) { + const Vec2f size = hi[size_t(k)] - lo[size_t(k)]; + total_area += size.x() * size.y(); + widest = std::max(widest, size.x()); + } + const float shelf_width = std::max(widest, std::sqrt(std::max(total_area, 0.f)) * 1.4f); + const float margin = std::max(shelf_width * 0.02f, 1e-4f); + std::vector order(static_cast(net_count)); + std::iota(order.begin(), order.end(), 0); + std::stable_sort(order.begin(), order.end(), [&](int a, int b) { + return hi[size_t(a)].y() - lo[size_t(a)].y() > hi[size_t(b)].y() - lo[size_t(b)].y(); + }); + std::vector shift(static_cast(net_count), Vec2f::Zero()); + float cursor_x = 0.f, cursor_y = 0.f, row_height = 0.f; + for (const int k : order) { + const Vec2f size = hi[size_t(k)] - lo[size_t(k)]; + if (cursor_x > 0.f && cursor_x + size.x() > shelf_width) { + cursor_x = 0.f; + cursor_y += row_height + margin; + row_height = 0.f; + } + shift[size_t(k)] = Vec2f(cursor_x, cursor_y) - lo[size_t(k)]; + cursor_x += size.x() + margin; + row_height = std::max(row_height, size.y()); + } + for (int c = 0; c < n; ++c) + if (net_of[size_t(c)] >= 0) + islands[size_t(c)].offset += shift[size_t(net_of[size_t(c)])]; + return islands; +} + +void average_island_scales(std::vector &islands) +{ + if (islands.empty()) + return; + float sum = 0.f; + for (const TextureIsland &island : islands) + sum += island.scale; + const float mean = sum / float(islands.size()); + for (TextureIsland &island : islands) + island.scale = mean; +} + +std::vector apply_lscm_uv_overrides(PatchUnwrap &unwrap, const std::vector> &overrides) +{ + std::vector edited(unwrap.uvs.size(), false); + if (overrides.empty()) + return edited; + // Mesh-vertex keys first, so an edit on one copy of the same vertex takes precedence over them. + std::map by_mesh_vertex; + for (const auto &[key, uv] : overrides) + if (key >= 0) + by_mesh_vertex[key] = uv; + if (!by_mesh_vertex.empty()) + for (size_t i = 0; i < unwrap.uvs.size(); ++i) + if (const auto it = by_mesh_vertex.find(unwrap.source_vertex[i]); it != by_mesh_vertex.end()) { + unwrap.uvs[i] = it->second; + edited[i] = true; + } + for (const auto &[key, uv] : overrides) + if (const int i = -key - 1; key < 0 && size_t(i) < unwrap.uvs.size()) { + unwrap.uvs[size_t(i)] = uv; + edited[size_t(i)] = true; + } + return edited; +} + +std::vector compute_lscm_uvs(const indexed_triangle_set &patch, const TextureDisplacementLayer &layer) +{ + // Padding disabled (0), matching the UV editor: the packed islands the editor shows and the ones the + // bake/preview samples must be laid out identically, or a hand placement made in the editor would + // land somewhere else in the baked result. + const PatchUnwrap unwrap = compute_patch_unwrap(patch, layer.lscm_seam_angle_deg, 0.f, layer.lscm_seam_edges); + if (unwrap.empty()) + return {}; + + // Manual UV edits (UV editor Vertex/Edge modes) replace the automatic raw unwrap coordinate, before the island + // transform - so the edit rides along with any island move/rotate exactly like the rest of the island. + PatchUnwrap edited_unwrap = unwrap; + const std::vector edited = apply_lscm_uv_overrides(edited_unwrap, layer.lscm_uv_overrides); + + // One UV per patch vertex: a seam vertex has several (one per chart it touches) and has to + // settle on one, since it can only be displaced to a single position. See compute_lscm_uvs()'s + // header comment - the surface stays watertight regardless. A copy that was edited by hand wins, + // so the edit is what bakes; otherwise the first copy does. + std::vector per_vertex(patch.vertices.size(), Vec2f::Zero()); + std::vector assigned(patch.vertices.size(), false); + for (const bool edited_pass : { true, false }) + for (size_t i = 0; i < edited_unwrap.uvs.size(); ++i) { + const int pv = edited_unwrap.source_vertex[i]; + if (pv < 0 || assigned[size_t(pv)] || edited[i] != edited_pass) + continue; + per_vertex[size_t(pv)] = apply_island_transform(edited_unwrap.uvs[i], edited_unwrap.vertex_chart[i], unwrap, layer.islands); + assigned[size_t(pv)] = true; + } + return per_vertex; +} + +std::vector compute_lscm_corner_uvs(const indexed_triangle_set &patch, const TextureDisplacementLayer &layer) +{ + // Padding 0 and the layer's own seam angle/edges, exactly as compute_lscm_uvs() does - the two must + // unwrap identically or a hand placement would land in one place on screen and another in the bake. + const PatchUnwrap unwrap = compute_patch_unwrap(patch, layer.lscm_seam_angle_deg, 0.f, layer.lscm_seam_edges); + if (unwrap.empty() || unwrap.source_face.size() != unwrap.indices.size()) + return {}; + + PatchUnwrap edited_unwrap = unwrap; + apply_lscm_uv_overrides(edited_unwrap, layer.lscm_uv_overrides); + + // No first-copy-wins collapse here: the unwrap's triangles are already per chart, so each corner + // simply takes its own chart's copy. A triangle the unwrap dropped (a sliver a chart rejected) keeps + // the zero it was initialised with; the callers treat that as "no placement" the same way they treat + // an empty result. + std::vector corner(patch.indices.size() * 3, Vec2f::Zero()); + for (size_t t = 0; t < edited_unwrap.indices.size(); ++t) { + const int f = edited_unwrap.source_face[t]; + if (f < 0 || size_t(f) >= patch.indices.size()) + continue; + const stl_triangle_vertex_indices &tri = edited_unwrap.indices[t]; + for (int k = 0; k < 3; ++k) { + const int uvi = tri[k]; + if (uvi < 0 || size_t(uvi) >= edited_unwrap.uvs.size()) + continue; + // The island transform is taken against the *unedited* unwrap, whose chart_centroid is the + // pivot the UV editor rotates about - same as compute_lscm_uvs(). + corner[size_t(f) * 3 + size_t(k)] = apply_island_transform(edited_unwrap.uvs[size_t(uvi)], + edited_unwrap.vertex_chart[size_t(uvi)], + unwrap, layer.islands); + } + } + return corner; +} + +namespace { +// apply_uv_transform()'s per-layer constants, worked out once. Triplanar sampling runs the transform +// three times per point, and recomputing the rotation's cos/sin and the tiling reciprocal on every one +// of them was most of the per-sample arithmetic. +struct UVTransform +{ + float scale, cs, sn, aspect; + Vec2f offset; + + UVTransform(const TextureDisplacementLayer &layer, float aspect_) + { + scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f; + const float rad = layer.rotation_deg * float(M_PI) / 180.f; + cs = std::cos(rad); + sn = std::sin(rad); + aspect = aspect_; + offset = layer.offset; + } + Vec2f operator()(const Vec2f &planar) const + { + const Vec2f scaled = planar * scale; + Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs); + // See apply_uv_transform() for why the aspect correction follows the rotation. + if (aspect > 0.f && aspect != 1.f) + rotated.y() *= aspect; + return rotated + offset; + } +}; + +// |n|^TRIPLANAR_BLEND_SHARPNESS per component. The exponent is a compile-time 4, so two squarings do +// what three std::pow calls per sample did. +static_assert(TRIPLANAR_BLEND_SHARPNESS == 4.f, "triplanar_weights() hard-codes the fourth power"); +inline Vec3f triplanar_weights(const Vec3f &normal) +{ + Vec3f w = normal.cwiseAbs(); + w = w.cwiseProduct(w); + return w.cwiseProduct(w); +} +} // namespace + +Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect) +{ + const float scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f; + const Vec2f scaled = planar * scale; + + const float rad = layer.rotation_deg * float(M_PI) / 180.f; + const float cs = std::cos(rad); + const float sn = std::sin(rad); + Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs); + + // Non-square textures. Without this the [0,1] square of uv covers the whole image whatever its + // proportions, so a 2:1 image is squeezed into a square tile and every feature in it comes out + // half as wide as it should be. `tiling_scale` is the tile's size along u; the tile is + // `tiling_scale * height / width` mm along v, which is exactly what keeps texels square - so + // dividing v by that extent is the same as multiplying it by width / height. A square texture has + // aspect 1 and is untouched, which is why this changes nothing for the shipped library. + // + // Applied after the rotation, not before: scaling one axis of an already-rotated coordinate is a + // shear, and doing it the other way round would make "Rotation" skew the pattern instead of + // turning it. + if (aspect > 0.f && aspect != 1.f) + rotated.y() *= aspect; + + return rotated + layer.offset; +} + +float blend_displacement(float accumulated, float value, TextureBlendMode mode) +{ + switch (mode) { + case TextureBlendMode::Subtract: return accumulated - value; + // Multiply/Divide scale rather than offset, so they take `value` as a factor relative to 1 mm + // (see TextureBlendMode): a 1 mm-deep layer sampling a white texel is then exactly neutral. + case TextureBlendMode::Multiply: return accumulated * value; + case TextureBlendMode::Divide: { + // Every height map has black regions, and a black texel samples to *exactly* zero - so this + // divisor really does hit zero in ordinary use, not just in some contrived edge case. Floor + // its magnitude: an unbounded 1/0 would not merely look wrong, it would fling vertices + // thousands of mm away and poison the mesh's bounding box (and with it every plate/print + // volume check downstream). The floor doubles as a cap on how far Divide can ever amplify + // the relief beneath it - at most 1/0.05 = 20x. + constexpr float min_divisor = 0.05f; + const float divisor = (std::abs(value) < min_divisor) ? std::copysign(min_divisor, value < 0.f ? -1.f : 1.f) : + value; + return accumulated / divisor; + } + case TextureBlendMode::Add: + default: return accumulated + value; + } +} + +bool project_uv_projective(const std::array &m, const Vec3f &position, Vec2f &uv) +{ + const float x = position.x(), y = position.y(), z = position.z(); + const float w = m[8] * x + m[9] * y + m[10] * z + m[11]; + // Strictly greater than zero: at w == 0 the point sits on the projector's plane and maps to + // infinity, and at w < 0 it is behind the projector, where dividing yields a plausible-looking + // but mirrored uv - the classic way a projected decal reappears on the back of a model. + if (!(w > 1e-6f)) + return false; + uv = Vec2f((m[0] * x + m[1] * y + m[2] * z + m[3]) / w, (m[4] * x + m[5] * y + m[6] * z + m[7]) / w); + return true; +} + +float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisplacementLayer &layer, + const Vec3f &position, const Vec3f &normal, + const Vec3f &patch_center, const Vec3f &patch_axis, const Vec2f *lscm_uv) +{ + if (texture.empty()) + return 0.f; + + // width / height of the height map, so a non-square image keeps its proportions (see + // apply_uv_transform()). Every projection except the projective "from view" one funnels through + // here, so this one line is what makes them all aspect-correct. + const float aspect = (texture.height > 0) ? float(texture.width) / float(texture.height) : 1.f; + const UVTransform xf(layer, aspect); + auto sample_at = [&](const Vec2f &planar) { + return texture.sample(xf(planar), layer.tile_enabled, layer.tile_method); + }; + + // Precomputed per-patch LSCM solve wins over the layer's own method (see the header): the + // caller only passes it when the patch actually parameterized successfully, so a patch that + // failed to unwrap falls through to the analytic methods below as its documented fallback. + if (lscm_uv != nullptr) + return sample_at(*lscm_uv); + + switch (layer.projection_method) { + case TextureProjectionMethod::Cylindrical: + return sample_at(project_cylindrical(position, patch_center, patch_axis)); + case TextureProjectionMethod::Spherical: + return sample_at(project_spherical(position, patch_center)); + case TextureProjectionMethod::ViewProjected: + if (layer.view_project_projective) { + // Exact projective placement written by the projection-frame overlay. Sampled directly, + // *without* apply_uv_transform(): the matrix already maps the window's border to the uv + // unit square, so the tiling/rotation/offset controls would displace it off the frame + // the user just aligned. A point behind the projector has no uv at all -> no height. + Vec2f uv; + if (!project_uv_projective(layer.view_project_matrix, position, uv)) + return 0.f; + return texture.sample(uv, layer.tile_enabled, layer.tile_method); + } + // Flat projection onto the captured projector plane. Single-valued per point, so unlike + // blended triplanar it is one sample, and it is what "project from view" places. + return sample_at(Vec2f(position.dot(layer.view_project_right), position.dot(layer.view_project_up))); + case TextureProjectionMethod::LSCM: // no usable unwrap for this patch - fall back to Triplanar + case TextureProjectionMethod::Triplanar: + default: break; + } + + // Blended tri-planar: sample all three axis-aligned planes and cross-fade between them by the + // normal's own components, instead of hard-switching to whichever single axis dominates. The + // hard switch is what produced a visible seam wherever the dominant axis flips (see + // TextureProjectionMethod::Triplanar); a weighted blend is continuous across that transition + // by construction, since the weight of the axis being left behind falls smoothly to zero. + Vec3f w = triplanar_weights(normal); + const float w_sum = w.x() + w.y() + w.z(); + if (w_sum < 1e-8f) + // Degenerate normal: no axis is meaningfully dominant, so no blend is meaningful either. + return sample_at(Vec2f(position.x(), position.y())); + w /= w_sum; + + // Each plane drops the axis it is named for, matching project_planar()'s own convention (which + // the GUI's on-canvas placement gizmo also relies on). + return w.x() * sample_at(Vec2f(position.y(), position.z())) + + w.y() * sample_at(Vec2f(position.x(), position.z())) + + w.z() * sample_at(Vec2f(position.x(), position.y())); +} + +bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDisplacementLayer &layer, + const Vec3f &position, const Vec3f &normal, Vec3f &out, const Vec3f &patch_center, + const Vec3f &patch_axis, const Vec2f *lscm_uv) +{ + if (!texture.has_color()) + return false; + + // Deliberately a transcription of sample_layer_height()'s dispatch rather than a shared template: + // the two differ in what "nothing here" means. Height returns 0, which is a perfectly good height + // (no displacement); colour has no such neutral value - black is a colour - so every path that + // returns 0 there has to report false here instead, and the caller leaves the triangle uncoloured. + const float aspect = (texture.height > 0) ? float(texture.width) / float(texture.height) : 1.f; + const UVTransform xf(layer, aspect); + auto sample_at = [&](const Vec2f &planar) { + return texture.sample_color(xf(planar), layer.tile_enabled, layer.tile_method); + }; + // Outside a non-tiled placement there is no texture at all - the same hard edge sample() gives the + // height. Checked explicitly because sample_color() reports it as black, which is a real colour. + auto covered = [&](const Vec2f &planar) { + if (layer.tile_enabled) + return true; + const Vec2f uv = xf(planar); + return uv.x() >= 0.f && uv.x() < 1.f && uv.y() >= 0.f && uv.y() < 1.f; + }; + + if (lscm_uv != nullptr) { + if (!covered(*lscm_uv)) + return false; + out = sample_at(*lscm_uv); + return true; + } + + switch (layer.projection_method) { + case TextureProjectionMethod::Cylindrical: { + const Vec2f p = project_cylindrical(position, patch_center, patch_axis); + if (!covered(p)) + return false; + out = sample_at(p); + return true; + } + case TextureProjectionMethod::Spherical: { + const Vec2f p = project_spherical(position, patch_center); + if (!covered(p)) + return false; + out = sample_at(p); + return true; + } + case TextureProjectionMethod::ViewProjected: + if (layer.view_project_projective) { + // The frame's own rectangle is the placement, so no apply_uv_transform() - see + // sample_layer_height(). A point behind the projector has no uv, hence no colour. + Vec2f uv; + if (!project_uv_projective(layer.view_project_matrix, position, uv)) + return false; + if (!layer.tile_enabled && (uv.x() < 0.f || uv.x() >= 1.f || uv.y() < 0.f || uv.y() >= 1.f)) + return false; + out = texture.sample_color(uv, layer.tile_enabled, layer.tile_method); + return true; + } else { + const Vec2f p(position.dot(layer.view_project_right), position.dot(layer.view_project_up)); + if (!covered(p)) + return false; + out = sample_at(p); + return true; + } + case TextureProjectionMethod::LSCM: // no usable unwrap for this patch - fall back to Triplanar + case TextureProjectionMethod::Triplanar: + default: break; + } + + // Blended tri-planar, weighted exactly as the height is, so colour and relief stay registered + // across the cross-fade band at a 90-degree edge. + Vec3f w = triplanar_weights(normal); + const float w_sum = w.x() + w.y() + w.z(); + if (w_sum < 1e-8f) { + const Vec2f p(position.x(), position.y()); + if (!covered(p)) + return false; + out = sample_at(p); + return true; + } + w /= w_sum; + + // A blend of three planes is only "not covered" where *every* contributing plane is outside the + // placement; where some are, the covered ones are renormalised so the colour does not fade toward + // black at the edge of an untiled tri-planar layer. + const std::array planes = { Vec2f(position.y(), position.z()), Vec2f(position.x(), position.z()), + Vec2f(position.x(), position.y()) }; + Vec3f acc = Vec3f::Zero(); + float acc_w = 0.f; + for (int i = 0; i < 3; ++i) + if (w[i] > 0.f && covered(planes[size_t(i)])) { + acc += w[i] * sample_at(planes[size_t(i)]); + acc_w += w[i]; + } + if (acc_w <= 0.f) + return false; + out = acc / acc_w; + return true; +} + +indexed_triangle_set extract_painted_patch(const indexed_triangle_set &base_mesh, + const TriangleSelector::TriangleSplittingData &facet_data) +{ + if (facet_data.triangles_to_split.empty()) + return {}; + + const TriangleMesh selector_mesh(base_mesh); + TriangleSelector selector(selector_mesh); + selector.deserialize(facet_data, false); + return selector.get_facets_strict(EnforcerBlockerType::ENFORCER); +} + +bool compute_layer_paint_anchor(const indexed_triangle_set &base_mesh, + const TriangleSelector::TriangleSplittingData &facet_data, + Vec3f &anchor_pos, + Vec3f &anchor_normal) +{ + const indexed_triangle_set patch = extract_painted_patch(base_mesh, facet_data); + if (patch.indices.empty()) + return false; + + Vec3f centroid_sum = Vec3f::Zero(); + Vec3f normal_sum = Vec3f::Zero(); + for (const stl_triangle_vertex_indices &tri : patch.indices) { + const Vec3f &a = patch.vertices[tri[0]]; + const Vec3f &b = patch.vertices[tri[1]]; + const Vec3f &c = patch.vertices[tri[2]]; + // Cross product magnitude is twice the face area, so this area-weights both sums the same + // way texture_displacement_vertex_normals() does above. + normal_sum += (b - a).cross(c - a); + centroid_sum += (a + b + c) / 3.f; + } + + anchor_pos = centroid_sum / float(patch.indices.size()); + anchor_normal = (normal_sum.norm() > 1e-8f) ? Vec3f(normal_sum.normalized()) : Vec3f::UnitZ(); + return true; +} + +// Area-weighted vertex normals of the undisplaced mesh. build_texture_displacement() computes +// these once, up front, and every layer both projects and displaces along them - so a vertex +// covered by several layers is pushed along one single, well-defined direction rather than along +// whatever direction the surface happened to be pointing partway through the stack. +std::vector texture_displacement_vertex_normals(const indexed_triangle_set &its) +{ + std::vector normals(its.vertices.size(), Vec3f::Zero()); + for (const stl_triangle_vertex_indices &tri : its.indices) { + const Vec3f &a = its.vertices[tri[0]]; + const Vec3f &b = its.vertices[tri[1]]; + const Vec3f &c = its.vertices[tri[2]]; + // Cross product magnitude is twice the face area, so this naturally area-weights the + // contribution of each incident face to its vertices. + const Vec3f area_weighted_normal = (b - a).cross(c - a); + normals[tri[0]] += area_weighted_normal; + normals[tri[1]] += area_weighted_normal; + normals[tri[2]] += area_weighted_normal; + } + for (Vec3f &n : normals) { + const float len = n.norm(); + n = (len > 1e-8f) ? Vec3f(n / len) : Vec3f::UnitZ(); + } + return normals; +} + +namespace { +// Shortest along-surface distance from every patch vertex to the patch boundary (the pinned vertices +// shared with the untouched surface), by a multi-source Dijkstra over the patch edges. Used for edge +// smoothing: the displacement is faded out as this distance goes to zero. +std::vector patch_boundary_distance(const indexed_triangle_set &patch, const std::vector &is_boundary) +{ + const size_t n = patch.vertices.size(); + std::vector>> adj(n); + for (const stl_triangle_vertex_indices &tri : patch.indices) + for (int i = 0; i < 3; ++i) { + const int a = tri[i], b = tri[(i + 1) % 3]; + if (a < 0 || b < 0 || size_t(a) >= n || size_t(b) >= n) + continue; + const float w = (patch.vertices[size_t(a)] - patch.vertices[size_t(b)]).norm(); + adj[size_t(a)].push_back({ b, w }); + adj[size_t(b)].push_back({ a, w }); + } + + std::vector dist(n, std::numeric_limits::infinity()); + using QN = std::pair; + std::priority_queue, std::greater> pq; + for (size_t v = 0; v < n; ++v) + if (v < is_boundary.size() && is_boundary[v]) { + dist[v] = 0.f; + pq.push({ 0.f, int(v) }); + } + while (!pq.empty()) { + const auto [d, u] = pq.top(); + pq.pop(); + if (d > dist[size_t(u)]) + continue; + for (const auto &[w, ew] : adj[size_t(u)]) { + const float nd = d + ew; + if (nd < dist[size_t(w)]) { + dist[size_t(w)] = nd; + pq.push({ nd, w }); + } + } + } + return dist; +} +} // namespace + +namespace { +// Majority filter over face adjacency: each triangle takes the most common colour among itself and +// the (up to three) triangles across its edges. Ties, and a triangle whose own colour is already the +// most common, keep what they had - so the filter only ever removes a facet that disagrees with its +// whole neighbourhood, and cannot drift a large region. +// +// Read from a snapshot of the previous pass, so the result does not depend on triangle order. +// Uncoloured triangles (-1) neither vote nor get voted on: the paint boundary is not noise. +void despeckle_triangle_colors(const indexed_triangle_set &mesh, std::vector &color, int passes) +{ + if (passes <= 0 || color.size() != mesh.indices.size()) + return; + const std::vector neighbors = its_face_neighbors(mesh); + if (neighbors.size() != mesh.indices.size()) + return; + + std::vector prev; + for (int pass = 0; pass < passes; ++pass) { + prev = color; + tbb::parallel_for(tbb::blocked_range(0, color.size()), + [&](const tbb::blocked_range &range) { + for (size_t i = range.begin(); i < range.end(); ++i) { + if (prev[i] < 0) + continue; + // At most four candidates (self plus three neighbours), so counting by a linear scan + // is cheaper than any map. + int cand[4] = { prev[i], -1, -1, -1 }; + int count[4] = { 1, 0, 0, 0 }; + int n = 1; + for (int e = 0; e < 3; ++e) { + const int nb = neighbors[i][e]; + if (nb < 0 || size_t(nb) >= prev.size() || prev[size_t(nb)] < 0) + continue; + const int c = prev[size_t(nb)]; + int k = 0; + for (; k < n; ++k) + if (cand[k] == c) { + ++count[k]; + break; + } + if (k == n && n < 4) { + cand[n] = c; + count[n] = 1; + ++n; + } + } + // Strictly greater, so a tie leaves the triangle alone. + int best = 0; + for (int k = 1; k < n; ++k) + if (count[k] > count[best]) + best = k; + if (count[best] > count[0]) + color[i] = cand[best]; + } + }); + } +} +} // namespace + +void merge_small_color_regions(const indexed_triangle_set &mesh, std::vector &color, float min_area_mm2) +{ + const size_t n = mesh.indices.size(); + if (min_area_mm2 <= 0.f || color.size() != n) + return; + const std::vector neighbors = its_face_neighbors(mesh); + if (neighbors.size() != n) + return; + + const auto edge_length = [&mesh](size_t f, int e) { + const stl_triangle_vertex_indices &t = mesh.indices[f]; + return (mesh.vertices[size_t(t[(e + 1) % 3])] - mesh.vertices[size_t(t[e])]).norm(); + }; + + // Connected components of equal colour: `faces` lists every coloured face, component by + // component, `start` delimits them. Uncoloured faces (-1) belong to no component and block the + // flood, so a region never grows across the paint's border. + std::vector component(n, -1); + std::vector faces; + std::vector start; + std::vector area; + std::vector stack; + faces.reserve(n); + for (size_t seed = 0; seed < n; ++seed) { + if (color[seed] < 0 || component[seed] >= 0) + continue; + const int c = color[seed]; + const int id = int(area.size()); + start.push_back(faces.size()); + area.push_back(0.f); + component[seed] = id; + stack.push_back(int(seed)); + while (!stack.empty()) { + const size_t f = size_t(stack.back()); + stack.pop_back(); + faces.push_back(int(f)); + const stl_triangle_vertex_indices &t = mesh.indices[f]; + const Vec3f &a = mesh.vertices[size_t(t[0])], &b = mesh.vertices[size_t(t[1])], &cv = mesh.vertices[size_t(t[2])]; + area[size_t(id)] += 0.5f * (b - a).cross(cv - a).norm(); + for (int e = 0; e < 3; ++e) { + const int nb = neighbors[f][e]; + if (nb < 0 || size_t(nb) >= n || component[size_t(nb)] >= 0 || color[size_t(nb)] != c) + continue; + component[size_t(nb)] = id; + stack.push_back(nb); + } + } + } + start.push_back(faces.size()); + + // Smallest first, so that when a small island borders a slightly larger one the larger one has + // not yet moved and the small one joins whatever the two of them sit in; the larger one then + // reads that colour in turn. + std::vector order; + for (int id = 0; id < int(area.size()); ++id) + if (area[size_t(id)] < min_area_mm2) + order.push_back(id); + std::sort(order.begin(), order.end(), [&area](int l, int r) { return area[size_t(l)] < area[size_t(r)]; }); + + std::vector> weights; // neighbouring colour -> shared edge length + for (const int id : order) { + const size_t begin = start[size_t(id)], end = start[size_t(id) + 1]; + const int own = color[size_t(faces[begin])]; + weights.clear(); + for (size_t k = begin; k < end; ++k) { + const size_t f = size_t(faces[k]); + for (int e = 0; e < 3; ++e) { + const int nb = neighbors[f][e]; + if (nb < 0 || size_t(nb) >= n) + continue; + const int c = color[size_t(nb)]; // read now: an earlier merge may have recoloured it + if (c < 0 || c == own) + continue; + const float len = edge_length(f, e); + auto it = std::find_if(weights.begin(), weights.end(), [c](const std::pair &w) { return w.first == c; }); + if (it == weights.end()) + weights.emplace_back(c, len); + else + it->second += len; + } + } + if (weights.empty()) + continue; // bordered only by uncoloured faces (or nothing): stays + const int target = std::max_element(weights.begin(), weights.end(), + [](const std::pair &l, const std::pair &r) { + return l.second < r.second; + })->first; + for (size_t k = begin; k < end; ++k) + color[size_t(faces[k])] = target; + } +} + +namespace { + +// Wired to the same layer stack via make_combined_displacement_sampler(), so layers, blend modes and +// projections behave identically in both paths and the comparison is between the meshing strategies. +indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set &mesh, + const std::vector &layers, + const TextureDisplacementFacetsData &facets_data, + const TextureDisplacementOptions &options, + const DisplacementProgressFn &progress, + const TextureColorRequest *color, + bool flip_normals, + BakeStageRecorder *debug, + TextureBakeStats *stats) +{ + HeightFieldSampler combined = make_combined_displacement_sampler(mesh, layers, facets_data); + if (!combined) + return mesh; // nothing decodable to displace with + + // Unpainted triangles are excluded, keeping them out of refinement and pinned thereafter. The + // paint is finer than that, though: a brush stroke splits a source triangle into pieces, and only + // some of them are painted. `painted_pieces` keeps every layer's painted pieces (they lie in the + // source surface) so the refined faces can be tested against the paint itself, not against the + // source triangle they came from. + std::vector excluded(mesh.indices.size(), 1); + indexed_triangle_set painted_pieces; + { + const TriangleMesh selector_mesh(mesh); + TriangleSelector selector(selector_mesh); + bool dirty = false; + for (const TriangleSelector::TriangleSplittingData &data : facets_data) { + if (data.triangles_to_split.empty()) + continue; + selector.deserialize(data, dirty); + dirty = true; + std::vector piece_src; + const indexed_triangle_set patch = + selector.get_facets_strict(EnforcerBlockerType::ENFORCER, &piece_src); + for (const int src : piece_src) + if (src >= 0 && size_t(src) < excluded.size()) + excluded[size_t(src)] = 0; + // `patch` carries the whole mesh's vertex array (see compact_patch_with_map()); append + // only what its pieces reference. + std::vector unused; + const indexed_triangle_set compact = compact_patch_with_map(patch, unused); + const int offset = int(painted_pieces.vertices.size()); + painted_pieces.vertices.insert(painted_pieces.vertices.end(), compact.vertices.begin(), compact.vertices.end()); + for (const stl_triangle_vertex_indices &t : compact.indices) + painted_pieces.indices.emplace_back(t[0] + offset, t[1] + offset, t[2] + offset); + } + } + if (std::all_of(excluded.begin(), excluded.end(), [](uint8_t e) { return e != 0; }) || painted_pieces.indices.empty()) + return mesh; // nothing painted + + // Distance to the nearest painted piece. Built once here; the tree is read-only afterwards, so + // the parallel stages below share it freely. + const AABBTreeIndirect::Tree3f painted_tree = + AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(painted_pieces.vertices, painted_pieces.indices); + // `foot`/`normal`, when asked for, are the closest point on the painted pieces and that piece's + // normal. The pieces lie in the *undisplaced* surface, so for a displaced point those two are the + // base position and normal underneath it - the frame colour has to be projected in (see below). + const auto painted_closest = [&painted_pieces, &painted_tree](const Vec3f &p, Vec3f *foot, Vec3f *normal) { + size_t hit = 0; + Vec3f hit_point; + const float d2 = AABBTreeIndirect::squared_distance_to_indexed_triangle_set( + painted_pieces.vertices, painted_pieces.indices, painted_tree, p, hit, hit_point); + if (foot != nullptr) + *foot = hit_point; + if (normal != nullptr && hit < painted_pieces.indices.size()) { + const stl_triangle_vertex_indices &t = painted_pieces.indices[hit]; + const Vec3f &a = painted_pieces.vertices[size_t(t[0])], &b = painted_pieces.vertices[size_t(t[1])], + &c = painted_pieces.vertices[size_t(t[2])]; + Vec3f n = (b - a).cross(c - a); + const float l = n.norm(); + *normal = (l > 0.f) ? Vec3f(n / l) : Vec3f::UnitZ(); + } + return d2; + }; + const auto painted_dist2 = [&painted_closest](const Vec3f &p) { return painted_closest(p, nullptr, nullptr); }; + // Before displacement the queried centroids lie in the same surface as the pieces, so anything + // beyond a hair is genuinely outside the paint. + constexpr float paint_tol = 0.05f; + const auto painted_at = [&painted_dist2](const Vec3f &p) { return painted_dist2(p) < paint_tol * paint_tol; }; + + // "Auto" resolution and budget (0 and -1) resolve here, from the texture and the model - the mesh + // is already in world mm at this point, so no transform is needed. + const bool auto_edge = options.v2_refine_mm <= 0.f, auto_budget = options.v2_max_triangles_k < 0; + const V2Resolution rec = (auto_edge || auto_budget) ? recommend_v2_resolution(mesh, layers) : V2Resolution{}; + TextureBake::PipelineSettings settings; + settings.refine_length = auto_edge ? std::max(0.05, double(rec.edge_mm)) : std::max(0.01f, options.v2_refine_mm); + settings.regularize = options.v2_regularize; + settings.max_triangles = size_t(auto_budget ? std::max(0, rec.budget_k) : options.v2_max_triangles_k) * 1000; + BOOST_LOG_TRIVIAL(info) << "TextureBake resolution: " << settings.refine_length << " mm" + << (auto_edge ? " (auto)" : "") << ", budget " << settings.max_triangles / 1000 << " k" + << (auto_budget ? " (auto)" : ""); + // How far a flat-face harvest may move the surface, tied to the resolution: what a bake refined to + // 1.4 mm edges can merge flat is not what one refined to 0.1 mm may touch. A thirty-second of the + // edge is well under a layer either way - at the default resolution it sheds about 5% of the + // triangles on a relief that covers the whole surface, and far more of one with flat ground in it - + // and the floor keeps a very fine resolution from spending the pass for nothing. + settings.harvest_tol = std::clamp(settings.refine_length / 32.0, 0.005, 0.05); + settings.preserve_untextured = true; + // Always on: relief driven under the plate is unprintable whichever pipeline produced it, so this + // is no longer a choice the user has to make. Only geometry that ends up below the model's own + // bottom is moved - downward relief that stays clear of the plate is untouched. + settings.clamp_below_plate = true; + settings.relocate = options.v2_relocate; + settings.flip_edges = options.v2_flip_edges; + // The sampler already returns millimetres, so the displacement stage must not scale it again. + settings.displace.amplitude = 1.f; + settings.displace.symmetric = false; + // The paint decides what moves here, so the angle limits stay off. + settings.displace.bottom_angle_limit = 0.f; + settings.displace.top_angle_limit = 0.f; + // The sampler's normal only picks the projection blend (triplanar weights); the displacement + // direction is the pipeline's own smooth normal. Handing it the Laplacian-smoothed blend normal + // spreads a crease's 50/50 blend over a band of rows instead of one: on a cube edge the single row + // of vertices that samples both faces' patterns half and half otherwise comes out as a row of + // notches, since it matches neither face. + settings.displace.blend_normal_smoothing = 32; + // Refined faces are asked against the paint itself, so a stroke narrower than a source triangle + // moves only what it covers. + // Only when some included source triangle is painted in part: the pieces then cover less area + // than the triangles they came from. Whole-triangle paint (the usual case, and every bench) has + // nothing to gain from a query per refined face. + { + const auto area_of = [](const indexed_triangle_set &its) { + double a = 0.0; + for (const stl_triangle_vertex_indices &t : its.indices) + a += 0.5 * double((its.vertices[size_t(t[1])] - its.vertices[size_t(t[0])]) + .cross(its.vertices[size_t(t[2])] - its.vertices[size_t(t[0])]).norm()); + return a; + }; + double included_area = 0.0; + for (size_t t = 0; t < mesh.indices.size(); ++t) + if (excluded[t] == 0) { + const stl_triangle_vertex_indices &f = mesh.indices[t]; + included_area += 0.5 * double((mesh.vertices[size_t(f[1])] - mesh.vertices[size_t(f[0])]) + .cross(mesh.vertices[size_t(f[2])] - mesh.vertices[size_t(f[0])]).norm()); + } + const double pieces_area = area_of(painted_pieces); + if (pieces_area < included_area * (1.0 - 1e-4)) + settings.painted = painted_at; + } + // Graded wherever some of the surface is unpainted. A yes/no to a radius, not a distance: most + // queries are far from the paint, and those the tree turns down at its root. + const bool all_painted = std::none_of(excluded.begin(), excluded.end(), [](uint8_t e) { return e != 0; }); + if (settings.painted || !all_painted) + settings.paint_within = [&painted_pieces, &painted_tree](const Vec3d &p, double radius) { + float r2 = float(radius * radius); + return AABBTreeIndirect::is_any_triangle_in_radius(painted_pieces.vertices, painted_pieces.indices, + painted_tree, Vec3f(p.cast()), r2); + }; + + TextureBake::DisplaceBounds bounds; + bounds.min = bounds.max = mesh.vertices.empty() ? Vec3f::Zero() : mesh.vertices.front(); + for (const Vec3f &v : mesh.vertices) { + bounds.min = bounds.min.cwiseMin(v); + bounds.max = bounds.max.cwiseMax(v); + } + + const auto sample = [&combined](const Vec3f &pos, const Vec3f &, const Vec3f &blend_normal) { + // The blend normal chooses the projection; the move itself is along the smooth normal, which + // the pipeline applies on its own. + return combined(pos, blend_normal); + }; + + // The pipeline takes its displacement direction from the soup's winding, so a mirrored placement + // would drive the whole relief inwards. The paint masks were read off `mesh` above, against its + // own vertex order, so the winding can only be turned round after that - here, on the copy that + // becomes the soup - and has to be turned back on the way out, since the caller undoes the same + // mirror when it maps the result back into the volume's coordinates. + indexed_triangle_set oriented = mesh; + if (flip_normals) + for (stl_triangle_vertex_indices &t : oriented.indices) + std::swap(t[1], t[2]); + + // 0 means no simplification, i.e. Bake mode. + const TextureBake::PipelineMode mode = settings.max_triangles > 0 ? TextureBake::PipelineMode::Export + : TextureBake::PipelineMode::Bake; + // Colour, when asked for. The sampler is built now so the simplification can see the colour + // boundaries: a simplified triangle must not span two colours, or its one colour is wrong over + // part of it (half a tile in the neighbour's colour, a tile edge that wanders). + const bool want_color = color != nullptr && color->out_triangle != nullptr && bool(color->quantize); + const ColorFieldSampler color_sampler = + want_color ? make_combined_color_sampler(mesh, layers, facets_data, color->quantize, color->quantize_pure) : ColorFieldSampler{}; + // + // The *palette* index, not the printed filament. The decimation treats any edge whose two faces + // differ as a crease (TextureBakeDecimate.cpp), so it must only ever see where the **perceived** + // colour changes - which is exactly what ColorResolveFn's own contract says the interleaving may + // never be fed into. Handing it the resolved filament made every Z band boundary a crease: on an + // upright wall that is one crease per band, so the collapse ran along those lines and left a stack + // of horizontal slivers, each printing in a single filament. Those were the horizontal colour + // lines in the baked result, and they also spent the triangle budget drawing a pattern the eye is + // meant to blend away. Faces the paint excludes are skipped by the pipeline itself. + const TextureBake::ColorSampleFn color_sample = + color_sampler ? TextureBake::ColorSampleFn([&color_sampler](const Vec3f &p, const Vec3f &n) { + return color_sampler(p, n); + }) + : TextureBake::ColorSampleFn{}; + // The pipeline works on `oriented`, whose winding was reversed above for a mirrored placement, so + // the stages it records are wound the same way. Note where they start and turn the whole range + // back afterwards, exactly as the result itself is turned back below. + const size_t debug_mark = (debug != nullptr) ? debug->mark() : 0; + TextureBake::PipelineResult result = TextureBake::run_pipeline( + TextureBake::to_soup(oriented, excluded), sample, settings, bounds, mode, excluded, + [&progress](const char *, double f) { + return !progress || progress(std::clamp(int(f * 100.0), 0, 99)); + }, + debug, color_sample); + if (debug != nullptr && flip_normals) + debug->rebase(debug_mark, nullptr, /* flip_winding */ true); + if (result.canceled || result.geometry.empty()) + return {}; + + if (stats != nullptr) { + stats->triangles_refined = result.triangles_refined; + stats->triangles_budget = result.triangles_budget; + stats->budget_limited = result.budget_limited; + } + indexed_triangle_set out = TextureBake::to_indexed_triangle_set(result.geometry); + if (out.indices.empty()) + return mesh; + if (flip_normals) + for (stl_triangle_vertex_indices &t : out.indices) + std::swap(t[1], t[2]); + + // Colour, per output triangle. The topology is new, so unlike the classic path there is no base + // triangle to inherit a colour from: each output triangle samples the colour stack at its own + // centroid, and takes colour only where the paint is - measured against the painted pieces, which + // an output centroid is never further from than the relief depth. Then the same despeckle and + // filament resolution as the classic path. + if (want_color) { + std::vector out_color(out.indices.size(), 0); + const ColorFieldSampler &sampler = color_sampler; + if (sampler) { + float max_depth = 0.f; + for (const TextureDisplacementLayer &layer : layers) + max_depth = std::max(max_depth, std::abs(layer.depth_mm)); + const float relief_tol = max_depth + paint_tol; + std::vector palette(out.indices.size(), -1); + tbb::parallel_for(tbb::blocked_range(0, out.indices.size()), [&](const tbb::blocked_range &r) { + for (size_t i = r.begin(); i < r.end(); ++i) { + const stl_triangle_vertex_indices &t = out.indices[i]; + const Vec3f &a = out.vertices[size_t(t[0])], &b = out.vertices[size_t(t[1])], &c = out.vertices[size_t(t[2])]; + const Vec3f centroid = (a + b + c) / 3.f; + // Sample on the *base* surface under this face, not on the relief. The projection + // is a function of position and normal, and the displacement has moved both: the + // triplanar blend weights three axis planes by |n|^4, so a face tilted ~45 degrees + // away from its base normal reads the image half through an unrelated plane. The + // patch border is a ring of exactly such faces - the relief ramps to zero there - + // which is the coloured fringe around the border, and the steep interior slopes + // streak for the same reason. The classic path samples the base patch for this very + // reason; this path was the inconsistent one. + Vec3f foot = centroid, base_n = Vec3f::UnitZ(); + const float d2 = painted_closest(centroid, &foot, &base_n); + if (!all_painted && d2 >= relief_tol * relief_tol) + continue; + palette[i] = sampler(foot, base_n); + } + }); + // The despeckle filter is for a fine, uniform mesh, where one facet flipping colour is + // noise. A simplified mesh is neither: its triangles are as large as the colour regions + // themselves and already end on the colour boundaries, so a majority vote among three + // neighbours would repaint whole features. Bake mode (no simplification) keeps it. + // Not "did a collapse run": a flat-face harvest leaves the mesh as fine as it was, and + // reading it as simplified switched the despeckle off for every bake that harvested. + const bool simplified = result.simplified; + despeckle_triangle_colors(out, palette, simplified ? 0 : color->despeckle_passes); + merge_small_color_regions(out, palette, color->min_color_region_mm2); + for (size_t i = 0; i < out.indices.size(); ++i) { + if (palette[i] < 0) + continue; + const stl_triangle_vertex_indices &t = out.indices[i]; + const Vec3f &a = out.vertices[size_t(t[0])], &b = out.vertices[size_t(t[1])], &c = out.vertices[size_t(t[2])]; + const Vec3f centroid = (a + b + c) / 3.f; + Vec3f normal = (b - a).cross(c - a); + const float nl = normal.norm(); + normal = (nl > 0.f) ? Vec3f(normal / nl) : Vec3f::UnitZ(); + const int filament = color->resolve ? color->resolve(palette[i], centroid, normal) : palette[i]; + if (filament >= 0) + out_color[i] = uint8_t(std::min(filament + 1, 255)); + } + } + *color->out_triangle = std::move(out_color); + } + return out; +} + +} // namespace + +// The bake proper. Runs entirely in whatever space `base_mesh` is given in; the public entry point +// below is what puts it in world space and brings the result back. +// `flip_normals` says the mesh is wound the opposite way round from its outward direction, which is +// what a mirroring world transform leaves behind: the positions are right, but every normal derived +// from the winding points into the model. See build_texture_displacement(). +static indexed_triangle_set build_texture_displacement_in_place( + const indexed_triangle_set &base_mesh, + const std::vector &layers, + const TextureDisplacementFacetsData &facets_data, + const TextureDisplacementOptions &options, + const DisplacementProgressFn &progress, + const TextureColorRequest *color, + bool flip_normals, + BakeStageRecorder *debug, + TextureBakeStats *stats) +{ + // The classic path moves the vertices the mesh already has, so there is no budget to report on. + (void) stats; + // Returns true to keep going. An aborted run returns {} (see the header): an empty mesh is the + // one result no caller can mistake for a finished bake and commit onto the volume. + const auto report = [&progress](int percent) { return !progress || progress(percent); }; + + // Stage capture for the debug view. A stage is timed from the end of the previous one, so the + // capture itself - a copy plus an edge scan - sits outside every measurement it reports. + auto stage_clock = std::chrono::steady_clock::now(); + const auto capture = [&](const char *name, const indexed_triangle_set &m, + const std::string &detail = {}) { + if (debug == nullptr) + return; + const double ms = + std::chrono::duration(std::chrono::steady_clock::now() - stage_clock).count(); + debug->capture(name, m.vertices, m.indices, ms, detail); + stage_clock = std::chrono::steady_clock::now(); + }; + + indexed_triangle_set mesh = base_mesh; + // TriangleSelector's vertex array starts with the mesh's own vertices (any extra ones, created + // where a brush stroke split a triangle, are appended after them), and get_facets_strict() + // emits exactly the *referenced* ones, in order. So selector vertex index i is our vertex i - + // but only if every vertex of `mesh` is referenced by some triangle, which is precisely what + // this call establishes. It is a no-op (indices untouched) for any mesh that already is, which + // in practice is all of them; it exists so an input carrying stray unreferenced vertices can't + // silently shift the indexing and displace the wrong vertices. + its_compactify_vertices(mesh); + if (mesh.vertices.empty() || mesh.indices.empty()) + return mesh; + + capture("input", mesh, "as handed to the bake"); + + if (options.pipeline_v2) + return build_texture_displacement_v2(mesh, layers, facets_data, options, progress, color, flip_normals, + debug, stats); + + // Layers are combined in slot order, like stacked layers in an image editor: each one folds its + // own displacement into the running total via its blend mode (see TextureBlendMode). + std::vector ordered_layers; + for (const TextureDisplacementLayer &layer : layers) + if (!layer.empty() && layer.slot >= 0 && size_t(layer.slot) < TEXTURE_DISPLACEMENT_MAX_LAYERS) + ordered_layers.push_back(&layer); + std::sort(ordered_layers.begin(), ordered_layers.end(), + [](const TextureDisplacementLayer *a, const TextureDisplacementLayer *b) { return a->slot < b->slot; }); + + // Every layer measures its displacement against the *original* surface - normals included - + // rather than against whatever the previous layer left behind. That is what lets all the layers + // be evaluated independently and merged per vertex, instead of having to re-mesh and remap the + // paint masks between them (see the header for why that earlier design was dropped). + std::vector vertex_normals = texture_displacement_vertex_normals(mesh); + + // ... with one correction, applied where the painted area does not cover every triangle around a + // vertex: there the direction to move in is the normal of the *painted* surface, not of the whole + // mesh. On the rim of a fully painted top face the whole-mesh normal is the 45 degrees bisector + // between the face and the side wall it meets, so displacing along it flares the rim outwards + // instead of raising it. Taken over the union of every layer's paint (triangles_to_split is + // exactly the set of original triangles a layer's brush touched - serialize() records an entry for + // each one that is split or carries a non-default state), so a vertex still has one single + // direction however many layers cover it. Interior vertices are unaffected: all their triangles + // are painted, so the two normals coincide. + { + std::vector painted_face(mesh.indices.size(), 0); + bool any_paint = false; + for (const TextureDisplacementLayer *layer : ordered_layers) + for (const TriangleSelector::TriangleBitStreamMapping &m : facets_data[size_t(layer->slot)].triangles_to_split) + if (size_t(m.triangle_idx) < mesh.indices.size()) { + painted_face[size_t(m.triangle_idx)] = 1; + any_paint = true; + } + if (any_paint) { + std::vector painted_normals(mesh.vertices.size(), Vec3f::Zero()); + for (size_t i = 0; i < mesh.indices.size(); ++i) { + if (!painted_face[i]) + continue; + const stl_triangle_vertex_indices &t = mesh.indices[i]; + const Vec3f fn = (mesh.vertices[t[1]] - mesh.vertices[t[0]]).cross(mesh.vertices[t[2]] - mesh.vertices[t[0]]); + for (int k = 0; k < 3; ++k) + painted_normals[size_t(t[k])] += fn; // area-weighted, same convention as the full normals + } + for (size_t v = 0; v < vertex_normals.size(); ++v) + if (const float l = painted_normals[v].norm(); l > 1e-8f) + vertex_normals[v] = painted_normals[v] / l; + // else: no painted triangle touches this vertex, so it will not be displaced anyway - + // leave the whole-mesh normal in place rather than zeroing it. + } + } + + // Both normal passes above read their direction out of the triangle winding, so a mirrored + // placement leaves every one of them pointing into the model - the relief would be carved rather + // than raised. Correct them once, here, where every later stage (displacement direction, the + // planar/triplanar projection axes, the cylinder axis) picks them up already right. + if (flip_normals) + for (Vec3f &n : vertex_normals) + n = -n; + + if (!report(5)) + return {}; + + std::vector displacement(mesh.vertices.size(), 0.f); + // uint8_t rather than std::vector: the sampling loop below writes these from several + // threads at once, and vector's bit packing makes writes to *distinct* elements a data + // race on the shared word. + std::vector displaced(mesh.vertices.size(), 0); + // Union, over every layer, of that layer's patch border - the vertices the post-process smoothing + // holds when TextureDisplacementOptions::smooth_skip_border is set. A vertex on any patch's edge + // counts, which is the conservative choice: hold it rather than let one layer's smoothing melt the + // rim another layer put there. + std::vector on_patch_border(mesh.vertices.size(), false); + bool any_displacement = false; + + // Colour is accumulated per *triangle*, not per vertex: it ends up in the volume's + // mmu_segmentation_facets, which assigns one filament to a whole facet. Layers are visited in + // ascending slot order, so a higher layer simply overwrites a lower one's colour where they + // overlap - the painter's-algorithm reading of a layer stack, and the one that matches how the + // panel lists them. + const bool want_color = color != nullptr && color->out_triangle != nullptr && bool(color->quantize); + // Palette indices, not filament indices: -1 for "no colour here". Kept in perceived-colour space + // for the whole pass so the despeckle filter below operates on what the eye sees, and the + // interleaving that turns a mixed entry into two real filaments happens once, at the very end. + std::vector triangle_palette; + if (want_color) + triangle_palette.assign(mesh.indices.size(), -1); + + const TriangleMesh selector_mesh(mesh); + // One selector for the whole stack, re-deserialized per layer. Its constructor computes + // its_face_neighbors() and its_face_normals() over the *entire* mesh, which on a subdivided model + // is by far the most expensive thing here - building a fresh one per layer paid that cost up to + // eight times over. reset() (what deserialize(..., true) calls) only rebuilds the vertex/triangle + // arrays; the neighbour and face-normal tables are immutable members and survive it. + TriangleSelector selector(selector_mesh); + bool selector_dirty = false; + + const int layer_count = std::max(int(ordered_layers.size()), 1); + int layer_index = 0; + for (const TextureDisplacementLayer *layer : ordered_layers) { + // Progress spans 5..65% across the layers; the apply and smoothing passes take it from there. + if (!report(5 + (60 * layer_index++) / layer_count)) + return {}; + + const TriangleSelector::TriangleSplittingData &data = facets_data[size_t(layer->slot)]; + if (data.triangles_to_split.empty()) + continue; + + const DecodedHeightTexture height = decode_height_texture(*layer); + if (height.empty()) + continue; + + // needs_reset only from the second layer on: the selector is already pristine on the first. + selector.deserialize(data, selector_dirty); + selector_dirty = true; + + const bool color_this_layer = want_color && layer->color_enabled; + // A flat-colour image is matched against the filaments alone (see TextureColorRequest). + const ColorQuantizeFn &layer_quantize = + (color_this_layer && color->quantize_pure && analyze_texture_detail(*layer).flat_colors) ? color->quantize_pure + : color->quantize; + std::vector patch_source; // sub-triangle -> base mesh triangle, only built when colouring + const indexed_triangle_set patch = + selector.get_facets_strict(EnforcerBlockerType::ENFORCER, color_this_layer ? &patch_source : nullptr); + if (patch.indices.empty()) + continue; + // get_facets_strict() returns the same vertex array whichever state is asked for (only the + // triangles are filtered), so `patch` and `rest` share one indexing - and, per the + // compactify above, it is our own. + const indexed_triangle_set rest = selector.get_facets_strict(EnforcerBlockerType::NONE); + + // A vertex used by even one *unpainted* triangle sits on this layer's boundary. It is still + // needed either way - the edge-smoothing falloff measures distance from it - but whether it is + // held flat is now the user's call (TextureDisplacementOptions::displace_border), because + // nothing can tear: the bake is topology-preserving, so a border vertex is one vertex shared + // by both regions and moving it just tilts the unpainted triangles that use it. + std::vector is_boundary(patch.vertices.size(), false); + for (const stl_triangle_vertex_indices &tri : rest.indices) + for (int i = 0; i < 3; ++i) { + is_boundary[tri[i]] = true; + if (tri[i] < int(mesh.vertices.size())) + on_patch_border[size_t(tri[i])] = true; + } + const bool pin_boundary = !options.displace_border; + + // Only the Cylindrical/Spherical methods need the centroid and axis; Triplanar blends each + // vertex's own normal and LSCM solves the patch globally. average_normal is also the fallback + // normal the colour pass below uses for a degenerate triangle. + Vec3f average_normal, patch_centroid, patch_axis; + texture_displacement_patch_frame(patch, vertex_normals, patch_centroid, patch_axis, average_normal); + + // A real unwrap of the whole patch, computed once here rather than per vertex - it is a + // per-chart solve over the whole patch, not a per-point formula. Cached, so repeating this + // for every slider tweak costs a hash rather than a re-solve (see compute_patch_unwrap()). + const std::vector lscm_uvs = (layer->projection_method == TextureProjectionMethod::LSCM) ? + compute_lscm_uvs(patch, *layer) : + std::vector{}; + // The colour pass below samples per *triangle*, so it takes the per-corner unwrap instead: the + // per-vertex collapse above would hand a triangle at a seam the island layout did not join its + // neighbour's placement, painting one triangle per face from the wrong part of the texture. + // (The displacement itself stays on lscm_uvs - a vertex has one position, so one height.) + const std::vector lscm_corner_uvs = (layer->projection_method == TextureProjectionMethod::LSCM) ? + compute_lscm_corner_uvs(patch, *layer) : + std::vector{}; + const bool corner_uv_ok = !lscm_corner_uvs.empty() && + lscm_corner_uvs.size() == patch.indices.size() * 3; + + // Colour, if this layer carries any. Area-weighted over each base triangle's *painted* part, + // so a triangle the brush only clipped a corner off takes the colour of that corner rather + // than of the whole triangle's worth of texture - and so a triangle straddling a colour + // boundary lands on whichever side covers more of it, instead of on whichever sub-triangle + // happened to be emitted first. One quantize call per triangle, after the averaging. + if (color_this_layer) { + const DecodedHeightTexture &tex = height; + if (tex.has_color()) { + std::vector sum(mesh.indices.size(), Vec3f::Zero()); + std::vector sum_area(mesh.indices.size(), 0.f); + for (size_t j = 0; j < patch.indices.size() && j < patch_source.size(); ++j) { + const size_t S = size_t(patch_source[j]); + if (S >= mesh.indices.size()) + continue; + const stl_triangle_vertex_indices &t = patch.indices[j]; + const Vec3f &pa = patch.vertices[size_t(t[0])]; + const Vec3f &pb = patch.vertices[size_t(t[1])]; + const Vec3f &pc = patch.vertices[size_t(t[2])]; + const float area2 = (pb - pa).cross(pc - pa).norm(); + if (area2 <= 0.f) + continue; + const Vec3f centroid = (pa + pb + pc) / 3.f; + + // The normal the triplanar blend weights by, and the unwrap coordinate the LSCM + // path needs, both averaged over the sub-triangle's corners - the same quantities + // the per-vertex height sampling uses, evaluated at the centroid instead. + Vec3f n = Vec3f::Zero(); + Vec2f uv = Vec2f::Zero(); + bool have_uv = !lscm_uvs.empty(); + for (int k = 0; k < 3; ++k) { + const int vi = t[k]; + if (vi < int(vertex_normals.size())) + n += vertex_normals[size_t(vi)]; + if (!have_uv) + continue; + if (corner_uv_ok) + uv += lscm_corner_uvs[j * 3 + size_t(k)]; + else if (size_t(vi) < lscm_uvs.size()) + uv += lscm_uvs[size_t(vi)]; + else + have_uv = false; + } + n = (n.norm() > 1e-8f) ? Vec3f(n.normalized()) : average_normal; + uv /= 3.f; + + Vec3f rgb; + if (sample_layer_color(tex, *layer, centroid, n, rgb, patch_centroid, patch_axis, + have_uv ? &uv : nullptr)) { + sum[S] += area2 * rgb; + sum_area[S] += area2; + } + } + for (size_t i = 0; i < mesh.indices.size(); ++i) + if (sum_area[i] > 0.f) { + const int idx = layer_quantize(sum[i] / sum_area[i]); + // A quantizer that declines this colour leaves whatever a lower layer put + // there, rather than punching a hole in it. + if (idx >= 0) + triangle_palette[i] = idx; + } + } + } + + // Edge smoothing: a per-vertex weight in [0, 1] that fades the displacement to zero toward the + // patch boundary. amount->0 leaves only the very edge softened; amount->1 fades the whole patch + // flat. k = (1-a)/a turns the normalized boundary distance into that weight (see the header). + std::vector edge_weight; + if (layer->edge_smoothing && layer->edge_smoothing_amount > 0.f) { + const std::vector bdist = patch_boundary_distance(patch, is_boundary); + float max_d = 0.f; + for (const float d : bdist) + if (std::isfinite(d)) + max_d = std::max(max_d, d); + edge_weight.assign(patch.vertices.size(), 1.f); + if (max_d > 1e-6f) { + const float a = std::clamp(layer->edge_smoothing_amount, 0.f, 1.f); + const float k = (a < 1.f) ? (1.f - a) / std::max(a, 1e-3f) : 0.f; + for (size_t v = 0; v < bdist.size() && v < edge_weight.size(); ++v) { + const float d = std::isfinite(bdist[v]) ? bdist[v] : max_d; + edge_weight[v] = std::clamp((d / max_d) * k, 0.f, 1.f); + } + } + } + + const float sign = layer->invert ? -1.f : 1.f; + // A vertex may be reached by several of the patch's triangles; each must fold into the + // running total exactly once, or a Multiply/Subtract layer would apply two or three times + // over depending on how many painted triangles happen to share the vertex. Collecting the + // unique list up front (cheap, one pass) is also what lets the expensive part - the texture + // sampling, which is three bilinear fetches plus three pow()s per vertex for triplanar - run + // in parallel below, instead of serially inside the triangle walk. + std::vector layer_vertices; + std::vector visited(patch.vertices.size(), 0); + layer_vertices.reserve(patch.vertices.size()); + for (const stl_triangle_vertex_indices &tri : patch.indices) + for (int i = 0; i < 3; ++i) { + const int vi = tri[i]; + // Split vertices the brush introduced live past the end of our own vertex array; + // they carry no displacement of their own and are not part of the output mesh. + if (vi >= int(mesh.vertices.size()) || (pin_boundary && is_boundary[vi]) || visited[vi]) + continue; + visited[vi] = 1; + layer_vertices.push_back(vi); + } + if (layer_vertices.empty()) + continue; + + std::vector sampled(layer_vertices.size(), 0.f); + tbb::parallel_for(tbb::blocked_range(0, layer_vertices.size()), + [&](const tbb::blocked_range &range) { + for (size_t k = range.begin(); k < range.end(); ++k) { + const size_t vi = size_t(layer_vertices[k]); + const Vec2f *lscm_uv = lscm_uvs.empty() ? nullptr : &lscm_uvs[vi]; + const float h = sample_layer_height(height, *layer, mesh.vertices[vi], vertex_normals[vi], + patch_centroid, patch_axis, lscm_uv); + // midlevel is the height that means "stay put", so anything below it displaces + // *inwards* - see TextureDisplacementLayer::midlevel. At the default of 0 this is + // exactly the old outward-only behaviour. + sampled[k] = (h - layer->midlevel) * layer->depth_mm * sign; + } + }); + + for (size_t k = 0; k < layer_vertices.size(); ++k) { + const size_t vi = size_t(layer_vertices[k]); + // The first layer to reach a vertex has nothing underneath it to blend with, so it + // always starts the total off additively - a Multiply/Divide against an implicit + // zero base would otherwise annihilate (or blow up) it, which is never what the + // user means by putting a mask on the bottom of the stack. + const float accumulated = displacement[vi]; + const float blended = blend_displacement(accumulated, sampled[k], + displaced[vi] ? layer->blend_mode : TextureBlendMode::Add); + // Edge smoothing fades this layer's *effect*, not its input. Scaling the input instead is + // only correct for Add/Subtract, whose neutral value is 0: on a Multiply layer a faded + // input approaches 0, which annihilates everything beneath it at the rim rather than + // leaving it alone, and on a Divide layer it approaches the 0.05 divisor floor, which + // amplifies the relief underneath by up to 20x exactly where it was meant to fade out. + // Interpolating the blended result back toward the accumulated total is the neutral + // element for every mode at once, and reduces to the old formula exactly for Add. + const float edge_w = edge_weight.empty() ? 1.f : edge_weight[vi]; + displacement[vi] = accumulated + (blended - accumulated) * edge_w; + displaced[vi] = 1; + } + any_displacement = true; + } + + if (!report(65)) + return {}; + + if (!any_displacement) + return mesh; + + // The model's own resting plane, taken before anything moves - see the clamp after smoothing. + float resting_z = std::numeric_limits::max(); + for (const Vec3f &v : mesh.vertices) + resting_z = std::min(resting_z, v.z()); + + size_t moved_count = 0; + for (size_t vi = 0; vi < mesh.vertices.size(); ++vi) + if (displaced[vi]) { + mesh.vertices[vi] += vertex_normals[vi] * displacement[vi]; + ++moved_count; + } + capture("displace", mesh, std::to_string(moved_count) + " of " + + std::to_string(mesh.vertices.size()) + " vertices moved"); + + // Post-process relaxation of what the height maps left behind, restricted to the vertices that + // actually moved - the untouched part of the model keeps its exact geometry, and the ring of + // vertices just outside the displaced set stays put and anchors the smoothing so the relief does + // not creep outward. `smooth_skip_border` additionally holds the patch's own outermost ring, whose + // neighbours are those pinned outsiders: relaxing it would drag the rim of the relief back down and + // leave the pattern looking half-melted right where it meets the edge. + if (options.smooth_enabled && options.smooth_strength > 0.f && options.smooth_iterations > 0) { + if (!report(70)) + return {}; + std::vector movable(mesh.vertices.size(), 0); + for (size_t vi = 0; vi < mesh.vertices.size(); ++vi) + movable[vi] = (displaced[vi] && !(options.smooth_skip_border && on_patch_border[vi])) ? 1 : 0; + // The pass hook only *stops* the relaxation early; the report(99) below is what turns a + // cancellation into an empty (uncommittable) result, since a cancelled run keeps reporting + // cancelled. + smooth_mesh_vertices(mesh, movable, options.smooth_strength, options.smooth_iterations, + progress ? DisplacementProgressFn([&report, it = options.smooth_iterations](int pass) { + return report(70 + (29 * (pass + 1)) / std::max(it, 1)); + }) : DisplacementProgressFn{}); + capture("smooth", mesh, + std::to_string(options.smooth_iterations) + " iterations at strength " + + std::to_string(options.smooth_strength)); + } + + // Nothing driven below the model's own resting plane can be printed: it is either through the + // build plate or, once the slicer drops the part back down onto it, holding the whole model up in + // the air. Push it back up to the plane. Only vertices the displacement actually moved are + // eligible - untouched geometry is already exactly where it started - and only those that ended + // up below it, so downward relief that stays clear of the plate is left alone. Runs in world + // space (see build_texture_displacement()), so this really is the plate and not some scaled + // stand-in for it. + size_t clamped_count = 0; + if (resting_z < std::numeric_limits::max()) + for (size_t vi = 0; vi < mesh.vertices.size(); ++vi) + if (displaced[vi] && mesh.vertices[vi].z() < resting_z) { + mesh.vertices[vi].z() = resting_z; + ++clamped_count; + } + capture("clamp to plate", mesh, std::to_string(clamped_count) + " vertices raised to z = " + + std::to_string(resting_z)); + + if (!report(99)) + return {}; + if (want_color) { + // Despeckle in perceived-colour space, then resolve each entry to a real filament. The order + // matters both ways round: filtering after the interleave would erase the bands it is supposed + // to keep, and interleaving before the filter would have the filter treat two halves of one + // blended colour as a disagreement. + despeckle_triangle_colors(mesh, triangle_palette, color->despeckle_passes); + merge_small_color_regions(mesh, triangle_palette, color->min_color_region_mm2); + + std::vector out_color(mesh.indices.size(), 0); + for (size_t i = 0; i < mesh.indices.size(); ++i) { + if (triangle_palette[i] < 0) + continue; + const stl_triangle_vertex_indices &t = mesh.indices[i]; + const Vec3f &a = mesh.vertices[size_t(t[0])], &b = mesh.vertices[size_t(t[1])], &c = mesh.vertices[size_t(t[2])]; + const Vec3f centroid = (a + b + c) / 3.f; + Vec3f normal = (b - a).cross(c - a); + const float nl = normal.norm(); + normal = (nl > 0.f) ? Vec3f(normal / nl) : Vec3f::UnitZ(); + const int filament = color->resolve ? color->resolve(triangle_palette[i], centroid, normal) + : triangle_palette[i]; + if (filament >= 0) + out_color[i] = uint8_t(std::min(filament + 1, 255)); + } + // Handed over only on a run that completed: every early return above is a cancellation, and + // the caller must not commit a half-computed colouring any more than a half-displaced mesh. + *color->out_triangle = std::move(out_color); + } + return mesh; +} + +indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh, + const std::vector &layers, + const TextureDisplacementFacetsData &facets_data, + const TextureDisplacementOptions &options, + const DisplacementProgressFn &progress, + const TextureColorRequest *color, + const Transform3d &volume_to_world, + BakeStageRecorder *debug, + TextureBakeStats *stats) +{ + // An untransformed volume on an untransformed instance is by far the common case, and the round + // trip costs two matrix multiplies per vertex on a mesh that can carry millions of them - so take + // the identity out of the way rather than paying for it. + // The texture is projected in the bake frame - world orientation and scale, anchored at the + // volume's origin (see texture_displacement_bake_frame()). + const Transform3d frame = texture_displacement_bake_frame(volume_to_world); + if (frame.matrix().isApprox(Transform3d::Identity().matrix())) + return build_texture_displacement_in_place(base_mesh, layers, facets_data, options, progress, color, + false, debug, stats); + + const Transform3d to_local = frame.inverse(); + // A mirroring placement leaves the positions correct but every winding-derived normal pointing + // the wrong way. The winding itself is deliberately *not* touched here: the paint masks encode + // each split triangle against its own vertex order, so reordering a triangle's vertices would + // mirror the paint inside it. The bake is told instead, and negates the normals it computes. + const bool mirrored = frame.linear().determinant() < 0.0; + + indexed_triangle_set world = base_mesh; + for (Vec3f &v : world.vertices) + v = (frame * v.cast()).cast(); + + const size_t debug_mark = (debug != nullptr) ? debug->mark() : 0; + indexed_triangle_set out = build_texture_displacement_in_place(world, layers, facets_data, options, + progress, color, mirrored, debug, stats); + // Everything the bake recorded is in world millimetres, like `out` itself. The debug view draws in + // the volume's local frame, so the stages are brought back the same way the result is. + if (debug != nullptr) + debug->rebase(debug_mark, &to_local, /* flip_winding */ false); + // A cancelled run returns {} and must stay {} - an empty mesh is the signal the caller checks + // before committing anything onto the volume. + if (out.vertices.empty()) + return out; + + for (Vec3f &v : out.vertices) + v = (to_local * v.cast()).cast(); + return out; +} + +void texture_displacement_patch_frame(const indexed_triangle_set &patch, const std::vector &vertex_normals, + Vec3f ¢er, Vec3f &axis, Vec3f &average_normal) +{ + Vec3f normal_sum = Vec3f::Zero(); + Vec3f centroid_sum = Vec3f::Zero(); + int count = 0; + for (const stl_triangle_vertex_indices &tri : patch.indices) + for (int i = 0; i < 3; ++i) { + const int vi = tri[i]; + centroid_sum += patch.vertices[size_t(vi)]; + ++count; + // A brush stroke that split a triangle appends new vertices past the base mesh's own, and + // vertex_normals is sized to the base mesh, so those indices must be skipped here. + if (vi < int(vertex_normals.size())) + normal_sum += vertex_normals[size_t(vi)]; + } + average_normal = (normal_sum.norm() > 1e-8f) ? Vec3f(normal_sum.normalized()) : Vec3f::UnitZ(); + center = (count > 0) ? Vec3f(centroid_sum / float(count)) : Vec3f::Zero(); + if (count == 0) { + // Nothing to measure: an upright cylinder about the origin, rather than the axis least aligned + // with a normal that is itself only a fallback. + axis = Vec3f::UnitZ(); + return; + } + + // The world axis least aligned with the average normal - perpendicular to the outward radial + // normal, as a cylinder's own axis would be. + const Vec3f an = average_normal.cwiseAbs(); + axis = (an.x() <= an.y() && an.x() <= an.z()) ? Vec3f::UnitX() : + (an.y() <= an.x() && an.y() <= an.z()) ? Vec3f::UnitY() : Vec3f::UnitZ(); +} + +Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world) +{ + // World orientation and scale, but the origin moved to where the volume's own origin sits: the + // texture then rides with the model when it is moved about the plate (and a single, untiled stamp + // starts on the model rather than at the plate's corner), while a tile is still `tiling_scale` + // printed millimetres whatever the instance's scale. + return Eigen::Translation3d(-volume_to_world.translation()) * volume_to_world; +} + +Transform3d texture_displacement_volume_to_world(const ModelVolume &volume) +{ + const ModelObject *object = volume.get_object(); + if (object == nullptr || object->instances.empty() || object->instances.front() == nullptr) + return volume.get_matrix(); + return object->instances.front()->get_matrix() * volume.get_matrix(); +} + +indexed_triangle_set build_texture_displacement(const ModelVolume &volume) +{ + TextureDisplacementFacetsData facets_data; + for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) + facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data(); + + return build_texture_displacement(volume.mesh().its, volume.texture_displacement_layers, facets_data, + volume.texture_displacement_options, {}, nullptr, + texture_displacement_volume_to_world(volume)); +} + +void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector &movable, float strength, + int iterations, const DisplacementProgressFn &on_pass) +{ + if (iterations <= 0 || mesh.vertices.empty() || movable.size() != mesh.vertices.size()) + return; + strength = std::clamp(strength, 0.f, 1.f); + if (strength <= 0.f) + return; + if (std::none_of(movable.begin(), movable.end(), [](uint8_t m) { return m != 0; })) + return; + + // One-ring neighbours as a CSR-style pair of arrays: counted, prefix-summed, then filled. A + // triangle contributes each of its edges to both endpoints, so a shared edge is listed once per + // incident triangle - the duplicates are harmless here, they just weight an interior edge the same + // way from both sides, and dropping them would cost a sort per vertex for no visible difference. + const size_t nv = mesh.vertices.size(); + std::vector start(nv + 1, 0); + for (const stl_triangle_vertex_indices &t : mesh.indices) + for (int e = 0; e < 3; ++e) { + ++start[size_t(t[e]) + 1]; + ++start[size_t(t[(e + 1) % 3]) + 1]; + } + for (size_t v = 0; v < nv; ++v) + start[v + 1] += start[v]; + const size_t total_refs = size_t(start[nv]); + std::vector nbr(total_refs, 0); + std::vector fill(start.begin(), start.begin() + nv); + for (const stl_triangle_vertex_indices &t : mesh.indices) + for (int e = 0; e < 3; ++e) { + const int a = t[e], b = t[(e + 1) % 3]; + nbr[size_t(fill[size_t(a)]++)] = b; + nbr[size_t(fill[size_t(b)]++)] = a; + } + + // Read every pass from a snapshot of the previous one, so the result does not depend on the order + // vertices happen to be visited in (a Gauss-Seidel sweep would smooth several times as hard at the + // end of the array as at the start). + std::vector prev; + for (int it = 0; it < iterations; ++it) { + prev = mesh.vertices; + // Each vertex reads only from `prev` and writes only its own slot, so the sweep parallelises + // with no synchronisation at all. + tbb::parallel_for(tbb::blocked_range(0, nv), [&](const tbb::blocked_range &range) { + for (size_t v = range.begin(); v < range.end(); ++v) { + if (!movable[v] || start[v] == start[v + 1]) + continue; + Vec3f sum = Vec3f::Zero(); + for (int k = start[v]; k < start[v + 1]; ++k) + sum += prev[size_t(nbr[size_t(k)])]; + const Vec3f avg = sum / float(start[v + 1] - start[v]); + mesh.vertices[v] = prev[v] + (avg - prev[v]) * strength; + } + }); + if (on_pass && !on_pass(it)) + return; // cancelled: leave the passes done so far in place, the caller decides what to do + } +} + +namespace { +// One decoded texture + placement per sampleable layer, in blend (slot) order. Held by shared_ptr so +// the returned closure owns it for as long as the subdivider keeps calling back. +// An unwrap turned into something a *point* sampler can use. LSCM has no formula from position to +// uv - it is a per-triangle map - so a point is placed on the painted patch (the nearest patch +// triangle, and its barycentric coordinates there) and the uv is interpolated from that triangle's own +// per-corner uvs. Exact for a point on the base surface, which is where both samplers are queried: the +// displacement samples refined positions before moving them, and the colour pass samples the foot +// point on the painted pieces. +struct LscmLookup { + indexed_triangle_set patch; // the layer's painted patch, as the unwrap was solved on + AABBTreeIndirect::Tree3f tree; + std::vector corner; // compute_lscm_corner_uvs(patch, layer) + + // False when `pos` is not on this layer's patch (farther than `tol`): there is no uv there, so the + // layer contributes nothing - the same as a non-tiled texture outside its placement. + bool uv_at(const Vec3f &pos, float tol, Vec2f &uv) const + { + size_t hit = 0; + Vec3f foot; + const float d2 = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(patch.vertices, patch.indices, tree, + pos, hit, foot); + if (d2 < 0.f || d2 > tol * tol || hit >= patch.indices.size()) + return false; + const stl_triangle_vertex_indices &t = patch.indices[hit]; + const Vec3f &a = patch.vertices[size_t(t[0])], &b = patch.vertices[size_t(t[1])], &c = patch.vertices[size_t(t[2])]; + const Vec3f e0 = b - a, e1 = c - a, ep = foot - a; + const float d00 = e0.dot(e0), d01 = e0.dot(e1), d11 = e1.dot(e1), dp0 = ep.dot(e0), dp1 = ep.dot(e1); + const float den = d00 * d11 - d01 * d01; + float w1 = 1.f / 3.f, w2 = 1.f / 3.f; // a degenerate triangle takes its centroid's uv + if (std::abs(den) > 1e-20f) { + w1 = (d11 * dp0 - d01 * dp1) / den; + w2 = (d00 * dp1 - d01 * dp0) / den; + } + const Vec2f *c3 = &corner[hit * 3]; + uv = (1.f - w1 - w2) * c3[0] + w1 * c3[1] + w2 * c3[2]; + return true; + } +}; + +// A layer's painted patch as a point-in-region test. Every layer is sampled on its own paint only - the +// analytic projections included: unlike an unwrap they are defined everywhere, so without this every layer's +// relief was stacked over every other layer's painted area, and the top layer's texture showed on all of them. +struct PatchRegion { + indexed_triangle_set patch; + AABBTreeIndirect::Tree3f tree; + + bool contains(const Vec3f &pos, float tol) const + { + size_t hit = 0; + Vec3f foot; + const float d2 = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(patch.vertices, patch.indices, tree, + pos, hit, foot); + return d2 >= 0.f && d2 <= tol * tol; + } +}; + +struct PreparedLayer { + DecodedHeightTexture tex; + TextureDisplacementLayer layer; // a copy of the params (depth/tiling/rotation/offset/blend/...) + Vec3f center; // patch centroid, for Cylindrical/Spherical + Vec3f axis; // cylinder axis, for Cylindrical + // Unwrap layers only; null when the unwrap failed, in which case sampling falls through to the + // layer's analytic fallback exactly as build_texture_displacement()'s classic path does. + std::shared_ptr lscm; + // The painted patch of a layer without an unwrap lookup (the unwrap's own lookup already stops at its patch). + // Null when the paint covers the whole mesh, where every point is on it. + std::shared_ptr region; + + // The uv to hand sample_layer_height()/sample_layer_color(): nullptr for every analytic projection + // (they project `pos` themselves). False means `pos` is off this layer's paint and the layer must be + // skipped. + bool lscm_uv(const Vec3f &pos, Vec2f &uv, const Vec2f *&out) const + { + out = nullptr; + // Queries lie on the base surface, so anything beyond a hair is off this layer's patch. + constexpr float ON_PATCH_TOL = 0.05f; + if (region && !region->contains(pos, ON_PATCH_TOL)) + return false; + if (!lscm) + return true; + if (!lscm->uv_at(pos, ON_PATCH_TOL, uv)) + return false; + out = &uv; + return true; + } +}; + +// Shared by both point samplers, so the height field and the colour field can never disagree about +// where a layer is placed. `need_color` additionally drops layers that cannot contribute colour. +std::shared_ptr> prepare_sampleable_layers( + const indexed_triangle_set &base_mesh, const std::vector &layers, + const TextureDisplacementFacetsData &facets_data, bool need_color) +{ + auto prepared = std::make_shared>(); + + if (base_mesh.indices.empty()) + return prepared; + + std::vector ordered; + for (const TextureDisplacementLayer &l : layers) + if (l.slot >= 0 && l.slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS)) + ordered.push_back(&l); + std::sort(ordered.begin(), ordered.end(), + [](const TextureDisplacementLayer *a, const TextureDisplacementLayer *b) { return a->slot < b->slot; }); + + const std::vector vertex_normals = texture_displacement_vertex_normals(base_mesh); + const TriangleMesh selector_mesh(base_mesh); + const float mesh_area = area_3d(base_mesh); + + for (const TextureDisplacementLayer *layer : ordered) { + // Unwrap layers used to be skipped here ("no per-point UV"). That made the default pipeline + // bake an unwrap layer as nothing at all - and since the job then clears the baked layers' + // paint, the painted region simply vanished. They get an LscmLookup below instead. + if (need_color && !layer->color_enabled) + continue; + const TriangleSelector::TriangleSplittingData &data = facets_data[size_t(layer->slot)]; + if (data.triangles_to_split.empty()) + continue; + const DecodedHeightTexture tex = decode_height_texture(*layer); + if (tex.empty() || (need_color && !tex.has_color())) + continue; + + TriangleSelector selector(selector_mesh); + selector.deserialize(data, false); + const indexed_triangle_set patch = selector.get_facets_strict(EnforcerBlockerType::ENFORCER); + if (patch.indices.empty()) + continue; + + // Patch centroid + cylinder axis, shared with build_texture_displacement() so a + // Cylindrical/Spherical layer's detach criterion matches the geometry the bake will produce. + Vec3f centroid, axis, average_normal; + texture_displacement_patch_frame(patch, vertex_normals, centroid, axis, average_normal); + + std::shared_ptr lscm; + if (layer->projection_method == TextureProjectionMethod::LSCM) { + // Solved on the very patch the classic path and the GUI solve it on (same geometry, seam + // angle and edges), so it hits the unwrap cache and lands exactly where the UV editor + // shows it, hand-placed islands and UV edits included. + auto l = std::make_shared(); + l->corner = compute_lscm_corner_uvs(patch, *layer); + if (l->corner.size() == patch.indices.size() * 3) { + l->patch = patch; + l->tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(l->patch.vertices, l->patch.indices); + lscm = std::move(l); + } + } + + std::shared_ptr region; + if (!lscm && area_3d(patch) < 0.9999f * mesh_area) { + auto r = std::make_shared(); + r->patch = patch; + r->tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(r->patch.vertices, r->patch.indices); + region = std::move(r); + } + + prepared->push_back({ tex, *layer, centroid, axis, std::move(lscm), std::move(region) }); + } + return prepared; +} +} // namespace + +ColorFieldSampler make_combined_color_sampler(const indexed_triangle_set &base_mesh, + const std::vector &layers, + const TextureDisplacementFacetsData &facets_data, + ColorQuantizeFn quantize, + ColorQuantizeFn quantize_pure) +{ + if (!quantize) + return nullptr; + auto prepared = prepare_sampleable_layers(base_mesh, layers, facets_data, /* need_color */ true); + if (prepared->empty()) + return nullptr; + // Per layer: a flat-colour image is matched against the filaments alone, when that quantizer + // was supplied; anything else may use the mixes. Decided once here, not per sample. + auto pure = std::make_shared>(prepared->size(), 0); + if (quantize_pure) + for (size_t i = 0; i < prepared->size(); ++i) + (*pure)[i] = analyze_texture_detail((*prepared)[i].layer).flat_colors ? 1 : 0; + + return [prepared, pure, quantize = std::move(quantize), quantize_pure = std::move(quantize_pure)](const Vec3f &pos, const Vec3f &normal) -> int { + // Last one wins: `prepared` is in ascending slot order and the bake lets a higher layer + // overwrite a lower one's colour, so the sampler has to resolve overlaps the same way. + int result = -1; + for (size_t i = 0; i < prepared->size(); ++i) { + const PreparedLayer &p = (*prepared)[i]; + Vec2f uv; + const Vec2f *lscm_uv = nullptr; + if (!p.lscm_uv(pos, uv, lscm_uv)) + continue; + Vec3f rgb; + if (sample_layer_color(p.tex, p.layer, pos, normal, rgb, p.center, p.axis, lscm_uv)) + if (const int idx = ((*pure)[i] ? quantize_pure : quantize)(rgb); idx >= 0) + result = idx; + } + return result; + }; +} + +HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set &base_mesh, + const std::vector &layers, + const TextureDisplacementFacetsData &facets_data) +{ + auto prepared = prepare_sampleable_layers(base_mesh, layers, facets_data, /* need_color */ false); + if (prepared->empty()) + return nullptr; + + return [prepared](const Vec3f &pos, const Vec3f &normal) -> float { + float total = 0.f; + bool any = false; + for (const PreparedLayer &p : *prepared) { + Vec2f uv; + const Vec2f *lscm_uv = nullptr; + if (!p.lscm_uv(pos, uv, lscm_uv)) + continue; // off this unwrap layer's patch: no uv, so no contribution + const float h = sample_layer_height(p.tex, p.layer, pos, normal, p.center, p.axis, lscm_uv); + const float sign = p.layer.invert ? -1.f : 1.f; + const float signed_h = (h - p.layer.midlevel) * p.layer.depth_mm * sign; + // The first (lowest) sampleable layer folds additively; the rest use their own blend mode - + // same rule build_texture_displacement() applies per vertex. + total = blend_displacement(total, signed_h, any ? p.layer.blend_mode : TextureBlendMode::Add); + any = true; + } + return total; + }; +} + +indexed_triangle_set subdivide_mesh_uniform(const indexed_triangle_set &mesh, float max_edge_length_mm, int max_iterations) +{ + indexed_triangle_set current = mesh; + const float max_edge_sq = max_edge_length_mm * max_edge_length_mm; + + for (int iter = 0; iter < max_iterations; ++iter) { + // One vertex-index pair (always stored low-index-first) -> the midpoint vertex already + // created for it in this pass, so the two triangles sharing that edge both get the exact + // same new vertex instead of two separate, coincident-but-distinct ones (which would leave + // the mesh non-manifold even though it looks fine). + std::unordered_map midpoint_cache; + auto edge_key = [](int a, int b) -> uint64_t { + if (a > b) + std::swap(a, b); + return (uint64_t(uint32_t(a)) << 32) | uint32_t(b); + }; + auto get_midpoint = [&](int a, int b) -> int { + const uint64_t key = edge_key(a, b); + auto it = midpoint_cache.find(key); + if (it != midpoint_cache.end()) + return it->second; + const int idx = int(current.vertices.size()); + current.vertices.push_back((current.vertices[a] + current.vertices[b]) * 0.5f); + midpoint_cache.emplace(key, idx); + return idx; + }; + + std::vector new_indices; + new_indices.reserve(current.indices.size()); + bool any_split = false; + for (const stl_triangle_vertex_indices &tri : current.indices) { + const Vec3f &a = current.vertices[tri[0]]; + const Vec3f &b = current.vertices[tri[1]]; + const Vec3f &c = current.vertices[tri[2]]; + if ((b - a).squaredNorm() <= max_edge_sq && (c - b).squaredNorm() <= max_edge_sq && (a - c).squaredNorm() <= max_edge_sq) { + new_indices.push_back(tri); + continue; + } + + any_split = true; + const int m01 = get_midpoint(tri[0], tri[1]); + const int m12 = get_midpoint(tri[1], tri[2]); + const int m20 = get_midpoint(tri[2], tri[0]); + new_indices.push_back(stl_triangle_vertex_indices(tri[0], m01, m20)); + new_indices.push_back(stl_triangle_vertex_indices(m01, tri[1], m12)); + new_indices.push_back(stl_triangle_vertex_indices(m20, m12, tri[2])); + new_indices.push_back(stl_triangle_vertex_indices(m01, m12, m20)); + } + + current.indices = std::move(new_indices); + if (!any_split) + break; + } + + return current; +} + +indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh, + const std::vector &refine_region, + float target_edge_length_mm, int max_triangles, + std::vector *out_source, const HeightFieldSampler &sampler, + float chord_tolerance_mm, float min_edge_length_mm, + float border_edge_length_mm, + const DisplacementProgressFn &progress, + const ColorFieldSampler &color, float color_edge_length_mm, + bool split_multi_crossings) +{ + // Neighbour slots that are not a triangle index. + constexpr int NB_BOUNDARY = -1; // open edge: terminal on its own, bisected from this side alone + constexpr int NB_NONMANIFOLD = -2; // >2 triangles on the edge: never bisected, that would tear it + + // v[] and nb[] are parallel: nb[e] is the triangle across edge (v[e], v[(e+1)%3]). `src` is the + // input triangle this one descends from - children inherit it, so a caller can carry per-triangle + // data (a paint mask) across the topology change with no geometric remap. + struct Tri { int v[3]; int nb[3]; int src; }; + + std::vector verts = mesh.vertices; + std::vector tris(mesh.indices.size()); + for (size_t i = 0; i < mesh.indices.size(); ++i) + tris[i] = { { mesh.indices[i][0], mesh.indices[i][1], mesh.indices[i][2] }, + { NB_BOUNDARY, NB_BOUNDARY, NB_BOUNDARY }, int(i) }; + + auto emit = [&]() -> indexed_triangle_set { + indexed_triangle_set out; + out.vertices = verts; + out.indices.reserve(tris.size()); + if (out_source) { + out_source->clear(); + out_source->reserve(tris.size()); + } + for (const Tri &t : tris) { + out.indices.emplace_back(t.v[0], t.v[1], t.v[2]); + if (out_source) + out_source->push_back(t.src); + } + return out; + }; + + // Feature-adaptive when a sampler and a positive tolerance are supplied; otherwise refinement is + // driven by the length baseline alone. + const bool feature_mode = bool(sampler) && chord_tolerance_mm > 0.f; + const bool color_mode = bool(color) && color_edge_length_mm > 0.f; + const bool multi_mode = feature_mode && split_multi_crossings; + float step_iso_mm = 0.f; // the mid-level a step crosses; set once the region is sampled + float step_dev_mm = std::numeric_limits::max(); // deviations above this are steps + const float color_sq = color_edge_length_mm > 0.f ? color_edge_length_mm * color_edge_length_mm : 0.f; + const float min_floor_sq = min_edge_length_mm > 0.f ? min_edge_length_mm * min_edge_length_mm : 0.f; + const float target_sq = target_edge_length_mm > 0.f ? target_edge_length_mm * target_edge_length_mm : 0.f; + const float border_sq = border_edge_length_mm > 0.f ? border_edge_length_mm * border_edge_length_mm : 0.f; + + // refine_region is indexed by input-triangle index, and every triangle's src stays in that range + // (children inherit their parent's src), so a wrong size would be an out-of-bounds read. Guard it. + if (refine_region.size() != mesh.indices.size() || int(tris.size()) + 2 > max_triangles) + return emit(); + if (!feature_mode && !color_mode && target_sq <= 0.f && border_sq <= 0.f) + return emit(); // no criterion at all + if (std::none_of(refine_region.begin(), refine_region.end(), [](uint8_t v) { return v != 0; })) + return emit(); // nothing flagged: no-op + + auto edge_key = [](int a, int b) -> uint64_t { + if (a > b) + std::swap(a, b); + return (uint64_t(uint32_t(a)) << 32) | uint32_t(b); + }; + + // Edge adjacency, built once here and then maintained incrementally by bisect() below. Rebuilding + // it per refinement pass is what made the previous version's cost scale with the whole model + // instead of with the refined region, and what forced the tiny pass budget that stopped + // refinement short. + { + // Sort the half-edges by their edge key and walk the equal runs, rather than hashing every one + // of them twice into an unordered_map. Same result, but the two expensive parts - forming the + // keys and ordering them - both parallelise, where a shared hash map cannot. The map also cost + // a second full pass of lookups purely to read back what the first pass had just inserted. + std::vector> he(tris.size() * 3); // (edge key, triangle * 3 + local edge) + tbb::parallel_for(tbb::blocked_range(0, tris.size()), + [&](const tbb::blocked_range &range) { + for (size_t ti = range.begin(); ti < range.end(); ++ti) + for (int e = 0; e < 3; ++e) + he[ti * 3 + size_t(e)] = { + edge_key(tris[ti].v[e], tris[ti].v[(e + 1) % 3]), int(ti) * 3 + e + }; + }); + tbb::parallel_sort(he.begin(), he.end()); + // Runs of equal key are the half-edges of one edge: one is a boundary, two are neighbours, + // more is non-manifold. Serial, but it is a single linear pass over an already ordered array. + for (size_t i = 0; i < he.size();) { + size_t j = i + 1; + while (j < he.size() && he[j].first == he[i].first) + ++j; + const size_t count = j - i; + if (count == 2) { + const int a = he[i].second, b = he[i + 1].second; + tris[size_t(a / 3)].nb[a % 3] = b / 3; + tris[size_t(b / 3)].nb[b % 3] = a / 3; + } else if (count > 2) { + for (size_t k = i; k < j; ++k) + tris[size_t(he[k].second / 3)].nb[he[k].second % 3] = NB_NONMANIFOLD; + } + // count == 1 keeps the NB_BOUNDARY it was initialised with. + i = j; + } + } + + // Feature mode: per-vertex surface normal, and the sampled displacement height at each vertex. + // Heights are filled in lazily - on a big model only a small painted region is ever looked at, and + // a sampler call is a texture fetch (plus trig) per layer, so sampling every vertex of the whole + // mesh up front was pure waste. Both arrays grow in lockstep with `verts`. + std::vector vnormal; + std::vector vheight; + std::vector vheight_valid; + if (feature_mode) { + vnormal.assign(verts.size(), Vec3f::Zero()); + for (const Tri &t : tris) { + const Vec3f fn = (verts[t.v[1]] - verts[t.v[0]]).cross(verts[t.v[2]] - verts[t.v[0]]); // area-weighted + for (int i = 0; i < 3; ++i) + vnormal[t.v[i]] += fn; + } + for (Vec3f &n : vnormal) { + const float l = n.norm(); + n = (l > 1e-12f) ? Vec3f(n / l) : Vec3f(Vec3f::UnitZ()); + } + vheight.assign(verts.size(), 0.f); + vheight_valid.assign(verts.size(), 0); + } + auto height_of = [&](int v) -> float { + if (!vheight_valid[v]) { + vheight[v] = sampler(verts[v], vnormal[v]); + vheight_valid[v] = 1; + } + return vheight[v]; + }; + + // Per-vertex filament index, sampled lazily and cached the same way the heights are. Needs the + // vertex normals, which feature mode also builds - so colour mode builds them when it is on alone. + std::vector vcolor; + std::vector vcolor_valid; + if (color_mode) { + if (vnormal.empty()) { + vnormal.assign(verts.size(), Vec3f::Zero()); + for (const Tri &t : tris) { + const Vec3f fn = (verts[t.v[1]] - verts[t.v[0]]).cross(verts[t.v[2]] - verts[t.v[0]]); + for (int i = 0; i < 3; ++i) + vnormal[t.v[i]] += fn; + } + for (Vec3f &n : vnormal) { + const float l = n.norm(); + n = (l > 1e-12f) ? Vec3f(n / l) : Vec3f(Vec3f::UnitZ()); + } + } + vcolor.assign(verts.size(), -2); // -2 = not sampled yet; -1 = sampled, no colour there + vcolor_valid.assign(verts.size(), 0); + } + auto color_of = [&](int v) -> int { + if (!vcolor_valid[v]) { + vcolor[v] = color(verts[v], vnormal[v]); + vcolor_valid[v] = 1; + } + return vcolor[v]; + }; + + // Sample the input mesh's own vertices up front, in parallel, for the region that is going to be + // refined. A sampler call is a texture fetch plus the projection's trigonometry per layer, and it + // is by far the most expensive thing here - but taken one at a time from inside the refinement + // loop it is also strictly serial. Every one of these vertices is read by the very first scoring + // pass anyway, so doing them together costs nothing extra and hands the work to every core. + // + // Only the region, and only the *initial* vertices: the laziness this replaces exists so that a + // small painted patch on a big model does not pay for the whole model (see height_of()), and that + // still holds. Midpoints created later stay lazy, because they do not exist yet. + if (feature_mode || color_mode) { + std::vector wanted(verts.size(), 0); + for (const Tri &t : tris) + if (refine_region[t.src] != 0) + for (int i = 0; i < 3; ++i) + wanted[size_t(t.v[i])] = 1; + // Each index is touched by exactly one iteration, so the lazy caches can be filled without + // synchronisation - and every value is the one height_of()/color_of() would have produced. + tbb::parallel_for(tbb::blocked_range(0, verts.size()), + [&](const tbb::blocked_range &range) { + for (size_t v = range.begin(); v < range.end(); ++v) { + if (!wanted[v]) + continue; + if (feature_mode) { + vheight[v] = sampler(verts[v], vnormal[v]); + vheight_valid[v] = 1; + } + if (color_mode) { + vcolor[v] = color(verts[v], vnormal[v]); + vcolor_valid[v] = 1; + } + } + }); + if (multi_mode) { + // The level a step crosses: midway through the relief actually present on the region. + float lo = std::numeric_limits::max(), hi = -lo; + for (size_t v = 0; v < verts.size(); ++v) + if (wanted[v]) { lo = std::min(lo, vheight[v]); hi = std::max(hi, vheight[v]); } + step_iso_mm = (lo < hi) ? 0.5f * (lo + hi) : 0.f; + step_dev_mm = (lo < hi) ? 0.4f * (hi - lo) : std::numeric_limits::max(); + } + } + + auto elen_sq = [&](int a, int b) -> float { return (verts[a] - verts[b]).squaredNorm(); }; + + // The one edge of a triangle taken as its "longest": greatest squared length, exact ties broken by + // the smaller (sorted) vertex-index key. Both triangles sharing an edge compute the same key for + // it, so they can never disagree about which of them is longest - the property the conformality + // argument and the LEPP walk's termination both rest on. + auto longest_local = [&](int ti) -> int { + const Tri &t = tris[ti]; + int best = 0; + float bl = elen_sq(t.v[0], t.v[1]); + uint64_t bk = edge_key(t.v[0], t.v[1]); + for (int e = 1; e < 3; ++e) { + const float l = elen_sq(t.v[e], t.v[(e + 1) % 3]); + const uint64_t k = edge_key(t.v[e], t.v[(e + 1) % 3]); + if (l > bl || (l == bl && k < bk)) { + bl = l; + bk = k; + best = e; + } + } + return best; + }; + + // How far the *displaced* surface departs from the flat triangle, sampled across the WHOLE + // triangle - the three edge midpoints and the centroid - not just one edge midpoint. Sampling the + // interior is what catches a hill that sits inside a triangle (the blind spot of an edge-only + // test). Cached per triangle: it can only change when the triangle is split, and then both + // children are fresh entries. + std::vector tri_err; + if (feature_mode) + tri_err.assign(tris.size(), -1.f); + // True when this triangle straddles a colour boundary: its corners, its edge midpoints and its + // centroid do not all take the same filament. The midpoints and centroid matter for the same + // reason they do in detail_error() - a boundary can cross a triangle without separating any two of + // its corners. Cached per triangle; a split invalidates both children. + std::vector tri_color_split; // 0 = unknown, 1 = straddles, 2 = uniform + if (color_mode) + tri_color_split.assign(tris.size(), 0); + auto straddles_color = [&](int ti) -> bool { + if (tri_color_split[ti] != 0) + return tri_color_split[ti] == 1; + const Tri &t = tris[ti]; + const Vec3f pa = verts[t.v[0]], pb = verts[t.v[1]], pc = verts[t.v[2]]; + const Vec3f na = vnormal[t.v[0]], nb = vnormal[t.v[1]], nc = vnormal[t.v[2]]; + const int ca = color_of(t.v[0]); + bool split = color_of(t.v[1]) != ca || color_of(t.v[2]) != ca; + if (!split) { + static const float BARY[4][3] = { { 0.5f, 0.5f, 0.f }, { 0.f, 0.5f, 0.5f }, + { 0.5f, 0.f, 0.5f }, { 1.f / 3, 1.f / 3, 1.f / 3 } }; + for (const auto &w : BARY) { + Vec3f n = w[0] * na + w[1] * nb + w[2] * nc; + const float nl = n.norm(); + n = (nl > 1e-12f) ? Vec3f(n / nl) : na; + if (color(w[0] * pa + w[1] * pb + w[2] * pc, n) != ca) { + split = true; + break; + } + } + } + tri_color_split[ti] = split ? 1 : 2; + return split; + }; + + // The spacing at which the chord test samples a triangle. Features narrower than the resolution + // floor cannot be resolved by refinement anyway, so sampling any finer than that only costs time; + // sampling any coarser is what let thin features fall between the samples. Below the floor's own + // scale it is held at 0.05 mm, since finer is invisible on an FDM part. + const float sample_spacing = std::max(min_edge_length_mm, 0.05f); + + auto detail_error = [&](int ti) -> float { + if (tri_err[ti] >= 0.f) + return tri_err[ti]; + const Tri &t = tris[ti]; + const Vec3f pa = verts[t.v[0]], pb = verts[t.v[1]], pc = verts[t.v[2]]; + const Vec3f na = vnormal[t.v[0]], nb = vnormal[t.v[1]], nc = vnormal[t.v[2]]; + const float ha = height_of(t.v[0]), hb = height_of(t.v[1]), hc = height_of(t.v[2]); + + // A barycentric lattice over the whole triangle, dense enough that no feature wider than the + // sample spacing can hide between two samples. Four fixed samples (the three edge midpoints + // and the centroid) were the previous test, and on a 1 mm starting triangle they left 0.5 mm + // gaps - wider than a grid line or a knurl ridge - so whole features were never seen, the + // triangle scored zero, and it was never refined. The lattice is bounded at 8 subdivisions + // (45 points) so a large triangle does not cost thousands of samples; anything it misses at + // that size is caught once its children are small enough for the lattice to reach it. + const float longest = std::sqrt(std::max({ (pb - pa).squaredNorm(), (pc - pb).squaredNorm(), (pa - pc).squaredNorm() })); + const int n = std::clamp(int(std::ceil(longest / sample_spacing)), 2, 8); + float maxerr = 0.f; + // Whether the mid-level contour crosses an edge with a jump that is a step's worth, walked at + // the sample spacing (the lattice below is too coarse on a long edge: a 1 mm edge gets 8 + // samples, and a 0.15 mm grid line slips between them). Such a triangle is the cutter's. + bool sharp_cross = false; + if (multi_mode) { + const Vec3f *P[3] = { &pa, &pb, &pc }; + const Vec3f *Nn[3] = { &na, &nb, &nc }; + const float H[3] = { ha, hb, hc }; + for (int e = 0; e < 3; ++e) { + const Vec3f &p0 = *P[e], &p1 = *P[(e + 1) % 3], &n0 = *Nn[e], &n1 = *Nn[(e + 1) % 3]; + const int m = std::clamp(int(std::ceil((p1 - p0).norm() / sample_spacing)), 2, 32); + float last = H[e]; + for (int i = 1; i <= m; ++i) { + float h; + if (i == m) h = H[(e + 1) % 3]; + else { + const float t = float(i) / float(m); + Vec3f nn = n0 + (n1 - n0) * t; + const float nl = nn.norm(); + nn = (nl > 1e-12f) ? Vec3f(nn / nl) : n0; + h = sampler(p0 + (p1 - p0) * t, nn); + } + if ((h > step_iso_mm) != (last > step_iso_mm) && std::abs(h - last) > step_dev_mm) { + sharp_cross = true; + break; + } + last = h; + } + if (sharp_cross) + break; + } + } + for (int i = 0; i <= n; ++i) + for (int j = 0; j <= n - i; ++j) { + const int k = n - i - j; + const float wa = float(i) / float(n), wb = float(j) / float(n), wc = float(k) / float(n); + float actual; + if (i == n) actual = ha; + else if (j == n) actual = hb; + else if (k == n) actual = hc; + else { + Vec3f nn = wa * na + wb * nb + wc * nc; + const float nl = nn.norm(); + nn = (nl > 1e-12f) ? Vec3f(nn / nl) : na; + actual = sampler(wa * pa + wb * pb + wc * pc, nn); + // Corners lie on the plane by construction; only the rest carry chord error. + maxerr = std::max(maxerr, std::abs(actual - (wa * ha + wb * hb + wc * hc))); + } + } + // A sharp step crossing the triangle is the cutter's job. Refinement cannot bring such a + // triangle under tolerance - a discontinuity has no chord - and would only carpet the step + // down to the floor, so its chord error is not counted. A feature lying entirely inside the + // triangle crosses no edge and still counts, which is what makes it surface until it does + // cross one. + if (multi_mode && sharp_cross) + maxerr = 0.f; + tri_err[ti] = maxerr; + return maxerr; + }; + + // How many times over its criteria a triangle is: <= 1 means "good enough, leave it alone", and + // the larger the value the more a split buys. Driving the heap with this is what makes a run that + // runs out of budget spend it on the worst offenders instead of wherever a sweep happened to + // reach. Triangles outside the region always score 0 - they are only ever touched by the conformal + // closure below, never refined on their own account. + auto priority = [&](int ti) -> float { + const Tri &t = tris[ti]; + const uint8_t flags = refine_region[t.src]; + if (flags == 0) + return 0.f; + const int le = longest_local(ti); + const float ll = elen_sq(t.v[le], t.v[(le + 1) % 3]); + if (ll <= min_floor_sq) + return 0.f; // at the resolution floor - also what stops a sharp texture step going forever + float p = 0.f; + if (flags & REFINE_PAINTED) { + p = (target_sq > 0.f) ? ll / target_sq : 0.f; + if (feature_mode) { + p = std::max(p, detail_error(ti) / chord_tolerance_mm); + } + // Colour is per facet, so a colour boundary can only be drawn where there are edges along + // it. Length target, floored by min_edge_length_mm above, exactly like the border band. + if (color_mode && straddles_color(ti)) + p = std::max(p, ll / color_sq); + } + // The band straddling the paint's edge, refined by plain edge length. Deliberately *not* run + // through detail_error(): the sampler reports no relief off the paint, so across its edge the + // chord test sees a step and would chase it down to the length floor. Length alone is what this + // band needs - the error it is fixing is the size of the triangles spanning the displacement + // step, not the curvature of anything. + if ((flags & REFINE_BORDER) && border_sq > 0.f) + p = std::max(p, ll / border_sq); + return p; + }; + + // What ordering the heap by. priority() alone saturates: a triangle straddling a hard step in the + // texture scores relief / tolerance however small it gets, so its children always come back to the + // top and the whole budget is spent carpeting step edges down to the floor while a large triangle + // with a moderate error a few millimetres away is never reached. Weighting by area makes the key + // the surface error a split actually removes, which is what a fixed budget should be spent on - + // and once the budget is gone, what remains is spread evenly instead of piled on one feature. + auto tri_area = [&](int ti) -> float { + const Tri &t = tris[ti]; + return 0.5f * (verts[t.v[1]] - verts[t.v[0]]).cross(verts[t.v[2]] - verts[t.v[0]]).norm(); + }; + auto heap_key = [&](int ti, float p) -> float { return p * tri_area(ti); }; + + auto set_nb = [&](int ti, int u, int v, int val) { + if (ti < 0) + return; + Tri &t = tris[ti]; + for (int e = 0; e < 3; ++e) + if ((t.v[e] == u && t.v[(e + 1) % 3] == v) || (t.v[e] == v && t.v[(e + 1) % 3] == u)) { + t.nb[e] = val; + return; + } + }; + + // Bisects triangle `ti` across its local edge `e`, which the caller has established is terminal. + // Both triangles on that edge are split in the one operation, around a single shared midpoint - + // which is exactly why a hanging node (and so a crack) can never appear. `ti` and the opposite + // triangle are each reused as one of their own children, so only two back-pointers in the + // surrounding mesh need repointing. Triangles whose geometry changed are left in `touched`. + std::vector touched; + auto bisect = [&](int ti, int e) -> bool { + const int a = tris[ti].v[e], b = tris[ti].v[(e + 1) % 3], c = tris[ti].v[(e + 2) % 3]; + const int n = tris[ti].nb[e]; + // Locate the shared edge from the far side before touching anything: bailing out half way + // through would be the one way this could leave a crack. + int f = -1; + if (n >= 0) { + for (int k = 0; k < 3; ++k) { + const int u = tris[n].v[k], w = tris[n].v[(k + 1) % 3]; + if ((u == a && w == b) || (u == b && w == a)) { + f = k; + break; + } + } + if (f < 0) { + tris[ti].nb[e] = NB_NONMANIFOLD; // inconsistent adjacency: refuse to split across it + return false; + } + } + + const int m = int(verts.size()); + verts.push_back(0.5f * (verts[a] + verts[b])); + if (feature_mode || color_mode) { + const Vec3f mn = vnormal[a] + vnormal[b]; + const float ml = mn.norm(); + vnormal.push_back(ml > 1e-12f ? Vec3f(mn / ml) : vnormal[a]); + } + if (feature_mode) { + vheight.push_back(0.f); + vheight_valid.push_back(0); + } + if (color_mode) { + vcolor.push_back(-2); + vcolor_valid.push_back(0); + } + + // Near side: ti becomes (a, m, c), the new triangle is (m, b, c). Both keep the original + // a->b->c winding. + const int nb_bc = tris[ti].nb[(e + 1) % 3]; + const int nb_ca = tris[ti].nb[(e + 2) % 3]; + const int src = tris[ti].src; + const int t2 = int(tris.size()); + tris.push_back(Tri{ { m, b, c }, { NB_BOUNDARY, nb_bc, ti }, src }); + { + Tri &t1 = tris[ti]; + t1.v[0] = a; t1.v[1] = m; t1.v[2] = c; + t1.nb[0] = NB_BOUNDARY; t1.nb[1] = t2; t1.nb[2] = nb_ca; + } + set_nb(nb_bc, b, c, t2); // that outer neighbour borders the second child now, not ti + if (feature_mode) { + tri_err.push_back(-1.f); + tri_err[ti] = -1.f; + } + if (color_mode) { + tri_color_split.push_back(0); + tri_color_split[ti] = 0; + } + touched.assign({ ti, t2 }); + + if (n < 0) { + return true; // boundary edge: nothing on the far side to split + } + + // Far side, same shape: n becomes (p, m, d), the new triangle is (m, q, d). + const int p = tris[n].v[f], q = tris[n].v[(f + 1) % 3], d = tris[n].v[(f + 2) % 3]; + const int nb_qd = tris[n].nb[(f + 1) % 3]; + const int nb_dp = tris[n].nb[(f + 2) % 3]; + const int nsrc = tris[n].src; + const int n2 = int(tris.size()); + tris.push_back(Tri{ { m, q, d }, { NB_BOUNDARY, nb_qd, n }, nsrc }); + { + Tri &n1 = tris[n]; + n1.v[0] = p; n1.v[1] = m; n1.v[2] = d; + n1.nb[0] = NB_BOUNDARY; n1.nb[1] = n2; n1.nb[2] = nb_dp; + } + set_nb(nb_qd, q, d, n2); + if (feature_mode) { + tri_err.push_back(-1.f); + tri_err[n] = -1.f; + } + if (color_mode) { + tri_color_split.push_back(0); + tri_color_split[n] = 0; + } + + // Stitch the two sides back together: whichever far child holds `a` borders the near child + // that holds `a`. (Which one that is depends on how n happens to be wound.) + const int side_a = (p == a) ? n : n2; + const int side_b = (p == a) ? n2 : n; + tris[ti].nb[0] = side_a; // near child (a, m, c), edge (a, m) + tris[t2].nb[0] = side_b; // near child (m, b, c), edge (m, b) + set_nb(side_a, a, m, ti); + set_nb(side_b, b, m, t2); + touched.assign({ ti, t2, n, n2 }); + return true; + }; + + // Worst-first. Entries go stale as their triangle is split; a stale entry is harmless - it is + // re-scored on pop and dropped or re-pushed. The ordering is a budget-allocation heuristic only: + // neither correctness nor conformality depends on it. + std::priority_queue> queue; + { + // Scoring the starting mesh means a detail_error() per triangle - four more sampler calls each + // - so it is worth spreading, even though the refinement that follows cannot be. Each entry is + // written by one iteration only, and the caches those calls fill (tri_err, tri_color_split) are + // likewise per triangle, so there is nothing shared to guard. The heap is then built from the + // finished array in index order, which is exactly the order the serial loop pushed in. + std::vector initial(tris.size(), 0.f); + tbb::parallel_for(tbb::blocked_range(0, tris.size()), + [&](const tbb::blocked_range &range) { + for (size_t ti = range.begin(); ti < range.end(); ++ti) + initial[ti] = priority(int(ti)); + }); + for (int ti = 0; ti < int(tris.size()); ++ti) + if (initial[size_t(ti)] > 1.f) + queue.emplace(heap_key(ti, initial[size_t(ti)]), ti); + } + + // Every iteration either drops one satisfied triangle from the queue or performs exactly one + // bisection, and bisections are capped by the triangle budget, so this always terminates. + // Progress is reported against the triangle budget, which is what the loop is bounded by. Polled + // rather than pushed on every bisection: a refinement spends its budget in hundreds of thousands + // of them, and every hook call wakes the UI's idle loop to repaint the notification. + const int start_tris = int(tris.size()); + const int budget_tris = std::max(1, max_triangles - start_tris); + int next_poll = start_tris; + while (!queue.empty() && int(tris.size()) + 2 <= max_triangles) { + if (progress && int(tris.size()) >= next_poll) { + next_poll = int(tris.size()) + std::max(1024, budget_tris / 100); + if (!progress(std::clamp((int(tris.size()) - start_tris) * 100 / budget_tris, 0, 100))) + break; // still conformal - whole bisections only; the caller decides whether to keep it + } + const int ti = queue.top().second; + queue.pop(); + if (priority(ti) <= 1.f) + continue; // stale: already refined past its criteria + + // Longest-Edge Propagation Path: step to the neighbour across the current longest edge for as + // long as that neighbour has a strictly longer one, then bisect the terminal edge we land on. + // Length strictly increases along the path, so it cannot cycle. + int cur = ti, split_edge = -1; + for (size_t guard = 0; guard <= tris.size(); ++guard) { + const int le = longest_local(cur); + const int nbr = tris[cur].nb[le]; + if (nbr == NB_NONMANIFOLD) + break; // cannot split across it without tearing the mesh: give up on this path + if (nbr == NB_BOUNDARY) { + split_edge = le; // boundary longest edge -> terminal + break; + } + const int nle = longest_local(nbr); + if (edge_key(tris[nbr].v[nle], tris[nbr].v[(nle + 1) % 3]) == + edge_key(tris[cur].v[le], tris[cur].v[(le + 1) % 3])) { + split_edge = le; // mutual longest edge -> terminal + break; + } + cur = nbr; + } + if (split_edge < 0 || !bisect(cur, split_edge)) + continue; // ti sits in a non-manifold neighbourhood and cannot be refined safely + + for (const int t : touched) + if (const float p = priority(t); p > 1.f) + queue.emplace(heap_key(t, p), t); + // The bisection may have been a step on the way to ti rather than ti itself, in which case ti + // is not in `touched` and has to go back on the heap to be walked again. + if (cur != ti) + if (const float p = priority(ti); p > 1.f) + queue.emplace(heap_key(ti, p), ti); + } + + return emit(); +} + +namespace { + +// Ear clipping of a simple polygon given counter-clockwise in 2D; `pts` are indexed by `poly`. Small +// polygons only (a triangle clipped by a few contour polylines), so the plain O(n^2) form is enough. +// A vertex that lies on a candidate ear's boundary does not block it, and when no ear can be found +// (numerically degenerate input) the flattest vertex is dropped rather than fanning blindly, which +// on a concave region would put triangles outside it; the zero-area triangle keeps the edges paired. +void ear_clip(const std::vector &pts, std::vector poly, const std::function &emit) +{ + auto cross2 = [](const Vec2f &o, const Vec2f &a, const Vec2f &b) { + return (a.x() - o.x()) * (b.y() - o.y()) - (a.y() - o.y()) * (b.x() - o.x()); + }; + // A corner flatter than this is collinear: several vertices along one edge of the triangle. Such + // a corner is never an ear - the zero-area triangle would lie on the edge, where the neighbour, + // seeing the same points from its side, may emit the same one. + float flat = 0.f; + { + Vec2f lo = pts[size_t(poly[0])], hi = lo; + for (int id : poly) { lo = lo.cwiseMin(pts[size_t(id)]); hi = hi.cwiseMax(pts[size_t(id)]); } + flat = 1e-5f * (hi - lo).squaredNorm(); + } + while (poly.size() > 3) { + const size_t n = poly.size(); + bool found = false; + for (size_t i = 0; i < n && !found; ++i) { + const int ia = poly[(i + n - 1) % n], ib = poly[i], ic = poly[(i + 1) % n]; + const Vec2f &a = pts[size_t(ia)], &b = pts[size_t(ib)], &c = pts[size_t(ic)]; + const float area2 = cross2(a, b, c); + if (area2 <= flat) + continue; // reflex or flat corner + const float eps = 1e-6f * area2; + bool clean = true; + size_t on_base = 0; + for (size_t j = 0; j < n && clean; ++j) { + const int ij = poly[j]; + if (ij == ia || ij == ib || ij == ic) + continue; + const Vec2f &p = pts[size_t(ij)]; + clean = !(cross2(a, b, p) > eps && cross2(b, c, p) > eps && cross2(c, a, p) > eps); + // On the ear's base, between its ends: a run of vertices along the base, as when + // several sit on one edge of the triangle. + if (std::abs(cross2(c, a, p)) <= flat && (p - c).dot(a - c) > 0.f && (p - a).dot(c - a) > 0.f) + ++on_base; + } + if (!clean) + continue; + if (on_base > 0) { + // The base carries other vertices. When those are all that is left of the polygon, + // in order from c round to a, fan them from b and finish; otherwise the ear is not + // clear of the rest of the polygon. + if (on_base + 3 != n) + continue; + for (size_t j = 0; j + 1 < n - 1; ++j) { + const int u = poly[(i + 1 + j) % n], v = poly[(i + 2 + j) % n]; + emit(ib, u, v); + } + poly.clear(); + return; + } + emit(ia, ib, ic); + poly.erase(poly.begin() + long(i)); + found = true; + } + if (!found) { + // Clip the smallest convex corner anyway, or failing that the flattest: a thin triangle + // keeps every edge accounted for, and a convex one at least stays on the polygon's side. + size_t pick = 0; + float best = std::numeric_limits::max(); + bool convex_found = false; + for (size_t i = 0; i < n; ++i) { + const Vec2f &a = pts[size_t(poly[(i + n - 1) % n])], &b = pts[size_t(poly[i])], &c = pts[size_t(poly[(i + 1) % n])]; + const float m = cross2(a, b, c); + if (m > flat && (!convex_found || m < best)) { best = m; pick = i; convex_found = true; } + else if (!convex_found && std::abs(m) < best) { best = std::abs(m); pick = i; } + } + emit(poly[(pick + n - 1) % n], poly[pick], poly[(pick + 1) % n]); + poly.erase(poly.begin() + long(pick)); + } + } + if (poly.size() == 3) + emit(poly[0], poly[1], poly[2]); +} + +// Douglas-Peucker on an open polyline; endpoints always survive. +void simplify_polyline(const std::vector &in, float tol, std::vector &out) +{ + out.clear(); + if (in.size() < 3) { out = in; return; } + std::vector keep(in.size(), 0); + keep.front() = keep.back() = 1; + std::vector> stack = { { 0, in.size() - 1 } }; + while (!stack.empty()) { + const auto [i0, i1] = stack.back(); + stack.pop_back(); + if (i1 <= i0 + 1) + continue; + const Vec2f d = in[i1] - in[i0]; + const float len = d.norm(); + float best = -1.f; + size_t bi = i0; + for (size_t i = i0 + 1; i < i1; ++i) { + const Vec2f r = in[i] - in[i0]; + const float dist = (len > 1e-12f) ? std::abs(d.x() * r.y() - d.y() * r.x()) / len : r.norm(); + if (dist > best) { best = dist; bi = i; } + } + if (best > tol) { + keep[bi] = 1; + stack.push_back({ i0, bi }); + stack.push_back({ bi, i1 }); + } + } + for (size_t i = 0; i < in.size(); ++i) + if (keep[i]) out.push_back(in[i]); +} + +} // namespace + +namespace { + +// The cutter's view of one crossing of the mid-level contour with a mesh edge. +struct Crossing +{ + Vec3f p, n; + float t; // parameter from the edge's lower vertex id toward the higher + int a = -1, b = -1; // the edge, lower vertex id first + float sin_min = 1.f; // sine of the smallest angle a contour makes with the edge + float dt_max = std::numeric_limits::max(); // room along the edge before a copy could + // pass the copies of the contour's next vertex + bool used = false; // some cut triangle's contour ends here + bool no_seam = false; // a contour joins it straight to its neighbour on the edge: nothing between + bool sharp = false; + float thick = 0.f; // narrower side of the feature at the crossing, along the gradient + int single = -1, lo = -1, hi = -1; + int near_a = -1, near_b = -1; // the copy on a's side and on b's, when doubled +}; +struct Edge +{ + std::vector xs; // sorted by t +}; + +inline uint64_t step_edge_key(int a, int b) +{ + if (a > b) std::swap(a, b); + return (uint64_t(uint32_t(a)) << 32) | uint32_t(b); +} + +// What cut_mesh_at_steps() learns about the mesh before it cuts anything: the vertex heights (and the +// nudged vertex positions), the crossings on every painted edge, and the verdict `ok` - whether this +// is a step texture worth cutting at all. The verdict is what the prepare path probes on the coarse +// mesh before it chooses how to refine, so refinement and cut agree on which textures are stepped. +struct StepScan +{ + bool ok = false; + std::vector pos, vnormal; + std::vector vh; + float iso = 0.f, range = 0.f; + std::unordered_map edges; + + std::pair sample_between(int a, int b, float t) const + { + const Vec3f p = pos[size_t(a)] + (pos[size_t(b)] - pos[size_t(a)]) * t; + Vec3f n = vnormal[size_t(a)] + (vnormal[size_t(b)] - vnormal[size_t(a)]) * t; + const float l = n.norm(); + n = (l > 1e-12f) ? Vec3f(n / l) : vnormal[size_t(a)]; + return std::make_pair(p, n); + } + bool side_of(float h) const { return h > iso; } +}; + +StepScan scan_steps(const indexed_triangle_set &mesh, const std::vector ®ion, + const HeightFieldSampler &sampler, float step_width_mm, float seam_gap_mm, + float min_feature_mm, bool nudge) +{ + StepScan scan; + const size_t nv = mesh.vertices.size(), nt = mesh.indices.size(); + if (!sampler || region.size() != nt || step_width_mm <= 0.f) + return scan; + + // 1. Heights at the vertices of the cuttable triangles, along the same normals the bake will use. + // Vertex positions are worked on in `pos`: a vertex that happens to sit within a texel of a step + // is nudged off it below, and everything from the edge march to the output uses the nudged place. + std::vector &pos = scan.pos; + std::vector &vnormal = scan.vnormal; + pos = mesh.vertices; + vnormal = texture_displacement_vertex_normals(mesh); + std::vector wanted(nv, 0); + for (size_t t = 0; t < nt; ++t) + if (region[t] != 0) + for (int k = 0; k < 3; ++k) + wanted[size_t(mesh.indices[t][k])] = 1; + std::vector &vh = scan.vh; + vh.assign(nv, 0.f); + tbb::parallel_for(tbb::blocked_range(0, nv), [&](const tbb::blocked_range &r) { + for (size_t v = r.begin(); v < r.end(); ++v) + if (wanted[v]) + vh[v] = sampler(pos[v], vnormal[v]); + }); + float hmin = std::numeric_limits::max(), hmax = -hmin; + for (size_t v = 0; v < nv; ++v) + if (wanted[v]) { hmin = std::min(hmin, vh[v]); hmax = std::max(hmax, vh[v]); } + const float range = scan.range = hmax - hmin; + if (!(range > 1e-6f)) + return scan; + const float iso = scan.iso = 0.5f * (hmin + hmax); + // Only a texture that is mostly steps is cut: one whose heights sit at two levels with little in + // between (a grid, a knurl, a logo). On a smooth or noisy relief the mid-level contour runs through + // every triangle without being a step anywhere, and cutting along it would only multiply the + // triangles; the chord-based refinement is the right tool there. Judged from the vertex heights, + // which sample the whole painted surface. + { + size_t near_level = 0, total = 0; + for (size_t v = 0; v < nv; ++v) + if (wanted[v]) { ++total; near_level += std::abs(vh[v] - iso) > 0.35f * range; } + if (total == 0 || double(near_level) < 0.6 * double(total)) + return scan; + } + + // 1b. A vertex inside the blend of a sharp step - the texture's bilinear ramp is a texel wide - would + // be displaced to a height between the two sides, and every triangle at it would ramp. Such a vertex + // is nudged along the surface away from the step, down its own side's slope, until it samples a + // pure value; a step's width or so. Only where the surface is flat around it: a vertex on a crease + // of the model (a box edge) stays where it is. Skipped by a probe, which only wants the verdict. + if (nudge) { + std::vector creased(nv, 0); + std::vector first_normal(nv, Vec3f::Zero()); + std::vector shortest(nv, std::numeric_limits::max()); // shortest edge at the vertex + for (size_t t = 0; t < nt; ++t) { + const auto &tri = mesh.indices[t]; + const Vec3f &a = pos[size_t(tri[0])]; + Vec3f fn = (pos[size_t(tri[1])] - a).cross(pos[size_t(tri[2])] - a); + const float fl = fn.norm(); + if (fl < 1e-12f) continue; + fn /= fl; + for (int k = 0; k < 3; ++k) { + Vec3f &f0 = first_normal[size_t(tri[k])]; + if (f0.isZero()) f0 = fn; + else if (f0.dot(fn) < 0.985f) creased[size_t(tri[k])] = 1; // ~10 degrees + const float el = (pos[size_t(tri[(k + 1) % 3])] - pos[size_t(tri[k])]).norm(); + shortest[size_t(tri[k])] = std::min(shortest[size_t(tri[k])], el); + shortest[size_t(tri[(k + 1) % 3])] = std::min(shortest[size_t(tri[(k + 1) % 3])], el); + } + } + tbb::parallel_for(tbb::blocked_range(0, nv), [&](const tbb::blocked_range &r) { + for (size_t v = r.begin(); v < r.end(); ++v) { + if (!wanted[v] || creased[v] || std::abs(vh[v] - iso) > 0.47f * range) + continue; + const Vec3f &n = vnormal[v]; + const Vec3f ax = (std::abs(n.x()) < 0.9f) ? Vec3f::UnitX() : Vec3f::UnitY(); + const Vec3f t1 = n.cross(ax).normalized(), t2 = n.cross(t1).normalized(); + const float d = step_width_mm; + const float gx = sampler(pos[v] + t1 * d, n) - sampler(pos[v] - t1 * d, n); + const float gy = sampler(pos[v] + t2 * d, n) - sampler(pos[v] - t2 * d, n); + Vec3f g = t1 * gx + t2 * gy; + const float gl = g.norm(); + if (gl < 0.25f * range) + continue; // not at a step: a slope, which the chord test handles + g /= gl; + // Down the slope for a low vertex, up it for a high one, in steps of half a texel, and + // never by more than a fraction of the shortest edge at the vertex: on a finely refined + // mesh a texel-sized move would fold the triangles around it. + const Vec3f dir = (vh[v] > iso) ? g : Vec3f(-g); + Vec3f best = pos[v]; + float bh = vh[v]; + const float limit = std::min(2.f * step_width_mm, 0.3f * shortest[v]); + for (int i = 1; i <= 8; ++i) { + const float dist = 0.25f * step_width_mm * float(i); + if (dist > limit) + break; + const Vec3f q = pos[v] + dir * dist; + const float h = sampler(q, n); + if ((h > iso) != (vh[v] > iso)) + break; // crossed the step: the nudge would change the vertex's side + best = q; bh = h; + if (std::abs(h - iso) > 0.48f * range) + break; + } + if (std::abs(bh - iso) > std::abs(vh[v] - iso)) { pos[v] = best; vh[v] = bh; } + } + }); + } + + const auto sample_between = [&scan](int a, int b, float t) { return scan.sample_between(a, b, t); }; + const auto side_of = [&scan](float h) { return scan.side_of(h); }; + + // 2. Crossings on the edges, found on the field itself - on a 1 mm edge over a texture with 30 um + // texels, interpolating the endpoint heights could put a step anywhere along the edge. Both + // triangles on an edge see the same crossings, which is what keeps the cut conformal. + auto &edges = scan.edges; + edges.reserve(nt); + + const auto find_crossings = [&](int a, int b, Edge &edge) { + if (a > b) std::swap(a, b); + const float len = (pos[size_t(b)] - pos[size_t(a)]).norm(); + const int steps = std::clamp(int(std::ceil(len / (0.5f * step_width_mm))), 4, 128); + float t0 = 0.f, h0 = vh[size_t(a)] - iso; + for (int i = 1; i <= steps; ++i) { + const float ti = float(i) / float(steps); + const auto [pp, nn] = sample_between(a, b, ti); + const float hi_ = sampler(pp, nn) - iso; + if (h0 * hi_ < 0.f || (hi_ == 0.f && h0 != 0.f)) { + float lo_t = t0, hi_t = ti, lo_h = h0; + for (int k = 0; k < 10; ++k) { + const float tm = 0.5f * (lo_t + hi_t); + const auto [pm, nm] = sample_between(a, b, tm); + if (lo_h * (sampler(pm, nm) - iso) <= 0.f) hi_t = tm; else { lo_t = tm; } + } + Crossing c; + c.a = a; c.b = b; + // Never exactly on a vertex: that would make a zero-area triangle of the split. + c.t = std::clamp(0.5f * (lo_t + hi_t), 1e-3f, 1.f - 1e-3f); + std::tie(c.p, c.n) = sample_between(a, b, c.t); + // Sharp means the field changes by at least half its range across `step_width_mm` in + // the steepest tangent direction - a step, not a slope that merely passes mid-level. + const Vec3f ax = (std::abs(c.n.x()) < 0.9f) ? Vec3f::UnitX() : Vec3f::UnitY(); + const Vec3f t1v = c.n.cross(ax).normalized(), t2v = c.n.cross(t1v).normalized(); + const float d = 0.5f * step_width_mm; + const float gx = sampler(c.p + t1v * d, c.n) - sampler(c.p - t1v * d, c.n); + const float gy = sampler(c.p + t2v * d, c.n) - sampler(c.p - t2v * d, c.n); + const float gl = std::sqrt(gx * gx + gy * gy); + c.sharp = gl >= 0.5f * range; + // How wide the feature is on either side of the step, along the gradient: the + // distance until the field returns to mid-level, in steps of half the step width, up + // to `thick_max`. A texture whose features are only a few texels wide has no pure + // interior for a seam copy to land in and is left to refinement (see the gate below). + { + const float thick_max = 4.f * step_width_mm; + c.thick = thick_max; + if (gl > 1e-12f) { + const Vec3f g = (t1v * gx + t2v * gy) / gl; + for (int sgn = -1; sgn <= 1; sgn += 2) { + const bool high = sgn > 0; // uphill along the gradient + for (int i = 1; i <= 8; ++i) { + const float dist = 0.5f * step_width_mm * float(i); + if (dist >= c.thick) + break; + if (side_of(sampler(c.p + g * (float(sgn) * dist), c.n)) != high) { + c.thick = std::min(c.thick, dist); + break; + } + } + } + } + } + edge.xs.push_back(c); + } + t0 = ti; h0 = hi_; + } + // A pair of crossings closer than the smallest feature the mesh is meant to carry is a feature + // too thin to print, and one closer than the seam gap has no room for two seams: dropping both + // leaves the surface flat there, at the side around it. + const float thinnest = std::max(min_feature_mm, seam_gap_mm); + for (size_t i = 0; i + 1 < edge.xs.size();) { + if ((edge.xs[i + 1].t - edge.xs[i].t) * len < thinnest) + edge.xs.erase(edge.xs.begin() + long(i), edge.xs.begin() + long(i) + 2); + else + ++i; + } + // Parity: the endpoints' sides say whether the count must be odd or even; the march starts and + // ends on the vertex heights themselves, so a mismatch is a numerical accident. Drop the lot + // then - a triangle split on a wrong count is worse than a ramp there. + const bool odd_expected = side_of(vh[size_t(a)]) != side_of(vh[size_t(b)]); + if ((edge.xs.size() % 2 == 1) != odd_expected) + edge.xs.clear(); + }; + + // Edges to examine, in a fixed order so the crossings can be found in parallel. + std::vector edge_keys; + for (size_t t = 0; t < nt; ++t) { + if (region[t] == 0) + continue; + const auto &tri = mesh.indices[t]; + for (int e = 0; e < 3; ++e) + edge_keys.push_back(step_edge_key(tri[e], tri[(e + 1) % 3])); + } + std::sort(edge_keys.begin(), edge_keys.end()); + edge_keys.erase(std::unique(edge_keys.begin(), edge_keys.end()), edge_keys.end()); + std::vector found(edge_keys.size()); + tbb::parallel_for(tbb::blocked_range(0, edge_keys.size()), [&](const tbb::blocked_range &r) { + for (size_t i = r.begin(); i < r.end(); ++i) + find_crossings(int(edge_keys[i] >> 32), int(uint32_t(edge_keys[i])), found[i]); + }); + size_t crossings = 0, sharp = 0; + double thick_sum = 0.; + for (size_t i = 0; i < edge_keys.size(); ++i) { + for (const Crossing &c : found[i].xs) { ++crossings; sharp += c.sharp; thick_sum += c.thick; } + edges.emplace(edge_keys[i], std::move(found[i])); + } + // The mid-level contour has to be a step nearly everywhere it is crossed, or this is not a step + // texture: a noisy relief that happens to sit at two levels is crossed all over, mostly gently. + if (crossings == 0 || double(sharp) < 0.8 * double(crossings)) + return scan; + // And the features have to be wider than the step itself. A texture whose blobs are only a few + // texels across (noise, a fine grain) is bimodal and sharp at every crossing, yet has no pure + // interior for the seam copies to land in, and its contour runs through every triangle: cutting it + // doubles the triangle count for walls the same size as the interpolation blur. Judged from the + // mean feature thickness at the crossings, measured in step widths (2.25 = four to five texels). + if (thick_sum < 2.25 * double(step_width_mm) * double(crossings)) + return scan; + + scan.ok = true; + return scan; + +} + +} // namespace + +bool texture_has_steps_to_cut(const indexed_triangle_set &mesh, const std::vector ®ion, + const HeightFieldSampler &sampler, float step_width_mm, float seam_gap_mm, + float min_feature_mm) +{ + return scan_steps(mesh, region, sampler, step_width_mm, seam_gap_mm, min_feature_mm, /*nudge*/ false).ok; +} + +indexed_triangle_set cut_mesh_at_steps(const indexed_triangle_set &mesh, const std::vector ®ion, + const HeightFieldSampler &sampler, float step_width_mm, + float seam_gap_mm, float min_feature_mm, std::vector *out_source, + size_t *out_cut_count) +{ + const size_t nv = mesh.vertices.size(), nt = mesh.indices.size(); + if (out_cut_count) + *out_cut_count = 0; + const auto passthrough = [&]() { + if (out_source) { + out_source->resize(nt); + std::iota(out_source->begin(), out_source->end(), 0); + } + return mesh; + }; + if (!sampler || region.size() != nt || step_width_mm <= 0.f) + return passthrough(); + + StepScan scan = scan_steps(mesh, region, sampler, step_width_mm, seam_gap_mm, min_feature_mm, /*nudge*/ true); + if (!scan.ok) + return passthrough(); + std::vector &pos = scan.pos; + const std::vector &vnormal = scan.vnormal; + std::vector &vh = scan.vh; + const float iso = scan.iso, range = scan.range; + auto &edges = scan.edges; + const auto sample_between = [&scan](int a, int b, float t) { return scan.sample_between(a, b, t); }; + const auto side_of = [&scan](float h) { return scan.side_of(h); }; + const auto edge_key = [](int a, int b) { return step_edge_key(a, b); }; + (void) vnormal; (void) range; (void) iso; + + // 3. Per triangle: the perimeter as a loop of corners and crossings, and the contour inside the + // triangle traced from a local raster of the field (marching squares at half the step width), as + // polylines from one crossing to another. The trace is what makes a corner of the pattern come out + // as a corner instead of a chord clipping it, and what pairs the crossings up - no guessing. A + // polyline is kept only when its two ends land on distinct crossings the edge march found; + // otherwise the triangle is split at its crossings without a seam (the neighbours' cuts still meet + // no T-junction) and the surface ramps there. Unpainted neighbours of a crossed edge are split the + // same way. + struct Chain + { + int i = -1, j = -1; // crossing indices in the loop, from i to j + std::vector pts; // interior vertices, from i toward j + std::vector lo, hi; // output copies of the interior vertices (same when single) + float clearance = std::numeric_limits::max(); // distance to the nearest other contour + }; + struct Loop + { + std::vector ids; // perimeter entries: corner vertex ids (>= 0) or -(1 + crossing index) + std::vector xs; // crossings in perimeter order + std::vector xpos; // their positions in ids + std::vector chains; + std::vector partner; // per crossing: the crossing its chain leads to + std::vector chain_of; + std::vector no_seam; // crossings joined to their neighbour on the edge with nothing between + Vec3f N = Vec3f::Zero(); + bool ok = false; + }; + const auto walk_edge = [&](int a, int b, std::vector &out) { + auto it = edges.find(edge_key(a, b)); + if (it == edges.end()) return; + Edge &edge = it->second; + if (a < b) for (auto &c : edge.xs) out.push_back(&c); + else for (auto ci = edge.xs.rbegin(); ci != edge.xs.rend(); ++ci) out.push_back(&*ci); + }; + std::vector loops(nt); + for (size_t t = 0; t < nt; ++t) { + const auto &tri = mesh.indices[t]; + Loop &L = loops[t]; + for (int e = 0; e < 3; ++e) { + L.ids.push_back(tri[e]); + std::vector on_edge; + walk_edge(tri[e], tri[(e + 1) % 3], on_edge); + for (Crossing *c : on_edge) { + L.xpos.push_back(int(L.ids.size())); + L.ids.push_back(-(1 + int(L.xs.size()))); + L.xs.push_back(c); + } + } + const Vec3f &a0 = pos[size_t(tri[0])]; + L.N = (pos[size_t(tri[1])] - a0).cross(pos[size_t(tri[2])] - a0); + const float nl = L.N.norm(); + if (nl > 1e-12f) L.N /= nl; + } + + const float cell = 0.5f * step_width_mm; + const auto trace = [&](size_t t) { + Loop &L = loops[t]; + const int n = int(L.xs.size()); + const auto &tri = mesh.indices[t]; + if (n == 0 || region[t] == 0) + return; + if (n % 2 != 0 || L.N.squaredNorm() < 0.5f) + return; + // Local frame on the triangle's plane; the loop runs counter-clockwise in it. + const Vec3f &A = pos[size_t(tri[0])], &B = pos[size_t(tri[1])], &C = pos[size_t(tri[2])]; + const Vec3f U = (B - A).normalized(), V = L.N.cross(U); + const auto to2 = [&](const Vec3f &p) { return Vec2f((p - A).dot(U), (p - A).dot(V)); }; + const auto to3 = [&](const Vec2f &q) { return Vec3f(A + U * q.x() + V * q.y()); }; + const Vec2f a2 = to2(A), b2 = to2(B), c2 = to2(C); + const float det = (b2.x() - a2.x()) * (c2.y() - a2.y()) - (c2.x() - a2.x()) * (b2.y() - a2.y()); + if (std::abs(det) < 1e-12f) + return; + + // A barycentric lattice over the triangle, dense enough that its boundary rows are at least as + // fine as the edge march. Its boundary points take their side from the march - the lower + // corner's side, flipped at every crossing passed - so the contour traced through the lattice + // ends exactly at the crossings the neighbours share; no clipping, no matching. A conflict + // between that and the far corner's own side (an edge whose crossings the march dropped for + // parity) leaves the triangle uncut. + const float longest = std::sqrt(std::max({ (b2 - a2).squaredNorm(), (c2 - b2).squaredNorm(), (a2 - c2).squaredNorm() })); + const int N = std::clamp(int(std::ceil(longest / cell)), 4, 96); + const float h = longest / float(N); + const int W = N + 1; + // Lattice point (j, k): A + (B - A) j/N + (C - A) k/N, j + k <= N. Edge 0 (A->B) is k == 0, + // edge 1 (B->C) is j + k == N, edge 2 (C->A) is j == 0. + const auto at2 = [&](int j, int k) { return Vec2f(a2 + (b2 - a2) * (float(j) / float(N)) + (c2 - a2) * (float(k) / float(N))); }; + std::vector f(size_t(W) * size_t(W), 0.f); + std::vector sign(size_t(W) * size_t(W), 0); + const auto id = [&](int j, int k) { return size_t(k) * size_t(W) + size_t(j); }; + for (int k = 0; k <= N; ++k) + for (int j = 0; j + k <= N; ++j) { + const float wb = float(j) / float(N), wc = float(k) / float(N), wa = 1.f - wb - wc; + Vec3f nn = wa * vnormal[size_t(tri[0])] + wb * vnormal[size_t(tri[1])] + wc * vnormal[size_t(tri[2])]; + const float nl = nn.norm(); + nn = (nl > 1e-12f) ? Vec3f(nn / nl) : L.N; + const float v = sampler(A + (B - A) * wb + (C - A) * wc, nn) - iso; + f[id(j, k)] = v; + sign[id(j, k)] = v > 0.f ? 1 : -1; + } + // Boundary sides from the march. Each edge walks from its first corner in loop order; the + // crossings on it are in that order in L.xs. Position along edge e of lattice point m/N. + const auto edge_point = [&](int e, int m) -> std::pair { // (j, k) + return e == 0 ? std::make_pair(m, 0) : e == 1 ? std::make_pair(N - m, m) : std::make_pair(0, N - m); + }; + // The crossings of edge e, with their parameter from the edge's first corner in loop order. + std::vector>> edge_x(3); + { + int e = -1, k = 0; + for (int idv : L.ids) { + if (idv >= 0) { ++e; continue; } + const Crossing &c = *L.xs[size_t(k)]; + // c.t runs from the lower vertex id; the loop walks tri[e] -> tri[(e + 1) % 3]. + const float u = (tri[e] < tri[(e + 1) % 3]) ? c.t : 1.f - c.t; + edge_x[size_t(e)].push_back({ u, k }); + ++k; + } + } + for (int e = 0; e < 3; ++e) { + int8_t s = side_of(vh[size_t(tri[e])]) ? 1 : -1; + size_t next = 0; + for (int m = 0; m <= N; ++m) { + const float u = float(m) / float(N); + while (next < edge_x[size_t(e)].size() && edge_x[size_t(e)][next].first < u) { s = -s; ++next; } + const auto [j, k] = edge_point(e, m); + sign[id(j, k)] = s; + // Keep the value on the march's side of zero, so interpolation toward the interior + // does not put a crossing where the march has none - and clear of zero, so a contour + // that does pass between this point and the next row is not put on the boundary. + if ((f[id(j, k)] > 0.f) != (s > 0)) + f[id(j, k)] = float(s) * std::max(std::abs(f[id(j, k)]), 0.05f * range); + } + if ((s > 0) != side_of(vh[size_t(tri[(e + 1) % 3])])) + return; // parity conflict on this edge + } + + // Marching triangles. A contour point lives on a lattice edge, keyed by its lower point and + // direction: 0 (j,k)-(j+1,k), 1 (j,k)-(j,k+1), 2 (j+1,k)-(j,k+1). A boundary lattice edge with + // a sign change carries exactly the march crossing in its interval, and is that crossing. + std::vector pts; + std::vector pt_x; // crossing index, or -1 for an interior point + std::unordered_map point_of; + std::vector> link; + bool bad = false; + const auto point_on = [&](int j, int k, int dir) -> int { + const uint32_t key = (uint32_t(j) << 16) | (uint32_t(k) << 2) | uint32_t(dir); + auto it = point_of.find(key); + if (it != point_of.end()) + return it->second; + // Endpoints (j0, k0) and (jb, kb). + const int j0 = dir == 2 ? j + 1 : j, k0 = k; + const int jb = dir == 0 ? j + 1 : j, kb = dir == 0 ? k : k + 1; + int xk = -1; + Vec2f p; + int e = -1; + float u0 = 0.f, u1 = 0.f; + if (k0 == 0 && kb == 0) { e = 0; u0 = float(j0) / float(N); u1 = float(jb) / float(N); } + else if (j0 + k0 == N && jb + kb == N) { e = 1; u0 = float(k0) / float(N); u1 = float(kb) / float(N); } + else if (j0 == 0 && jb == 0) { e = 2; u0 = float(N - k0) / float(N); u1 = float(N - kb) / float(N); } + if (e >= 0) { + const float lo = std::min(u0, u1), hi = std::max(u0, u1); + for (const auto &[u, k_] : edge_x[size_t(e)]) + if (u >= lo && u < hi) { if (xk >= 0) bad = true; xk = k_; } + if (xk < 0) { bad = true; p = 0.5f * (at2(j0, k0) + at2(jb, kb)); } + else p = to2(L.xs[size_t(xk)]->p); + } else { + const float fa = f[id(j0, k0)], fb = f[id(jb, kb)]; + const float s = (fa != fb) ? std::clamp(fa / (fa - fb), 0.f, 1.f) : 0.5f; + p = at2(j0, k0) + (at2(jb, kb) - at2(j0, k0)) * s; + } + const int pid = int(pts.size()); + pts.push_back(p); + pt_x.push_back(xk); + link.push_back({ -1, -1 }); + point_of.emplace(key, pid); + return pid; + }; + const auto connect = [&](int p, int q) { + for (int *slot : { &link[size_t(p)][0], &link[size_t(p)][1] }) + if (*slot < 0) { *slot = q; break; } + for (int *slot : { &link[size_t(q)][0], &link[size_t(q)][1] }) + if (*slot < 0) { *slot = p; break; } + }; + // Up triangle (j,k),(j+1,k),(j,k+1): edges dir 0 at (j,k), dir 1 at (j,k), dir 2 at (j,k). + // Down triangle (j+1,k),(j+1,k+1),(j,k+1): dir 1 at (j+1,k), dir 0 at (j,k+1), dir 2 at (j,k). + for (int k = 0; k < N; ++k) + for (int j = 0; j + k < N; ++j) { + { + const int8_t s0_ = sign[id(j, k)], s1_ = sign[id(j + 1, k)], s2_ = sign[id(j, k + 1)]; + if (!(s0_ == s1_ && s1_ == s2_)) { + std::vector ends; + if (s0_ != s1_) ends.push_back(point_on(j, k, 0)); + if (s0_ != s2_) ends.push_back(point_on(j, k, 1)); + if (s1_ != s2_) ends.push_back(point_on(j, k, 2)); + if (ends.size() == 2) connect(ends[0], ends[1]); + } + } + if (j + k + 1 < N) { + const int8_t s0_ = sign[id(j + 1, k)], s1_ = sign[id(j + 1, k + 1)], s2_ = sign[id(j, k + 1)]; + if (!(s0_ == s1_ && s1_ == s2_)) { + std::vector ends; + if (s0_ != s1_) ends.push_back(point_on(j + 1, k, 1)); + if (s1_ != s2_) ends.push_back(point_on(j, k + 1, 0)); + if (s0_ != s2_) ends.push_back(point_on(j, k, 2)); + if (ends.size() == 2) connect(ends[0], ends[1]); + } + } + } + if (bad) + return; + + // Chains from every crossing to the crossing it leads to. Closed loops never touch the + // boundary and are left alone - refinement resolves a feature that sits entirely inside a + // triangle. + std::vector matched(static_cast(n), -1); + std::vector chains; + std::vector> raws; // the traced polyline of each chain, crossing to crossing + std::vector seen(pts.size(), 0); + for (size_t s = 0; s < pts.size(); ++s) { + if (seen[s] || pt_x[s] < 0) + continue; + std::vector full; + int prev = -1, cur = int(s); + while (cur >= 0 && !seen[size_t(cur)]) { + seen[size_t(cur)] = 1; + full.push_back(pts[size_t(cur)]); + if (pt_x[size_t(cur)] >= 0 && cur != int(s)) + break; + const int nx0 = link[size_t(cur)][0], nx1 = link[size_t(cur)][1]; + const int next = (nx0 != prev) ? nx0 : nx1; + prev = cur; cur = next; + } + if (cur < 0 || pt_x[size_t(cur)] < 0 || cur == int(s)) + return; // a contour that starts at a crossing and ends nowhere: the lattice is inconsistent + Chain ch; + ch.i = pt_x[s]; + ch.j = pt_x[size_t(cur)]; + if (matched[size_t(ch.i)] >= 0 || matched[size_t(ch.j)] >= 0 || ch.i == ch.j) + return; + matched[size_t(ch.i)] = ch.j; + matched[size_t(ch.j)] = ch.i; + chains.push_back(std::move(ch)); + raws.push_back(std::move(full)); + } + // How close each contour comes to another: two sides of a thin feature. Simplifying either by + // the usual tolerance, or offsetting its copies by the usual gap, could then push it across + // the other, so both are scaled to the clearance. Segment-to-segment over every pair. + { + const auto seg_dist = [](const Vec2f &a, const Vec2f &b, const Vec2f &c, const Vec2f &d) { + const auto pt_seg = [](const Vec2f &q, const Vec2f &u, const Vec2f &v) { + const Vec2f uv = v - u; + const float t = std::clamp((q - u).dot(uv) / std::max(uv.squaredNorm(), 1e-12f), 0.f, 1.f); + return (q - (u + uv * t)).norm(); + }; + return std::min({ pt_seg(a, c, d), pt_seg(b, c, d), pt_seg(c, a, b), pt_seg(d, a, b) }); + }; + for (size_t x = 0; x < raws.size(); ++x) + for (size_t y = x + 1; y < raws.size(); ++y) { + float best = std::min(chains[x].clearance, chains[y].clearance); + for (size_t i = 0; i + 1 < raws[x].size(); ++i) + for (size_t j = 0; j + 1 < raws[y].size(); ++j) + best = std::min(best, seg_dist(raws[x][i], raws[x][i + 1], raws[y][j], raws[y][j + 1])); + chains[x].clearance = std::min(chains[x].clearance, best); + chains[y].clearance = std::min(chains[y].clearance, best); + } + } + for (size_t c = 0; c < chains.size(); ++c) { + Chain &ch = chains[c]; + const std::vector &full = raws[c]; + const float tol = std::min(h, 0.25f * ch.clearance); + const float gap = std::min(seam_gap_mm, 0.5f * ch.clearance); + std::vector simple; + simplify_polyline(full, tol, simple); + // A vertex within the seam gap of the previous one, or of the far end, leaves its copies no + // room to clear the blend of the step, and a corner that close to an edge is within the + // seam's own width anyway: it is merged into its neighbour. + { + std::vector spaced; + spaced.push_back(simple.front()); + for (size_t m = 1; m + 1 < simple.size(); ++m) + if ((simple[m] - spaced.back()).norm() >= gap && (simple[m] - simple.back()).norm() >= gap) + spaced.push_back(simple[m]); + spaced.push_back(simple.back()); + simple = std::move(spaced); + } + const int m_loop = int(L.ids.size()); + const bool adjacent = (L.xpos[size_t(ch.j)] - L.xpos[size_t(ch.i)] + m_loop) % m_loop == 1 || + (L.xpos[size_t(ch.i)] - L.xpos[size_t(ch.j)] + m_loop) % m_loop == 1; + if (simple.size() == 2 && full.size() > 2 && adjacent) { + // Never down to a bare chord between two crossings next to each other on one edge: the + // shallow pocket between them would be joined along the edge itself, and the regions on + // either side would overlap. The raw point farthest from the chord stays. + const Vec2f d = full.back() - full.front(); + const float dl = std::max(d.norm(), 1e-9f); + size_t bi = full.size() / 2; + float bd = 0.1f * tol; // below this the contour is straight: take its middle point + for (size_t m = 1; m + 1 < full.size(); ++m) { + const Vec2f r = full[m] - full.front(); + const float dist = std::abs(d.x() * r.y() - d.y() * r.x()) / dl; + if (dist > bd) { bd = dist; bi = m; } + } + simple.insert(simple.begin() + 1, full[bi]); + } + // Hairpins - the contour doubling back on itself with less than the seam gap between the + // two sides, the tip of a grain line thinner than the gap - would give a region polygon + // whose two sides cross. The spike vertex goes, and the feature is flattened there, as a + // thin feature on an edge is. + for (bool again = true; again;) { + again = false; + for (size_t m = 1; m + 1 < simple.size(); ++m) { + const Vec2f d1 = simple[m] - simple[m - 1], d2 = simple[m + 1] - simple[m]; + const float l1 = d1.norm(), l2 = d2.norm(); + if (l1 < 1e-9f || l2 < 1e-9f) { simple.erase(simple.begin() + long(m)); again = true; break; } + if (d1.dot(d2) / (l1 * l2) > -0.5f) + continue; // turns less than 120 degrees: a corner, not a hairpin + // Distance between the two sides: the shorter side's far end to the other side. + const Vec2f &tip = simple[m]; + const auto dist_to = [&](const Vec2f &q, const Vec2f &a, const Vec2f &b) { + const Vec2f ab = b - a; + const float t = std::clamp((q - a).dot(ab) / std::max(ab.squaredNorm(), 1e-12f), 0.f, 1.f); + return (q - (a + ab * t)).norm(); + }; + const float between = (l1 < l2) ? dist_to(simple[m - 1], tip, simple[m + 1]) : dist_to(simple[m + 1], simple[m - 1], tip); + if (between < seam_gap_mm) { + simple.erase(simple.begin() + long(m)); + again = true; + break; + } + } + } + for (size_t m = 1; m + 1 < simple.size(); ++m) + ch.pts.push_back(to3(simple[m])); + if (ch.pts.empty() && adjacent) { + // Two crossings next to each other on one edge and the contour, once simplified, going + // straight from one to the other: an empty pocket. There is nothing to wall off - a wall + // along the edge would be a zero-area strip, and the neighbour would lay its own on top + // - so neither crossing gets a seam. Recorded here and applied once the tracing is done. + L.no_seam.push_back(ch.i); + L.no_seam.push_back(ch.j); + } + } + for (int k = 0; k < n; ++k) + if (matched[size_t(k)] < 0) + return; + L.chains = std::move(chains); + L.partner = std::move(matched); + L.chain_of.assign(static_cast(n), -1); + for (size_t c = 0; c < L.chains.size(); ++c) { + L.chain_of[size_t(L.chains[c].i)] = int(c); + L.chain_of[size_t(L.chains[c].j)] = int(c); + } + L.ok = true; + }; + tbb::parallel_for(tbb::blocked_range(0, nt), [&](const tbb::blocked_range &r) { + for (size_t t = r.begin(); t < r.end(); ++t) + trace(t); + }); + size_t cut_count = 0; + for (size_t t = 0; t < nt; ++t) { + cut_count += loops[t].ok ? 1 : 0; + if (loops[t].ok) + for (int k : loops[t].no_seam) + loops[t].xs[size_t(k)]->no_seam = true; + } + + // 4. Which crossings a contour reaches, and how obliquely: the copies of a doubled crossing are moved + // apart along the edge, so the shallower the contour meets the edge the further apart they need to + // be to sit the same distance clear of it. + for (size_t t = 0; t < nt; ++t) { + Loop &L = loops[t]; + if (!L.ok) + continue; + for (auto &ch : L.chains) { + Crossing &p = *L.xs[size_t(ch.i)], &q = *L.xs[size_t(ch.j)]; + p.used = q.used = true; + const Vec3f p_next = ch.pts.empty() ? q.p : ch.pts.front(); + const Vec3f q_prev = ch.pts.empty() ? p.p : ch.pts.back(); + for (auto [c, toward] : { std::make_pair(&p, p_next), std::make_pair(&q, q_prev) }) { + Vec3f d = toward - c->p; + const float dl = d.norm(); + if (dl < 1e-12f) continue; + const Vec3f ed = (pos[size_t(c->b)] - pos[size_t(c->a)]).normalized(); + c->sin_min = std::min(c->sin_min, ed.cross(d / dl).norm()); + // Only the part of a move along the edge that runs along the contour brings the copy + // toward the next vertex's copies. With no interior vertex the chord's two ends share + // it; otherwise the interior copies, placed first, say how much room there is. + if (ch.pts.empty()) + c->dt_max = std::min(c->dt_max, 0.45f * dl / std::max(std::abs(ed.dot(d / dl)), 1e-3f)); + } + } + } + + // 5. Output vertices: the originals, then one or two per crossing and per interior contour vertex. + // A sharp crossing that a contour reaches is doubled, into two vertices on the edge either side of + // it, each on its own side of the contour; a triangle on that edge that is not cut itself simply + // carries both on its perimeter, one after the other, so the cut neighbour's wall still meets a + // closed surface. Keeping the copies on the edge is what keeps every triangle's split planar and + // inside the triangle. A crossing that is not sharp, or that no contour reaches, stays one vertex + // and the surface ramps there. + indexed_triangle_set out; + out.vertices = pos; + const float half_gap = 0.5f * seam_gap_mm; + // Interior contour vertices: doubled when the contour's ends are, offset across the contour along + // the bisector of the two segments meeting there, toward the high side; mitre limited, and held + // inside the triangle - a copy pushed across an edge would fold the region over the neighbour. + for (size_t t = 0; t < nt; ++t) { + Loop &L = loops[t]; + if (!L.ok) + continue; + const auto &tri = mesh.indices[t]; + const Vec3f &A = pos[size_t(tri[0])], &B = pos[size_t(tri[1])], &C = pos[size_t(tri[2])]; + const Vec3f U = (B - A).normalized(), V = L.N.cross(U); + const Vec2f a2(0.f, 0.f), b2((B - A).dot(U), (B - A).dot(V)), c2((C - A).dot(U), (C - A).dot(V)); + const float det = (b2.x() - a2.x()) * (c2.y() - a2.y()) - (c2.x() - a2.x()) * (b2.y() - a2.y()); + const auto bary2 = [&](const Vec3f &p) { + const Vec2f q((p - A).dot(U), (p - A).dot(V)); + const float wb = ((q.x() - a2.x()) * (c2.y() - a2.y()) - (c2.x() - a2.x()) * (q.y() - a2.y())) / det; + const float wc = ((b2.x() - a2.x()) * (q.y() - a2.y()) - (q.x() - a2.x()) * (b2.y() - a2.y())) / det; + return Vec3f(1.f - wb - wc, wb, wc); + }; + // The largest fraction of the offset that keeps the copy inside, with a little to spare. + const auto inside_fraction = [&](const Vec3f &p, const Vec3f &off) { + const Vec3f w0 = bary2(p), w1 = bary2(p + off); + float lambda = 1.f; + for (int i = 0; i < 3; ++i) + if (w1[i] < 0.f && w0[i] > w1[i]) + lambda = std::min(lambda, w0[i] / (w0[i] - w1[i])); + return 0.9f * std::max(lambda, 0.f); + }; + const bool s0 = side_of(vh[size_t(mesh.indices[t][0])]); + for (auto &ch : L.chains) { + const Crossing &p = *L.xs[size_t(ch.i)], &q = *L.xs[size_t(ch.j)]; + const bool doubled = p.sharp && p.used && !p.no_seam && q.sharp && q.used && !q.no_seam; + // The arc right after crossing ch.i lies on one side; which way that is from the contour + // says where "high" is. The arc's side: the loop's first corner's, flipped once per + // crossing passed, so after crossing index k it is !s0 for even k and s0 for odd. + const bool arc_high = (ch.i % 2 == 0) ? !s0 : s0; + const int after_pos = L.xpos[size_t(ch.i)] + 1; + const int entry = L.ids[size_t(after_pos) % L.ids.size()]; + const Vec3f ref = entry >= 0 ? pos[size_t(entry)] : L.xs[size_t(-entry - 1)]->p; + const size_t m = ch.pts.size(); + ch.lo.resize(m); ch.hi.resize(m); + // The left of the walk from i to j is one side of the contour all along it; whether that + // side is high is settled once, at the first segment, where the arc is a known reference. + // The direction the contour leaves the crossing in, taken over a usable length: the first + // interior vertex may sit right next to it. + Vec3f d_first = q.p - p.p; + for (size_t k = 0; k < m; ++k) + if ((ch.pts[k] - p.p).norm() > 0.25f * seam_gap_mm) { d_first = ch.pts[k] - p.p; break; } + d_first.normalize(); + const bool left_high = (L.N.cross(d_first).dot(ref - p.p) > 0.f) == arc_high; + for (size_t k = 0; k < m; ++k) { + if (!doubled) { + ch.lo[k] = ch.hi[k] = int(out.vertices.size()); + out.vertices.push_back(ch.pts[k]); + continue; + } + const Vec3f prev = (k == 0) ? p.p : ch.pts[k - 1]; + const Vec3f next = (k + 1 == m) ? q.p : ch.pts[k + 1]; + Vec3f d1 = (ch.pts[k] - prev).normalized(), d2 = (next - ch.pts[k]).normalized(); + Vec3f n1 = L.N.cross(d1), n2 = L.N.cross(d2); + Vec3f bis = n1 + n2; + float bl = bis.norm(); + Vec3f off; + if (bl < 1e-6f) off = n1; + else { + bis /= bl; + // Mitre: the offset polyline stays `half_gap` from both segments, up to twice that. + const float cos_half = std::max(bis.dot(n1), 0.5f); + off = bis / cos_half; + } + if (!left_high) off = -off; + // No further along either segment than half its length, so copies never cross their + // neighbours'; across the segments the offset is free. + const float ol = off.norm(); + const float reach = std::min({ half_gap, 0.45f * ch.clearance, + 0.45f * (ch.pts[k] - prev).norm() / std::max(std::abs(off.dot(d1)) / ol, 1e-3f), + 0.45f * (next - ch.pts[k]).norm() / std::max(std::abs(off.dot(d2)) / ol, 1e-3f) }); + const Vec3f off_lo = -off * (reach * inside_fraction(ch.pts[k], -off * reach)); + const Vec3f off_hi = off * (reach * inside_fraction(ch.pts[k], off * reach)); + // As for the crossings: each copy at the first of a few positions outward that samples + // pure, or the vertex stays single. + const auto place = [&](const Vec3f &o, bool high) -> int { + for (int step = 1; step <= 4; ++step) { + const Vec3f q = ch.pts[k] + o * (0.5f * float(step)); + if (step > 2 && inside_fraction(ch.pts[k], q - ch.pts[k]) < 0.999f) break; + const float h = sampler(q, L.N) - iso; + if ((h > 0.f) == high && std::abs(h) >= 0.3f * range) { + out.vertices.push_back(q); + return int(out.vertices.size()) - 1; + } + } + return -1; + }; + const int il = place(off_lo, false), ih = place(off_hi, true); + if (il < 0 || ih < 0) { + out.vertices.resize(out.vertices.size() - (il >= 0 ? 1 : 0) - (ih >= 0 ? 1 : 0)); + ch.lo[k] = ch.hi[k] = int(out.vertices.size()); + out.vertices.push_back(ch.pts[k]); + continue; + } + ch.lo[k] = il; ch.hi[k] = ih; + } + // Room for the end crossings' copies along the edge: the nearer of the first interior + // vertex's copies, measured along the contour, less a margin, over the edge's share of + // that direction. + if (m > 0) + for (auto [c, k, other] : { std::make_tuple(&p, size_t(0), q.p), std::make_tuple(&q, m - 1, p.p) }) { + (void) other; + Crossing &cr = const_cast(*c); + const Vec3f d = ch.pts[k] - cr.p; + const float dl = d.norm(); + if (dl < 1e-9f) continue; + const Vec3f dn = d / dl; + const float u_lo = (out.vertices[size_t(ch.lo[k])] - cr.p).dot(dn); + const float u_hi = (out.vertices[size_t(ch.hi[k])] - cr.p).dot(dn); + const Vec3f ed = (pos[size_t(cr.b)] - pos[size_t(cr.a)]).normalized(); + cr.dt_max = std::min(cr.dt_max, 0.9f * std::max(std::min(u_lo, u_hi), 0.f) / std::max(std::abs(ed.dot(dn)), 1e-3f)); + } + } + } + + for (auto &kv : edges) { + Edge &edge = kv.second; + for (size_t i = 0; i < edge.xs.size(); ++i) { + Crossing &c = edge.xs[i]; + if (!(c.sharp && c.used) || c.no_seam) { + c.single = int(out.vertices.size()); + out.vertices.push_back(c.p); + continue; + } + const float len = (pos[size_t(c.b)] - pos[size_t(c.a)]).norm(); + // Room along the edge: a third of the way to the previous and the next crossing or vertex, + // and less than half the shortest contour segment leaving the crossing, so the copies of + // the contour's next vertex cannot cross these. + const float t_prev = (i == 0) ? 0.f : edge.xs[i - 1].t; + const float t_next = (i + 1 == edge.xs.size()) ? 1.f : edge.xs[i + 1].t; + float dt = half_gap / std::max(c.sin_min, 0.15f) / std::max(len, 1e-6f); + dt = std::min({ dt, (c.t - t_prev) / 3.f, (t_next - c.t) / 3.f, c.dt_max / std::max(len, 1e-6f) }); + if (!(dt > 1e-6f)) { + c.single = int(out.vertices.size()); + out.vertices.push_back(c.p); + continue; + } + // The side of the edge before this crossing: the lower vertex's, flipped once per crossing + // passed on the way. + const bool before_high = side_of(vh[size_t(c.a)]) != (i % 2 == 1); + // Each copy has to sample a pure value of its own side - inside the step's blend it would be + // displaced to a height between the sides, and one such vertex tilts every triangle at it. + // The copy is placed at the first of a few positions along the edge, from dt outward, that + // samples pure; when none does the crossing stays one vertex and the surface ramps across + // that edge only. + // The search never goes past what the neighbours allow: less than half way to the next + // crossing along the edge (so two crossings' copies never meet), most of the way to the + // edge's vertex, and not past dt_max toward the contour's next vertex. + const float lo_share = (i == 0) ? 0.9f : 0.49f, hi_share = (i + 1 == edge.xs.size()) ? 0.9f : 0.49f; + const float t_lo_lim = c.t - std::min(lo_share * (c.t - t_prev), c.dt_max / std::max(len, 1e-6f)); + const float t_hi_lim = c.t + std::min(hi_share * (t_next - c.t), c.dt_max / std::max(len, 1e-6f)); + const auto place = [&](float sgn, bool high) -> int { + for (int step = 1; step <= 6; ++step) { + const float tt = c.t + sgn * dt * (0.5f * float(step)); + if (tt < t_lo_lim || tt > t_hi_lim) break; + const auto [q, qn] = sample_between(c.a, c.b, tt); + const float h = sampler(q, qn) - iso; + if ((h > 0.f) == high && std::abs(h) >= 0.3f * range) { + out.vertices.push_back(q); + return int(out.vertices.size()) - 1; + } + } + return -1; + }; + const int na = place(-1.f, before_high), nb = place(+1.f, !before_high); + if (na < 0 || nb < 0) { + out.vertices.resize(out.vertices.size() - (na >= 0 ? 1 : 0) - (nb >= 0 ? 1 : 0)); + c.single = int(out.vertices.size()); + out.vertices.push_back(c.p); + continue; + } + c.near_a = na; c.near_b = nb; + c.lo = before_high ? c.near_b : c.near_a; + c.hi = before_high ? c.near_a : c.near_b; + } + } + // 6. Output triangles: each region of a cut triangle ear-clipped on its own side's copies, a wall + // strip per contour wound with the surface, everything else passed through or plainly split. + out.indices.reserve(nt * 2); + std::vector source; + source.reserve(nt * 2); + const auto emit = [&](int a, int b, int c, int src) { out.indices.emplace_back(a, b, c); source.push_back(src); }; + const auto copy_for = [&](const Crossing &c, bool high) { return c.single >= 0 ? c.single : (high ? c.hi : c.lo); }; + // Triangulates a triangle that carries extra vertices on its edges without a zero-area sliver: the + // first extra vertex found is joined to the opposite corner, which splits the triangle in two that + // carry fewer extras each, and so on. A fan from a corner cannot do this - the extras on that + // corner's own edges are collinear with it. Each side is the corner followed by the extras on the + // way to the next corner. + const std::function, 3>, int)> split_tri = + [&](std::array, 3> side, int src) { + for (int e = 0; e < 3; ++e) + if (side[size_t(e)].size() > 1) { + const auto &S = side[size_t(e)]; + const int a = S[0], x = S[1], c = side[size_t((e + 2) % 3)][0]; + std::vector rest(S.begin() + 1, S.end()); // x and the extras after it + split_tri({ std::vector{ a }, std::vector{ x }, side[size_t((e + 2) % 3)] }, src); + split_tri({ std::move(rest), side[size_t((e + 1) % 3)], std::vector{ c } }, src); + return; + } + emit(side[0][0], side[1][0], side[2][0], src); + }; + // A wall strip between the low and high copies of a contour, from crossing p to crossing q. + const auto emit_wall = [&](const Crossing &p, const Chain &ch, const Crossing &q, const Vec3f &N, int src) { + std::vector lo, hi; + lo.push_back(copy_for(p, false)); hi.push_back(copy_for(p, true)); + for (size_t k = 0; k < ch.pts.size(); ++k) { lo.push_back(ch.lo[k]); hi.push_back(ch.hi[k]); } + lo.push_back(copy_for(q, false)); hi.push_back(copy_for(q, true)); + auto tri_up = [&](int i, int j, int k) { + if (i == j || j == k || k == i) return; + const Vec3f &A = out.vertices[size_t(i)], &B = out.vertices[size_t(j)], &C = out.vertices[size_t(k)]; + if ((B - A).cross(C - A).dot(N) >= 0.f) emit(i, j, k, src); else emit(i, k, j, src); + }; + for (size_t k = 0; k + 1 < lo.size(); ++k) { + tri_up(lo[k], lo[k + 1], hi[k + 1]); + tri_up(lo[k], hi[k + 1], hi[k]); + } + }; + + for (size_t t = 0; t < nt; ++t) { + const auto &tri = mesh.indices[t]; + const Loop &L = loops[t]; + const int n = int(L.xs.size()); + if (n == 0) { emit(tri[0], tri[1], tri[2], int(t)); continue; } + const size_t m = L.ids.size(); + const bool s0 = side_of(vh[size_t(tri[0])]); + + if (L.ok) { + // Every crossing starts one region: the run of the loop after it, on the side the field has + // there, following any other contour it meets back to the loop, until it returns to the + // start. Each region is tracked per (crossing, side) so the one between two contours is + // emitted once, not once per contour. The region's vertices are collected in the triangle's + // plane for ear clipping, since a traced contour can make it concave. + const Vec3f &A = pos[size_t(tri[0])], &B = pos[size_t(tri[1])]; + const Vec3f U = (B - A).normalized(), V = L.N.cross(U); + std::vector pts2; + std::vector ids2; + const auto push = [&](int id) { + const Vec3f &p = out.vertices[size_t(id)]; + pts2.emplace_back((p - A).dot(U), (p - A).dot(V)); + ids2.push_back(id); + }; + // Interior vertices of chain c, walking from crossing `from`, on side S. + const auto push_chain = [&](int c, int from, bool S) { + const Chain &ch = L.chains[size_t(c)]; + const auto &cp = S ? ch.hi : ch.lo; + if (from == ch.i) for (size_t k = 0; k < cp.size(); ++k) push(cp[k]); + else for (size_t k = cp.size(); k-- > 0;) push(cp[k]); + }; + const auto side_after = [&](int k) { return (k % 2 == 0) ? !s0 : s0; }; + std::vector> consumed(static_cast(n), { false, false }); + for (int k = 0; k < n; ++k) { + const bool S = side_after(k); + if (consumed[size_t(k)][S]) + continue; + pts2.clear(); ids2.clear(); + push(copy_for(*L.xs[size_t(k)], S)); + consumed[size_t(k)][S] = true; + size_t i = size_t(L.xpos[size_t(k)] + 1) % m; + for (size_t guard = 0; guard < 2 * m; ++guard) { + const int id = L.ids[i]; + if (id >= 0) { push(id); i = (i + 1) % m; continue; } + const int c = -id - 1; + consumed[size_t(c)][S] = true; + push(copy_for(*L.xs[size_t(c)], S)); + if (c == L.partner[size_t(k)]) + break; + // Follow the contour from c to its partner, then continue the loop after it. + const int pc = L.partner[size_t(c)]; + push_chain(L.chain_of[size_t(c)], c, S); + consumed[size_t(pc)][S] = true; + push(copy_for(*L.xs[size_t(pc)], S)); + if (pc == L.partner[size_t(k)]) + break; + i = size_t(L.xpos[size_t(pc)] + 1) % m; + } + // Close the polygon along the region's own contour back to k, in reverse. + push_chain(L.chain_of[size_t(k)], L.partner[size_t(k)], S); + std::vector poly(ids2.size()); + std::iota(poly.begin(), poly.end(), 0); + ear_clip(pts2, poly, [&](int a, int b, int c) { emit(ids2[size_t(a)], ids2[size_t(b)], ids2[size_t(c)], int(t)); }); + } + for (const auto &ch : L.chains) + emit_wall(*L.xs[size_t(ch.i)], ch, *L.xs[size_t(ch.j)], L.N, int(t)); + continue; + } + // Not cut: split at the crossings so the neighbours' cuts meet no T-junction. A doubled crossing + // contributes both copies, the one nearer the vertex the walk comes from first. + std::array, 3> side; + int e = -1; + for (size_t i = 0; i < m; ++i) { + if (L.ids[i] >= 0) { ++e; side[size_t(e)].push_back(L.ids[i]); continue; } + const Crossing &c = *L.xs[size_t(-L.ids[i] - 1)]; + if (c.single >= 0) { side[size_t(e)].push_back(c.single); continue; } + const bool from_a = tri[e] == c.a; + side[size_t(e)].push_back(from_a ? c.near_a : c.near_b); + side[size_t(e)].push_back(from_a ? c.near_b : c.near_a); + } + split_tri(std::move(side), int(t)); + } + // Safety net: a texture whose features are of the seam's own scale can fold copies over each + // other in spite of everything above. When more than a trace of the output faces the wrong way, + // the cut is not trusted and the mesh goes out as it came in. + { + size_t inverted = 0; + for (size_t i = 0; i < out.indices.size(); ++i) { + const auto &f = out.indices[i]; + const auto &g = mesh.indices[size_t(source[i])]; + const Vec3f n = (out.vertices[size_t(f[1])] - out.vertices[size_t(f[0])]).cross(out.vertices[size_t(f[2])] - out.vertices[size_t(f[0])]); + const Vec3f ns = (mesh.vertices[size_t(g[1])] - mesh.vertices[size_t(g[0])]).cross(mesh.vertices[size_t(g[2])] - mesh.vertices[size_t(g[0])]); + inverted += n.dot(ns) < 0.f && n.norm() > 1e-7f; + } + if (double(inverted) > 0.002 * double(out.indices.size())) + return passthrough(); + } + if (out_source) *out_source = std::move(source); + if (out_cut_count) *out_cut_count = cut_count; + return out; +} + +} // namespace Slic3r diff --git a/src/libslic3r/TextureDisplacement.hpp b/src/libslic3r/TextureDisplacement.hpp new file mode 100644 index 0000000000..e97178abca --- /dev/null +++ b/src/libslic3r/TextureDisplacement.hpp @@ -0,0 +1,1139 @@ +#ifndef slic3r_TextureDisplacement_hpp_ +#define slic3r_TextureDisplacement_hpp_ + +#include +#include +#include +#include +#include + +#include +#include // view_project_matrix is a std::array +#include +#include + +#include + +#include "Point.hpp" +#include "TriangleMesh.hpp" +#include "TriangleSelector.hpp" + +namespace Slic3r { + +// Optional step-by-step capture of a bake; see TextureBake/TextureBakeDebug.hpp. Forward declared and +// taken by pointer so this header, which most of the texture feature includes, does not grow a +// dependency for something only the debug view and the benchmarks use. +class BakeStageRecorder; + +class ModelVolume; + +// Bits of subdivide_mesh_adaptive()'s per-triangle `refine_region` mask. See that function. +static constexpr uint8_t REFINE_PAINTED = 1; +static constexpr uint8_t REFINE_BORDER = 2; + +// Progress/cancellation hook for the (potentially multi-second) bake. Called with a 0..100 +// percentage; return false to abort. See build_texture_displacement(). +using DisplacementProgressFn = std::function; + +// Maximum number of simultaneous texture-displacement layers a single ModelVolume can hold. +// Each layer owns its own paint mask (ModelVolume::texture_displacement_facet(slot)), so this +// is also the number of independent EnforcerBlockerType selectors kept per volume. +static constexpr size_t TEXTURE_DISPLACEMENT_MAX_LAYERS = 8; + +// How a layer's height texture is sampled outside its [0, 1) tile when tiling is enabled. Ignored +// (always clamp) when TextureDisplacementLayer::tile_enabled is false. +enum class TextureTileMethod : int +{ + Repeat = 0, // wrap around, tile i and tile i+1 are identical (default) + MirroredRepeat = 1, // wrap around, every other tile is mirrored (no visible seam at tile edges) +}; + +// How a layer's texture is mapped onto the mesh. +enum class TextureProjectionMethod : int +{ + // Standard *blended* tri-planar projection: the texture is sampled once per world axis (the + // XY, XZ and YZ planes) and the three samples are blended per vertex, weighted by that + // vertex's own normal raised to TRIPLANAR_BLEND_SHARPNESS. + // + // Earlier versions instead *hard-picked* the single axis most aligned with the normal. That + // has a real, visible failure mode at any edge where the dominant axis flips: on a +X face the + // planar coordinate is (y, z), on a -Y face it is (x, z), so at the shared edge u jumps from + // y_edge to x_edge. On a box centred near the origin those two agree at the (+,+) and (-,-) + // corners (making them look fine) but differ by the full corner width at the (+,-) and (-,+) + // corners, which is exactly the "two bad corners, two good ones" seam that was reported. + // Blending across the transition removes that hard discontinuity by construction. + Triplanar = 0, + // Wrapped around an axis running through the patch's centroid. The axis itself is picked + // automatically as the world axis *least* aligned with the patch's average normal (since a + // cylinder's own axis is perpendicular to its outward radial normal) - a reasonable default + // for roughly cylindrical selections, not a precise fit for arbitrary geometry. + Cylindrical = 1, + // Wrapped around the patch's centroid using longitude/latitude - reasonable for roughly + // spherical/rounded selections, again an approximation rather than an exact geodesic map. + Spherical = 2, + // Real UV unwrap of the painted patch - a proper low-distortion flattening rather than a + // planar/cylindrical/spherical approximation. The patch is first cut into charts along its + // sharp edges and each chart is flattened on its own (see compute_patch_unwrap()), so a patch + // that is not a single developable surface still unwraps sensibly. Falls back to Triplanar for + // any chart that cannot be flattened at all. + LSCM = 3, + // Flat projection along a fixed direction captured from the 3D camera ("project from view"): the + // texture is laid onto the painted area as seen from that angle, like a decal projector. Single + // planar map (no per-face axis switch), so it can smear on faces turned away from the projector - + // that is inherent to view projection and is the user's call, not a bug. The projector's two + // in-plane axes live in TextureDisplacementLayer::view_project_right/up. + ViewProjected = 4, +}; + +// Dihedral angle (degrees) above which an edge between two painted triangles becomes a chart seam +// - i.e. the unwrap is cut there rather than being forced to flatten across it. +// +// The whole point of this being a threshold rather than "flatten everything as one piece": three +// faces meeting at a cube corner are not developable, so a single-chart solve has to distort them +// badly to lie flat (they splay out into a fan, which is what "it merges all the edges into a +// triangle" describes). Cutting at the 90-degree edges instead lets each face flatten exactly. +// Meanwhile a smoothly curved surface - a subdivided sphere, say - has only small angles between +// neighbouring triangles, stays a single chart, and unwraps as one piece the way it should. +static constexpr float LSCM_DEFAULT_SEAM_ANGLE_DEG = 30.f; + +// Exponent the tri-planar blend weights are raised to (see TextureProjectionMethod::Triplanar). +// Higher means a tighter, more "hard-edged" transition between the three axis projections; lower +// means a wider cross-fade. 4 is the usual default: tight enough that a flat face is sampled +// almost purely along its own axis, wide enough that a 90-degree edge has no visible hard seam. +static constexpr float TRIPLANAR_BLEND_SHARPNESS = 4.f; + +// How a layer's displacement combines with the displacement accumulated by the layers below it +// (i.e. those in lower slots), evaluated per vertex. Analogous to an image editor's layer blend +// modes, except the quantity being blended is a signed displacement distance in mm rather than a +// pixel value. +// +// Add/Subtract are in mm and need no further explanation. Multiply/Divide are *scaling* operations +// and therefore need a unit convention: they treat the layer's own value as a unitless factor +// relative to 1 mm. That makes `depth_mm` act as a gain - a layer with depth 1 mm and a white +// (1.0) texel multiplies the accumulated relief by exactly 1, i.e. leaves it unchanged - which is +// the behaviour that makes a Multiply layer usable as a mask over the layers beneath it. +enum class TextureBlendMode : int +{ + Add = 0, // acc + value (default; several layers pile their relief up together) + Subtract = 1, // acc - value (carve this layer's relief out of the layers below) + Multiply = 2, // acc * (value / 1mm) (mask/modulate the layers below by this layer) + Divide = 3, // acc / (value / 1mm) (inverse mask; guarded against a zero/near-zero divisor) +}; + +// Combines one layer's signed displacement `value` (mm) into `accumulated` (mm) per `mode`. +// Shared by the bake/preview path and exposed for tests. +float blend_displacement(float accumulated, float value, TextureBlendMode mode); + +// Where one unwrap island (chart) sits in UV space, on top of wherever compute_patch_unwrap() first +// packed it. This is what the UV editor's drag/rotate gestures write to, so a user can lay the +// islands out by hand - move them, rotate them, overlap them - rather than being stuck with the +// automatic packing. +// +// Indexed by chart id, which compute_patch_unwrap() assigns in first-encountered-triangle order. That +// is stable for a given patch and seam angle, but *not* across a change to either: repainting the +// patch, or moving the seam-angle slider, can renumber the charts and so leave a hand-placed island +// applied to a different one. Accepted deliberately - the alternative is a persistent chart identity +// that survives arbitrary re-segmentation, which is a much larger problem than this feature warrants. +struct TextureIsland +{ + Vec2f offset = Vec2f::Zero(); // in the unwrap's own mm space + float rotation_deg = 0.f; // about the island's own centroid + // About the island's own centroid too. 1 = the size compute_patch_unwrap() gave it, which is + // already its true surface area in mm - so scaling an island away from 1 deliberately makes its + // texel density differ from its neighbours'. See average_island_scales(). + float scale = 1.f; + + template void serialize(Archive &ar) { ar(offset, rotation_deg, scale); } +}; + +// Sets every island's scale to the mean of the current ones (Blender's "Average Islands Scale"). +// Only meaningful after islands have been scaled by hand: compute_patch_unwrap() already sizes every +// chart to its true mm area, so a freshly unwrapped patch has uniform texel density to begin with. +void average_island_scales(std::vector &islands); + +// One texture asset plus its projection/displacement parameters. Several layers may be painted +// onto overlapping areas of the same volume: their displacements are combined per vertex, in slot +// order, each layer folding into the total via its own TextureBlendMode (see +// build_texture_displacement()). This is what "layered/blended" texture displacement means here. +struct TextureDisplacementLayer +{ + // Index into ModelVolume::texture_displacement_facets, assigned once when the layer is + // created. Not reused for the lifetime of the ModelVolume, so a deleted layer's slot simply + // becomes unused rather than being handed to a different layer. + int slot = -1; + + std::string name; + // Path on the local filesystem the image was loaded from (informational; may be stale or + // empty, e.g. after loading a .3mf on a different machine). + std::string path; + // Path inside the .3mf archive once saved (empty until the project is saved once). + std::string path_in_3mf; + // Raw encoded image bytes. Only 8-bit grayscale PNG is understood by decode_height_texture() + // (libslic3r has no GUI image toolkit available); the GUI converts any imported image to that + // format before storing it here, so the baking code never needs to depend on wxWidgets. + std::shared_ptr> image_data; + + float depth_mm = 0.4f; // maximum displacement along the surface normal, in mm + float tiling_scale = 12.5f; // size of one texture tile, in mm + float rotation_deg = 0.f; + Vec2f offset = Vec2f::Zero(); + bool invert = false; + + // The height value that means "don't move this vertex". The sampled height (0..1) has this + // subtracted before being scaled by depth_mm, so with the default of 0 the surface only ever + // moves *outwards* (the classic height-map convention), while 0.5 makes mid-grey neutral and + // lets darker texels cut *into* the surface - an engraved-and-embossed result from one map. + // + // Cutting inward is not free: vertices move along their own normals, which converge inside a + // concave corner and inside a thin wall, so a large depth_mm against a small feature really can + // fold the surface through itself. There is no cheap way to detect that here (it needs a full + // self-intersection test on the displaced mesh), so the GUI warns rather than promising safety. + float midlevel = 0.f; + + // Optional blur applied to the decoded height map before it is sampled, in [0, 1]: 0 is the raw + // texture, 1 the strongest blur. Softens the relief (rounds hard edges, removes speckle) without + // needing a pre-blurred source image. Applied in decode_height_texture(), so it feeds the true + // preview, the UV editor backdrop and the bake identically. + float smoothing = 0.f; + + // Optional feathering of the displacement toward the edge of the painted patch. When enabled, the + // displacement is scaled down as a vertex approaches the patch boundary, so the relief blends + // smoothly into the surrounding surface instead of ending abruptly. `edge_smoothing_amount` in + // (0, 1] sets how far the fade reaches into the patch: small values only soften a thin band at the + // very edge, 1 fades the whole patch to nothing (the painted face comes out flat). Off by default. + bool edge_smoothing = false; + float edge_smoothing_amount = 0.5f; + + // Only used by TextureProjectionMethod::LSCM: when set, a fresh unwrap is laid out as a connected + // net (adjacent charts unfolded edge-to-edge along a spanning tree, see compute_connected_net()) + // rather than as separately packed islands. On by default. Hand-moving an island overrides its + // placement until the next re-unwrap. + bool auto_connect_islands = true; + + // When false, the texture is sampled once (clamped to its edge pixels outside [0, 1)) instead + // of being repeated - useful for a single decal-like placement rather than a repeating tile. + bool tile_enabled = true; + TextureTileMethod tile_method = TextureTileMethod::Repeat; + + TextureProjectionMethod projection_method = TextureProjectionMethod::Triplanar; + // Only used by TextureProjectionMethod::LSCM. See LSCM_DEFAULT_SEAM_ANGLE_DEG. + float lscm_seam_angle_deg = LSCM_DEFAULT_SEAM_ANGLE_DEG; + // Only used by TextureProjectionMethod::LSCM: gap left between islands by the automatic packing, + // in the unwrap's mm space. Negative means "auto" (a small fraction of the packed size), which is + // what a patch that has never had the slider touched gets. + float island_padding_mm = -1.f; + // Only used by TextureProjectionMethod::LSCM: edges the unwrap is forced to cut along, on top of + // whatever the seam angle already cuts. Each pair is an undirected edge in *mesh vertex index* + // space (first < second). This is what "mark seam" (manual) and "cut island" (auto) both write to. + // Mesh-index space, so like the paint masks these are dropped on any topology change. + std::vector> lscm_seam_edges; + + // Only used by TextureProjectionMethod::ViewProjected: the projector's in-plane axes, in the + // volume's *local* space, captured from the camera when the user hits "Project from view". A point + // projects to Vec2f(dot(pos, right), dot(pos, up)) before the usual tiling/rotation/offset. + Vec3f view_project_right = Vec3f::UnitX(); + Vec3f view_project_up = Vec3f::UnitY(); + + // Also ViewProjected, and takes precedence over the two axes above when set: an exact *projective* + // map from a local-space position straight to a texture uv, written by the projection-frame overlay + // (the semi-transparent window dragged over the 3D view - its border becomes the uv unit square). + // + // Row-major 3x4, applied to the homogeneous point p~ = (x, y, z, 1): + // uv = ( row0.p~ / row2.p~ , row1.p~ / row2.p~ ) + // The perspective divide is the whole point. view_project_right/up can only express an *affine* + // projection, which matches an orthographic camera exactly but not a perspective one - under + // perspective the near end of a part projects larger than the far end, and no pair of axes + // reproduces that. Folding the camera's full projection*view*model product into one matrix does. + // Because a point behind the projector has row2.p~ <= 0 and no meaningful uv, sampling must check + // the sign rather than divide blindly; see project_uv_projective(). + // + // Note this map already includes placement, so the usual tiling/rotation/offset transform is NOT + // applied on top of it - the window's own position and size are the placement. + bool view_project_projective = false; + std::array view_project_matrix{}; + // Only used by TextureProjectionMethod::LSCM: hand placement of the unwrap's islands, indexed by + // chart id (see TextureIsland). Shorter than the chart count simply means the missing ones are + // still where the automatic packing put them. + std::vector islands; + + // Only used by TextureProjectionMethod::LSCM: persistent "join" groups, indexed by chart id. Charts + // that share a group id move together as one in the UV editor - this is what the explicit "Join" + // command records (over and above placing the child next to its parent). An entry of -1, or an index + // past the end of the vector, means the chart is its own singleton group (moves alone). Empty means + // every chart is a singleton. Same chart-renumbering caveat as `islands`: a re-unwrap can reshuffle + // chart ids, so this is meaningful only against the unwrap it was made on. + std::vector island_groups; + + // Only used by TextureProjectionMethod::LSCM: manual UV edits made in the UV editor's Vertex/Edge + // select modes. Each pair is (key, its overriding raw-unwrap coordinate in mm) - the *raw* unwrap + // position, i.e. before the island transform, so the edited vertex still moves and rotates with its + // island. The key names what is edited (see apply_lscm_uv_overrides()): + // - negative: one unwrapped vertex of the current unwrap, as lscm_uv_override_key(index). This is what + // the editor stores, so a seam vertex dragged in one island leaves its copies in the neighbouring + // islands where they are. Like `islands`, meaningful only against the unwrap it was made on. + // - zero or positive: a mesh vertex, every unwrapped copy of it (how edits were stored before). + std::vector> lscm_uv_overrides; + + // How this layer folds into the displacement accumulated by the layers below it. Ignored for + // the lowest-slot painted layer, which has nothing beneath it to combine with (the GUI shows + // it as the "Base" layer and hides the control). + TextureBlendMode blend_mode = TextureBlendMode::Add; + + // Colour this layer's painted area from the texture's own colours, on top of displacing by its + // height. Only meaningful when the texture actually has colour (DecodedHeightTexture::has_color()): + // the shipped library is grayscale, so this does nothing there. + // + // Colour lands in the volume's mmu_segmentation_facets - the same per-triangle filament assignment + // the MMU paint gizmo writes - so its resolution is the *mesh's*, not the image's, and a triangle + // gets exactly one filament. That is why the adaptive subdivision has a colour criterion of its + // own (see subdivide_mesh_adaptive()): without triangles along a colour boundary there is nothing + // for the boundary to be drawn on. + bool color_enabled = false; + + bool empty() const { return !image_data || image_data->empty(); } + + template void save(Archive &ar) const + { + std::string blob = image_data ? std::string(image_data->begin(), image_data->end()) : std::string(); + ar(slot, name, path, path_in_3mf, blob, depth_mm, tiling_scale, rotation_deg, offset, invert, tile_enabled, + static_cast(tile_method), static_cast(projection_method), lscm_seam_angle_deg, islands, + static_cast(blend_mode), midlevel, island_padding_mm, lscm_seam_edges, view_project_right, + view_project_up, smoothing, edge_smoothing, edge_smoothing_amount, auto_connect_islands, island_groups, + lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled); + } + template void load(Archive &ar) + { + std::string blob; + int tile_method_int = 0; + int projection_method_int = 0; + int blend_mode_int = 0; + ar(slot, name, path, path_in_3mf, blob, depth_mm, tiling_scale, rotation_deg, offset, invert, tile_enabled, + tile_method_int, projection_method_int, lscm_seam_angle_deg, islands, blend_mode_int, midlevel, + island_padding_mm, lscm_seam_edges, view_project_right, view_project_up, smoothing, edge_smoothing, + edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective, + view_project_matrix, color_enabled); + image_data = blob.empty() ? nullptr : std::make_shared>(blob.begin(), blob.end()); + tile_method = static_cast(tile_method_int); + projection_method = static_cast(projection_method_int); + blend_mode = static_cast(blend_mode_int); + } +}; + +// How a *mixed* palette entry - one that names two filaments rather than one - is turned into real +// per-facet paint. An MMU extrudes one filament at a time, so an intermediate colour exists only by +// interleaving two of them finely enough that the eye does the blending. +enum class ColorMixMode : int +{ + // Horizontal bands: which of the two filaments a point takes depends on its height, so + // consecutive print layers alternate. This is how filament-blend prints actually work, and on a + // vertical-ish surface it reads as a genuinely smooth colour. On a near-horizontal surface a whole + // layer is one band, so the blend disappears - that is what XYDither is for. + ZBands = 0, + // An ordered (Bayer) checkerboard across the surface, at any orientation. Independent of layer + // height, but its cell is around the size of one facet, so a fine mix can read as texture rather + // than as a clean blend. + XYDither = 1, + // Per triangle, by its orientation: bands where the surface is upright enough for consecutive + // layers to alternate, the checkerboard where it faces up or down and a layer would be one band. + // The default - a flat-topped part with a mix on top gets no blend at all from bands alone. + Auto = 2, +}; + +// Settings that apply to the whole layer stack rather than to one layer, held per ModelVolume next +// to texture_displacement_layers and consumed by build_texture_displacement(). +struct TextureDisplacementOptions +{ + // Whether the painted patch's *border* vertices - the ones also used by unpainted triangles - + // are displaced along with the rest, or pinned flat. + // + // Pinning them was originally justified as keeping the patch from tearing away from the + // surrounding surface. That reasoning no longer applies: since the bake became + // topology-preserving it only ever *moves* the input's own vertices, so a border vertex is one + // vertex shared by both regions and moving it simply tilts the unpainted triangles that use it - + // nothing can come apart. What pinning actually does is clamp the outermost ring of the relief to + // zero, which on a fully painted face collapses the pattern into a ring of steep ramps right at + // the edge (the "it doesn't extrude at the border" artifact). Displacing it is the default; + // pinning is kept for the case where the relief must not spill past the paint at all. + bool displace_border = true; + + // Optional Laplacian relaxation of the displaced surface, run after all layers have been folded + // in - a post-process, not a texture filter (TextureDisplacementLayer::smoothing blurs the height + // map instead, before it is ever sampled). Rounds off the hard steps a bitmap height map leaves + // behind. Restricted to vertices the displacement actually moved, so the rest of the model keeps + // its exact geometry. `smooth_strength` in [0, 1] is how far each pass moves a vertex toward the + // average of its neighbours. + bool smooth_enabled = false; + float smooth_strength = 0.3f; + int smooth_iterations = 2; + + // Hold the painted patch's outermost ring of vertices out of the smoothing. Those vertices sit + // next to unpainted ones that are pinned by definition, so relaxing them drags the rim of the + // relief back down toward the undisplaced surface - the pattern looks half-melted exactly where it + // meets the edge, however crisp the rest of it is. Excluding them keeps the border extruded at + // full depth and smooths only the interior. On by default; turn it off to soften the outer edge + // deliberately (which is a blunter version of the per-layer edge-smoothing falloff). + bool smooth_skip_border = true; + + // Which bake pipeline. On (the default): refine, remove slivers, displace and simplify in one run, + // nothing to prepare first. Off, the classic path: the mesh is prepared first (remesh, adaptive + // subdivision, step cut) and then displaced vertex by vertex, keeping the topology - which is what + // colour needs, since the per-facet assignment has nothing stable to attach to once the topology is + // rebuilt. Projects saved with the classic path keep it: the flag is stored per volume. + bool pipeline_v2 = true; + // Refinement edge length, mm. 0 (the default) means automatic: chosen from the texture's texel + // size and sharpness and the model's size, see recommend_v2_resolution(). A saved project with an + // explicit value keeps it. + float v2_refine_mm = 0.f; + bool v2_regularize = false; + // Simplification target in thousands of triangles. -1 (the default) means automatic, from the + // same recommendation; 0 skips simplification, which is worth comparing on its own. + int v2_max_triangles_k = -1; + // Slide vertices onto the texture's own edges before displacing. Displacement moves vertices along + // the normal only, so without this a step in the image is reproduced wherever the triangle grid + // happens to fall, as a staircase rather than a straight wall. + bool v2_relocate = false; + // Data-dependent edge flips before displacement (see TextureBakeFlip.hpp). On by default and not a + // user setting; deliberately left out of serialize() so project files are unaffected. + bool v2_flip_edges = true; + + // Colour, all of which belongs to the stack rather than to any one layer: it is about how the + // printer will realise the colours, not about which image they came from. + + // Interleave pairs of filaments to get colours between them - so four loaded filaments offer far + // more than four colours. Whether a given layer's colours actually use mixes is decided from its + // image (TextureDetail::flat_colors): a texture of flat colours prints in single filaments, a + // photograph or gradient in mixes. Off forces single filaments everywhere. + bool color_mix_enabled = true; + ColorMixMode color_mix_mode = ColorMixMode::Auto; + // Majority-filter passes over the assigned colours. See TextureColorRequest::despeckle_passes - + // this is the control for it, and 2 is enough to clear the salt-and-pepper an image with detail + // finer than the mesh leaves behind, without eating features that are genuinely a facet wide. + int color_despeckle = 2; + + template void serialize(Archive &ar) + { + int mix_mode = int(color_mix_mode); + ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border, + pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k, + v2_relocate, color_mix_enabled, mix_mode, color_despeckle); + color_mix_mode = ColorMixMode(mix_mode); + } +}; + +// How much detail a height texture carries: central differences of the grey image, the mean gradient +// and the share of texels steeper than 30 grey levels, mapped to how many texels one mesh edge may +// span (1 for a hard-edged image, 4 for a smooth one). Cached per image, like the decode. +struct TextureDetail +{ + float mean_gradient = 0.f; + float sharp_fraction = 0.f; + float pixels_per_edge = 4.f; + // How much of the image its eight most common colours cover (8 levels per channel), and the + // verdict: a "flat-colour" image (tiles, logos, camouflage) whose colours should each print in a + // single filament, versus a photograph or gradient where interleaved filament mixes pay off. + float flat_share = 0.f; + bool flat_colors = false; +}; +TextureDetail analyze_texture_detail(const TextureDisplacementLayer &layer); + +// The default pipeline's automatic resolution and budget, when the options leave them at "auto": +// edge = the model's world-space diagonal / 250, clamped to [0.05, 5] mm and rounded up to 0.01; +// budget 750 k. The texel size of the finest layer and its sharpness class are +// reported alongside for the panel. `edge_mm` is 0 for an empty mesh. +struct V2Resolution +{ + float edge_mm = 0.f; + int budget_k = 0; + float texel_mm = 0.f; + float pixels_per_edge = 0.f; + bool budget_bound = false; // the edge came from the triangle cap, not from the texture +}; +V2Resolution recommend_v2_resolution(const indexed_triangle_set &mesh, + const std::vector &layers, + const Transform3d &volume_to_world = Transform3d::Identity()); + +// Decoded height (and, for a colour source image, colour) samples, independent of any GUI/OpenGL +// texture object so they can be evaluated from a background bake Job as well as from GUI-side +// preview code. +struct DecodedHeightTexture +{ + std::vector pixels; // height: row-major, top-to-bottom, one byte per pixel + // Colour: the same grid, three bytes per pixel, or empty when the source image was grayscale. + // A grayscale height map has no colour to give - `pixels` is not a colour, it is a height - so + // has_color() is what the whole colour feature keys off: a layer set to colour a model with a + // grayscale texture on it simply colours nothing. + std::vector rgb; + int width = 0; + int height = 0; + + bool empty() const { return width <= 0 || height <= 0 || pixels.empty(); } + bool has_color() const { return !empty() && rgb.size() == size_t(width) * size_t(height) * 3; } + // Bilinearly sampled height in [0, 1] at a normalized uv coordinate. When tile_enabled is false, + // a uv outside [0, 1) samples as 0 - the texture simply is not there, rather than its border + // row/column being smeared outward forever (which is what clamping the coordinate would do, and + // was a real reported bug). Callers rely on this to get a hard edge: it is how the projection + // frame's border becomes the edge of the displacement. + float sample(const Vec2f &uv, bool tile_enabled = true, TextureTileMethod tile_method = TextureTileMethod::Repeat) const; + // The same sample, in colour: linear RGB components in [0, 1]. Outside a non-tiled placement, and + // for a grayscale source, this is (0, 0, 0) - callers pair it with has_color() and with the + // height's own coverage rather than trying to read "no texture here" out of the colour itself. + Vec3f sample_color(const Vec2f &uv, bool tile_enabled = true, + TextureTileMethod tile_method = TextureTileMethod::Repeat) const; + + // Where a uv lands on the texel grid: the four texels of the bilinear tap and their weights. + // Shared by sample() and sample_color(), so a layer's height and its colour can never end up + // read from different places in the image. False means the uv is outside a non-tiled placement - + // no texture there at all (see sample()). + struct TexelTap + { + int x0 = 0, y0 = 0, x1 = 0, y1 = 0; + float tx = 0.f, ty = 0.f; + }; + bool texel_tap(const Vec2f &uv, bool tile_enabled, TextureTileMethod tile_method, TexelTap &out) const; +}; + +// Decode a layer's raw image bytes into sampleable height data, plus colour when the source has any. +// Both 8-bit grayscale PNGs (the shipped library, and anything imported before colour was kept) and +// colour PNGs are accepted; for a colour source the height is its luminance, using the same +// coefficients wxImage::ConvertToGreyscale() uses, so a texture imported as colour displaces exactly +// as it did when the importer flattened it to grey on the way in. Returns an empty +// DecodedHeightTexture if image_data is empty or is not a PNG at all. +DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer); + +// Maps a linear RGB colour in [0, 1] to an index into the caller's palette, or -1 for "no colour". +// +// Deliberately a callback rather than a function here: matching a colour to a filament is a +// *perceptual* question (CIEDE2000 over CIELAB), and that machinery - slic3r/Utils/ColorSpaceConvert +// and GuiColor - lives on the GUI side along with the list of filaments actually loaded. libslic3r +// samples the image and decides *where* colour changes; the GUI decides *which* filament each colour +// is. See GLGizmoTextureDisplacement::make_palette_quantizer(). +using ColorQuantizeFn = std::function; + +// Resolves a palette index plus a surface position to the filament index that position should print +// in. A pure entry ignores the position; a mixed one interleaves its two filaments per ColorMixMode. +// +// Deliberately separate from ColorQuantizeFn, and deliberately *not* used by the subdivision's colour +// criterion: that criterion asks where the **perceived** colour changes, and must not see the +// interleaving. Refining on every band or dither-cell boundary would spend the whole triangle budget +// drawing a pattern the eye is supposed to blend away. +using ColorResolveFn = std::function; + +// One printable colour: either a loaded filament on its own, or a blend of two of them realised by +// interleaving (see ColorMixMode). Plain data, so it can be captured into a background job. +struct PrintableColor +{ + Vec3f rgb = Vec3f::Zero(); // what it looks like; for a mix, the perceptual average of the two + int a = 0; // filament index + int b = 0; // the second filament; == a for a pure entry + int num = 1; // a's share of the interleave, out of `den` + int den = 1; + bool is_mix() const { return a != b; } +}; + +// Everything needed to colour a mesh, captured on the main thread and handed to a job. An empty +// palette means nothing is colouring, which is the state every one of these paths starts in. +struct TextureColorSettings +{ + std::vector palette; + std::vector palette_pure; // the filaments alone, for flat-colour images + ColorMixMode mix_mode = ColorMixMode::ZBands; + float layer_height = 0.2f; // sizes the Z bands + float dither_cell_mm = 0.4f; // sizes the XY dither cells + int despeckle_passes = 2; + + bool empty() const { return palette.empty(); } +}; + +// Raw dominant-axis planar projection of `position` (in mm, not yet scaled/rotated/offset by any +// layer), dropping the axis position that best aligns with `normal`. Exposed on its own (rather +// than only inline inside project_texture_displacement_uv()) so GUI code - the on-canvas +// "adjust texture placement" gizmo - can map a dragged 3D point into the exact same 2D space +// tiling_scale/rotation_deg/offset operate in, without duplicating the axis-selection logic. +Vec2f project_planar(const Vec3f &position, const Vec3f &normal); + +// Applies a layer's tiling_scale/rotation_deg/offset to an already-projected planar coordinate +// (in mm, dominant-axis planar, cylindrical, spherical, or CGAL LSCM output - any of them, all +// share this same final step). Exposed separately so build_texture_displacement() can route CGAL +// LSCM's per-patch UV solve through the same scale/rotate/offset controls as every other +// projection method, without going through project_texture_displacement_uv()'s own dispatch +// (which only knows how to compute the *analytic* methods from a single vertex + normal). +// `aspect` is the height map's width / height. It scales the v axis so a non-square image is not +// squeezed into a square tile: `tiling_scale` is the tile's size along u, and the tile is +// `tiling_scale * height / width` mm along v, which keeps texels square. 1 (the default) is the +// square case and leaves the coordinate exactly as it always was. +Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f); + +// Applies a row-major 3x4 projective matrix (see TextureDisplacementLayer::view_project_matrix) to a +// local-space point, writing the resulting texture uv. Returns false - and leaves `uv` untouched - +// when the point lies behind the projector or on its plane (w <= 0), where there is no meaningful uv +// and dividing would produce a mirrored or infinite coordinate. Callers treat that as "no height". +bool project_uv_projective(const std::array &m, const Vec3f &position, Vec2f &uv); + +// Sample a layer's height texture at a mesh-local position, honouring the layer's projection +// method, tiling scale, rotation, offset and tiling mode. Returns a height in [0, 1]. +// +// This returns a *height* rather than a UV because TextureProjectionMethod::Triplanar is a blend +// of three separate axis projections and therefore takes three texture samples per vertex - there +// is no single UV that represents it. The other methods do map to one UV internally. +// - `normal` is this specific vertex's own normal; used only by Triplanar (for its blend weights). +// - `patch_center`/`patch_axis` describe the painted patch as a whole (its centroid, and - for +// Cylindrical only - the wrap axis); used only by the Cylindrical/Spherical methods. +// - `lscm_uv`, when non-null, is this vertex's precomputed LSCM coordinate and takes precedence +// over `layer.projection_method` (LSCM is a single per-patch solve, not a per-vertex formula, +// so build_texture_displacement() computes it once up front and passes it in here). +// patch_center/patch_axis are cheap to compute once per patch and passed through unchanged for +// every vertex rather than being re-derived per call. +float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisplacementLayer &layer, + const Vec3f &position, const Vec3f &normal, + const Vec3f &patch_center = Vec3f::Zero(), const Vec3f &patch_axis = Vec3f::UnitZ(), + const Vec2f *lscm_uv = nullptr); + +// The same sample, in colour, through the identical projection/tiling/placement path - so a layer's +// colour lands on the model exactly where its relief does, whatever projection it is using. Returns +// false (leaving `out` untouched) when the texture has no colour, or when the point falls outside a +// non-tiled placement, or behind a projective "from view" projector: all three mean "this layer does +// not colour this point", which is different from "this layer colours it black". +bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDisplacementLayer &layer, + const Vec3f &position, const Vec3f &normal, Vec3f &out, + const Vec3f &patch_center = Vec3f::Zero(), const Vec3f &patch_axis = Vec3f::UnitZ(), + const Vec2f *lscm_uv = nullptr); + +// Area-weighted centroid and average normal of a layer's currently painted patch, in mesh-local +// coordinates - the same measurements build_texture_displacement() uses to pick its dominant +// projection axis. Used by the GUI to anchor the on-canvas "adjust texture placement" gizmo to +// wherever the layer is actually painted. Returns false (leaving the outputs untouched) if the +// layer has nothing painted yet. +bool compute_layer_paint_anchor(const indexed_triangle_set &base_mesh, + const TriangleSelector::TriangleSplittingData &facet_data, + Vec3f &anchor_pos, + Vec3f &anchor_normal); + +// Extracts the currently painted patch from a volume's base mesh + stored facet data - the same +// extraction build_texture_displacement() and compute_layer_paint_anchor() each do internally via +// TriangleSelector::get_facets_strict(ENFORCER). Returns an empty mesh if nothing is painted. +// Exposed so GUI code (the LSCM "UV editor" preview pane) can get the same patch build_texture_ +// displacement() would act on, without duplicating the deserialize/get_facets_strict boilerplate. +indexed_triangle_set extract_painted_patch(const indexed_triangle_set &base_mesh, + const TriangleSelector::TriangleSplittingData &facet_data); + +// A patch flattened into 2D. The patch is first split into charts along edges sharper than +// `seam_angle_deg` (see LSCM_DEFAULT_SEAM_ANGLE_DEG), each chart is flattened independently, the +// charts are scaled to their true mm size and packed side by side. +// +// A vertex sitting on a seam belongs to several charts at once and therefore has a *different* UV +// in each of them, so this cannot be a plain "one UV per patch vertex" array: seam vertices are +// duplicated, once per chart touching them. `indices` is the patch's own triangle list re-indexed +// onto that duplicated vertex set, and `source_vertex` maps each duplicate back to the patch vertex +// it came from. +struct PatchUnwrap +{ + std::vector uvs; // one per unwrapped vertex, in mm + std::vector source_vertex; // unwrapped vertex -> index into patch.vertices + std::vector vertex_chart; // unwrapped vertex -> chart (island) id + // The patch's triangles re-indexed into `uvs` - but *grouped by chart*, not left in the patch's + // own order: the charts are flattened one at a time and then concatenated. `source_face` is the + // map back, so anything that needs UVs per triangle corner (as opposed to per vertex) can place + // them against its own triangle list. See compute_lscm_corner_uvs(). + std::vector indices; + std::vector source_face; // unwrapped triangle -> index into patch.indices + // Per chart, the centroid of its uvs - the point a TextureIsland's rotation turns about. + std::vector chart_centroid; + // Edges belonging to exactly one triangle: the outline of each island. Indices into `uvs`. This + // is what the UV editor draws highlighted, so the boundaries the seam angle cut are visible. + std::vector> boundary_edges; + int chart_count = 0; + + bool empty() const { return indices.empty(); } +}; + +// Applies an island's hand placement (scale + rotation about its own centroid, then offset) to one +// unwrapped UV. A chart with no entry in `islands` is left exactly where the packing put it. +Vec2f apply_island_transform(const Vec2f &uv, int chart, const PatchUnwrap &unwrap, const std::vector &islands); + +// The same transform as a 2x3 affine matrix (columns: x basis, y basis, translation), for callers +// that would otherwise apply it to every vertex of an island one at a time. The UV editor renders +// each island through this as a uniform, which is what lets a drag move an island without touching +// its vertex buffer at all. +Eigen::Matrix island_transform_matrix(int chart, const PatchUnwrap &unwrap, const std::vector &islands); + +// Lays the unwrap's charts out as connected nets: charts that share a mesh edge are unfolded so +// their shared edge coincides (a cube -> its six faces joined along a spanning tree of edges, the rest +// left as free borders). Charts stay separate islands, so their borders still show and any of them can +// still be moved by hand afterwards. Each net grows from the largest chart not yet placed; a chart whose +// triangles would overlap the net stays out of it and starts a net of its own. The nets are then packed +// side by side. Returns one placement per chart. See the gizmo's auto-connect option. +std::vector compute_connected_net(const PatchUnwrap &unwrap); + +// The placement that unfolds `child` onto `parent` along their shared mesh edge, honouring `parent`'s +// current placement in `islands`. Returns false if the two charts share no edge. Backs the manual +// "Join" command; compute_connected_net() does the same thing across a whole spanning tree. +bool join_chart_placement(const PatchUnwrap &unwrap, const std::vector &islands, + int child, int parent, TextureIsland &out_child); + +// Unwraps `patch` as described above. Charts that are flat (within a degree) are projected onto +// their own tangent plane directly, which is both exact and far cheaper than a solve; only genuinely +// curved charts go through CGAL's LSCM parameterizer (MeshBoolean::cgal::parameterize_lscm()). A +// chart that LSCM cannot flatten at all (it is not a topological disk - closed, or with a hole) +// falls back to that same tangent-plane projection. +// +// `padding_mm` is the gap the packing leaves between islands; negative means auto (see +// TextureDisplacementLayer::island_padding_mm). `seam_edges` are extra edges to cut along regardless +// of angle (manual/auto seams), in the patch's own vertex-index space (which is the mesh's, since the +// patch carries the whole vertex array - see get_facets_strict()). +// +// Results are cached, keyed on the patch's geometry, the seam angle, the padding and the seam edges: +// nothing else about a layer (depth, tiling, rotation, offset, texture, island placement) changes the +// unwrap, so dragging any of those sliders must not pay for a re-solve. +PatchUnwrap compute_patch_unwrap(const indexed_triangle_set &patch, float seam_angle_deg = LSCM_DEFAULT_SEAM_ANGLE_DEG, + float padding_mm = -1.f, const std::vector> &seam_edges = {}); + +// One UV per patch vertex, **for displacement only**. Displacement is inherently per-vertex - a +// vertex has exactly one position, so it can only be pushed out by one height - which means a seam +// vertex has to settle on a single one of its charts' UVs (the first, arbitrarily). That is not a +// compromise in the result: the surface stays watertight either way, since neighbouring vertices +// each move along their own normals and nothing depends on the UVs agreeing across the seam. +// +// Anything that samples or draws per *triangle* must use compute_lscm_corner_uvs() instead. This +// collapse is wrong for those: a triangle at a seam that the island layout did not join gets handed +// a neighbouring island's placement, which showed up as a single skewed triangle per face and as +// every island's texture following the lowest-numbered island when it was dragged. +// +// Returns an empty vector if the patch has no triangles. Takes the whole layer because it applies +// both the layer's seam angle and its hand-placed islands. +std::vector compute_lscm_uvs(const indexed_triangle_set &patch, const TextureDisplacementLayer &layer); + +// Three UVs per patch triangle (corner 0, 1, 2 of triangle i at index 3i..3i+2), in the patch's own +// triangle order. Unlike compute_lscm_uvs() this keeps a seam vertex's separate per-chart copies: a +// triangle belongs to exactly one chart and is given that chart's UVs, which is what every consumer +// that works per triangle rather than per vertex needs - the fast preview's flat mesh, the checker +// overlay and the bake's per-facet colour. +// +// Returns an empty vector if the patch has no triangles or the unwrap carries no source_face map. +std::vector compute_lscm_corner_uvs(const indexed_triangle_set &patch, const TextureDisplacementLayer &layer); + +// The TextureDisplacementLayer::lscm_uv_overrides key for one unwrapped vertex (an index into PatchUnwrap::uvs). +inline int lscm_uv_override_key(int unwrapped_vertex) { return -(unwrapped_vertex + 1); } + +// Writes the overrides into `unwrap.uvs`: mesh-vertex keys onto every copy of their vertex, then unwrapped-vertex +// keys onto their one copy. Returns, per unwrapped vertex, whether an override set it. +std::vector apply_lscm_uv_overrides(PatchUnwrap &unwrap, const std::vector> &overrides); + +// One paint mask (as stored by ModelVolume::texture_displacement_facets) per possible layer slot. +using TextureDisplacementFacetsData = std::array; + +// Bake all painted texture-displacement layers into `base_mesh`'s geometry, restricted to the +// painted area(s) only (the rest of the mesh is left untouched). Returns the mesh unchanged if +// nothing is painted or no layer has a usable texture. +// +// **Topology-preserving**: the returned mesh has exactly `base_mesh`'s vertices and triangles, in +// the same order - only the positions of displaced vertices differ. Every layer's paint mask is +// evaluated against `base_mesh` directly, and each vertex accumulates a single signed displacement +// (in mm) that all the layers covering it fold into, in slot order, via their TextureBlendMode. +// The vertex is then moved once, along its base-mesh normal, by that accumulated total. +// +// This replaced an earlier design that instead applied the layers *sequentially*, re-meshing after +// each one and carrying the next layer's paint mask onto the result with +// TriangleSelector::remap_painting(). That was the cause of a real "the second texture is never +// applied" bug: remapping a mask onto a mesh whose vertices had just been displaced out from under +// it routinely produced an empty bitstream, and the layer was then silently skipped. It is also +// what forced the per-layer vertex duplication and the final its_compactify_vertices() pass. The +// accumulate-then-displace formulation has neither problem, is substantially faster (no remap, no +// welding, one pass over the mesh), and - because the output keeps the input's exact vertex +// indexing - lets the GUI overlay a preview on the base mesh without any index translation. +// +// A vertex used by even one *unpainted* triangle of a layer's mask sits on that layer's boundary. +// Whether it moves is TextureDisplacementOptions::displace_border; see that field for why displacing +// it is safe (and the default). Either way the *direction* every vertex moves in is the area-weighted +// normal of the triangles that are painted in at least one layer - not of the whole mesh - so a +// border vertex travels along the painted surface's own normal instead of a blend with whatever +// unpainted geometry meets it there. Without that, the rim of a fully painted face would displace +// along the 45 degrees bisector it shares with the side wall and flare outwards. Interior vertices +// have every incident triangle painted, so for them the two are the same normal. +// +// Takes plain copied data rather than a ModelVolume reference so it is safe to call from a +// background thread (e.g. a bake Job's process() method) on a snapshot captured on the main +// thread, without touching the live Model concurrently with the UI. +// +// Known limitation: this does not attempt to remap texture-displacement paint data across +// topology-changing operations performed outside this gizmo (e.g. ModelObject::split(), +// mesh-boolean ops) the way TriangleSelector::remap_painting() does for the other paint channels. +// Such operations will silently drop any unbaked texture-displacement paint on the affected +// volume. This is an explicit extension point for a later phase, not an oversight. +// +// `progress`, when set, is called from the worker thread with a 0..100 completion percentage as the +// bake proceeds. Returning false from it aborts the run, which then returns an *empty* mesh - never +// a partially displaced one, so a cancelled bake can never be mistaken for a finished result and +// committed. It exists because this is the one call in the feature that can take seconds on a +// subdivided mesh, and without it the progress notification the Job framework puts on screen sits at +// 0% for the whole run and offers no way to close it (its close button only appears at 100%). +// +// `color`, when given, also reports which filament each triangle should print in - see +// TextureColorRequest. +struct TextureColorRequest +{ + // RGB -> palette index. Supplied by the GUI, which owns both the perceptual matching and the list + // of filaments actually loaded (see ColorQuantizeFn). + ColorQuantizeFn quantize; + // The same over the loaded filaments alone, no mixes. Optional; when given, a layer whose image is + // made of flat colours (TextureDetail::flat_colors) is matched with this one, so a tile or a logo + // prints in single filaments while a photograph on another layer may still use mixes. + ColorQuantizeFn quantize_pure; + // Palette index + position -> filament. Optional: without it a palette index is taken to be a + // filament index directly, which is the no-mixing case. + ColorResolveFn resolve; + // Majority-filter passes over the *perceived* colour, before any interleaving is resolved. + // + // Sampling a detailed image once per triangle leaves salt-and-pepper wherever the image's own + // detail is finer than the mesh: two neighbouring facets land either side of some contour and flip + // colour independently. Replacing each facet's colour with the most common one among itself and + // its edge neighbours removes exactly that, and leaves any feature wider than a facet alone. 0 + // turns it off. + int despeckle_passes = 0; + // After the despeckle: connected patches of one colour smaller than this (mm^2) are recoloured + // to whatever borders them most - see merge_small_color_regions(). The despeckle only reaches + // single facets; an image detail a few facets wide still leaves thousands of pinhead islands + // that the slicer's multi-material segmentation cannot digest. 0 turns it off. + float min_color_region_mm2 = 0.5f; + // Filled per *base mesh* triangle (the bake is topology-preserving, so this indexes the returned + // mesh too): the quantize callback's index plus one, or 0 for "this triangle takes no colour from + // the texture". The +1 is not arbitrary - it lines up with EnforcerBlockerType, where 0 is NONE + // ("use the volume's own filament") and 1..16 are Extruder1..16, so the caller can hand these + // straight to a TriangleSelector without a second mapping table. + std::vector *out_triangle = nullptr; +}; + +// Recolours connected patches of one colour whose area is under `min_area_mm2` to the colour that +// borders them most (by shared edge length). Colour is per triangle, -1 = none (never merged into, +// never merged away). Removes the confetti a detailed image leaves on a fine mesh - thousands of +// one-facet zones, which the slicer's multi-material segmentation cannot digest. Patches are +// processed smallest-first, reading their neighbours' current colour, so a chain of tiny islands +// collapses into its surroundings rather than into each other. +void merge_small_color_regions(const indexed_triangle_set &mesh, std::vector &color, float min_area_mm2); + +// Where the volume sits on the plate: its instance transform times its own volume transform, i.e. +// mesh coordinates -> world millimetres. +// +// Every number the user sets is in real millimetres on the printed part - "Depth (mm)", "Tile size +// (mm)" - and the build plate is a world plane, so the bake runs in world space and transforms the +// result back. Doing it in the volume's own coordinates instead made a scaled instance stretch both +// the relief depth and the tiling by the scale factor, and under a non-uniform scale it also +// displaced along the wrong direction: a mesh normal maps to the world normal through the inverse +// transpose, not through the transform itself, so the relief leaned. Identity - the default - is +// exactly the old behaviour and is what an untransformed volume gives. +// What a bake spent, for whoever wants to report it. Only the default pipeline fills it in; the +// classic path moves the vertices the mesh already has and has nothing to say here. +struct TextureBakeStats +{ + // What the refinement produced, before simplification. + size_t triangles_refined = 0; + // What the result had to fit into: the budget for what this bake refines, plus the triangles it + // only preserves (an earlier bake's relief, which this one does not paint). + size_t triangles_budget = 0; + // The refined mesh did not fit, and removing its flat faces was not enough, so the simplification + // had to take detail out of it: the result carries less of the texture than the resolution asked for. + bool budget_limited = false; +}; + +indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh, + const std::vector &layers, + const TextureDisplacementFacetsData &facets_data, + const TextureDisplacementOptions &options = {}, + const DisplacementProgressFn &progress = {}, + const TextureColorRequest *color = nullptr, + const Transform3d &volume_to_world = Transform3d::Identity(), + // When given and enabled, receives the mesh after each + // stage, already brought back into `base_mesh`'s frame. + BakeStageRecorder *debug = nullptr, + // When given, receives what the bake spent. + TextureBakeStats *stats = nullptr); + +// `volume`'s mesh coordinates -> world millimetres: its first instance's transform times its own. +// The mesh is shared by every instance, so a multi-instance object can only be baked for one of +// them; the first is what the gizmo edits against. Identity when the volume has no object yet. +Transform3d texture_displacement_volume_to_world(const ModelVolume &volume); +// The frame the bake and the previews project the texture in: `volume_to_world` with its translation +// removed, i.e. world orientation and scale about the volume's own origin. See build_texture_displacement(). +Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world); + +// Area-weighted vertex normals of `its` - the directions the bake both projects and displaces along. +std::vector texture_displacement_vertex_normals(const indexed_triangle_set &its); + +// The frame the Cylindrical and Spherical projections wrap around: `patch`'s triangle-corner centroid, +// the world axis *least* aligned with the average of `vertex_normals` over those corners (a cylinder's +// own axis is perpendicular to its outward radial normal), and that average normal itself. +// +// Results come out in whatever frame `patch` is given in. The bake calls this with the patch already in +// the bake frame (see texture_displacement_bake_frame()), so a preview that wants to reproduce the +// bake's projection must too, or it wraps the texture around a different centre. Corners past the end +// of `vertex_normals` - the ones a brush stroke appended - contribute to the centroid but carry no +// normal, exactly as the bake's own loops skip them. +void texture_displacement_patch_frame(const indexed_triangle_set &patch, + const std::vector &vertex_normals, + Vec3f ¢er, Vec3f &axis, Vec3f &average_normal); + +// Convenience overload for main-thread callers: extracts the mesh/layers/paint data/options from +// `volume` and forwards to the overload above. +indexed_triangle_set build_texture_displacement(const ModelVolume &volume); + +// Laplacian relaxation of `mesh` in place, restricted to the vertices flagged in `movable` (sized to +// the mesh's vertex count; anything else is held exactly where it is and still acts as an anchor for +// its neighbours). Each of `iterations` passes moves a movable vertex a `strength` fraction of the +// way to the average of the vertices it shares an edge with, computed from the positions at the +// start of that pass so the result does not depend on vertex order. +// +// Topology-preserving like the bake itself, so it composes with it: this is what "smooth the relief +// after displacing it" runs, and it is also safe to run standalone on an already baked mesh. +// `strength` is clamped to [0, 1]; 0 iterations, an empty/mis-sized `movable`, or an all-false one +// leave the mesh untouched. +// `on_pass`, when set, is called with the 0-based index of each completed pass; returning false stops +// the relaxation there, leaving the passes already done in place. +void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector &movable, float strength, + int iterations, const DisplacementProgressFn &on_pass = {}); + +// Returns a scalar height (in mm - a displacement magnitude) at a surface point, given that point's +// position and interpolated normal. This is what feature-adaptive subdivision samples to decide +// where the displaced surface has *curvature* worth spending triangles on. Called serially from the +// subdivider, so it only needs to be safe on the calling thread. +using HeightFieldSampler = std::function; + +// Builds a sampler of the *combined* (all-layers) displacement height in mm at an arbitrary surface +// point, for feature-adaptive subdivision. It mirrors build_texture_displacement()'s per-layer setup +// (decode, patch centroid, cylinder axis, blend order, "lowest layer folds additively") but evaluates +// per point instead of per vertex. Two deliberate simplifications, both erring toward *more* detail +// (safe - over-refinement is never a crack): every sampleable layer is evaluated at every point (no +// per-point paint-mask test, so a point sees all layers' textures, not only the ones painted there), +// and edge-smoothing's boundary falloff is ignored. LSCM layers have no per-point UV and are skipped. +// Returns a null sampler (bool false) when no layer can be sampled - the caller then falls back to +// uniform adaptive subdivision. +// Which filament the texture stack would put at a point, as a palette index (or -1 for "no colour +// here"). The colour analogue of HeightFieldSampler, and used the same way: to decide where the +// adaptive subdivision needs triangles. Colour lands per *facet*, so a colour boundary is a step the +// mesh can only draw if there are edges along it - the chord-error test that drives the height +// refinement is blind to it, exactly as it is blind to the paint's own border. +using ColorFieldSampler = std::function; + +// The colour counterpart of make_combined_displacement_sampler(), over the same layers, and skipping +// the same ones (LSCM has no per-point UV). Layers without color_enabled, and layers whose texture is +// grayscale, contribute nothing; a higher slot wins over a lower one where they overlap, matching the +// bake. Returns null when no layer can colour anything, in which case there is nothing to refine for. +ColorFieldSampler make_combined_color_sampler(const indexed_triangle_set &base_mesh, + const std::vector &layers, + const TextureDisplacementFacetsData &facets_data, + ColorQuantizeFn quantize, + ColorQuantizeFn quantize_pure = nullptr); + +HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set &base_mesh, + const std::vector &layers, + const TextureDisplacementFacetsData &facets_data); + +// Uniformly subdivides `mesh` (every triangle recursively split into 4 via edge midpoints, using a +// shared cache so a midpoint is computed once and reused by both triangles on either side of that +// edge) until every edge is at or below max_edge_length_mm, or max_iterations passes have run, +// whichever comes first (bounding the worst-case triangle-count explosion on a very fine target). +// +// This exists so a low-poly input model can still get fine-grained texture displacement detail - +// build_texture_displacement() can only ever move existing vertices, so a patch with only a +// handful of vertices to begin with cannot show much detail no matter the texture's resolution. +// +// Deliberately whole-mesh and uniform, not limited to a painted patch: subdividing only part of a +// mesh while leaving the rest untouched creates a classic T-junction/cracking problem where the +// denser and sparser regions meet (the finer side has edge midpoints the coarser side doesn't +// know about). Uniform, whole-mesh subdivision has no such seam and stays manifold, at the cost of +// applying everywhere rather than just where texture detail is actually wanted - meant to be run +// once, deliberately, before painting (see the gizmo's "Subdivide model" button), not automatically +// during baking. +indexed_triangle_set subdivide_mesh_uniform(const indexed_triangle_set &mesh, float max_edge_length_mm, int max_iterations = 6); + +// Adaptive subdivision by Rivara longest-edge bisection, restricted to a region. +// +// Unlike subdivide_mesh_uniform() this only densifies where asked - `refine_region` (indexed by +// input-triangle index; empty or all-false => no-op) flags the triangles allowed to drive refinement +// - so a small painted patch on a large model does not quadruple the whole model's triangle count. +// It is nonetheless *conformal*: it never leaves a T-junction/crack at the boundary between the +// refined and coarse regions (the trap that made subdivide_mesh_uniform() deliberately whole-mesh). +// +// A triangle wants refining while it is over at least one of these, whichever applies: +// - length: its longest edge exceeds `target_edge_length_mm` (a *baseline* - it applies in feature +// mode too, and is what stops a coarse triangle from being declared flat merely because +// the four points the chord test samples happened to land at similar heights on a +// high-frequency texture: the classic aliasing stall); +// - feature: (only when `sampler` is set and `chord_tolerance_mm > 0`) the *displaced* surface +// departs from the flat triangle by more than `chord_tolerance_mm`, measured as the max +// over the three edge midpoints AND the centroid of |sampled displacement - the flat +// triangle's barycentric interpolation|. Sampling the interior, not just edge midpoints, +// is what catches a hill that sits inside a triangle. This is a *curvature* test: it is +// exactly zero on a plane or a linear ramp (barycentric interpolation is exact there, so +// those stay coarse - the case a gradient criterion would over-refine) and large on a +// hill/ridge/noise. +// `min_edge_length_mm` is a hard floor under both: no triangle whose longest edge is already at or +// below it is ever refined, which is also what guarantees termination across a sharp texture step +// (where the chord error never falls below the tolerance no matter how fine the mesh gets). +// +// Refinement runs to completion, not for a fixed number of passes: triangles are taken worst-first +// from a max-heap keyed by how many times over its criteria each one is, so a run that hits the +// `max_triangles` budget has spent it on the largest errors rather than wherever a sweep happened to +// reach. The budget is the only bound on a pathological height field; stopping on it leaves a +// perfectly valid, still-conformal mesh. +// +// How it stays crack-free: only "terminal" edges are ever bisected - an edge that is the longest edge +// of *both* triangles sharing it (or a boundary edge that is the longest of its one triangle). +// Bisecting such an edge splits both its triangles 1->2 around the same new midpoint, so a hanging +// node is never created. The edge to split for a triangle that wants refining is found by Rivara +// longest-edge propagation (LEPP): walk to the longest edge of ever-longer-edged neighbours until a +// terminal edge is reached, and bisect that. Edge length strictly increases along the path (ties +// broken by a mesh-vertex key, which both sides of an edge compute identically), so the walk cannot +// cycle, and Rivara's result is that repeating it refines the original triangle in a bounded number +// of bisections - closing the propagation gap a plain per-edge test leaves behind. The transition +// triangles this pulls in just outside the region are the graded band that makes the size change +// conformal; they are a bounded cost paid once, not a per-pass tax. +// +// If `out_source` is non-null it is resized to the output triangle count and out_source[i] receives +// the input triangle that output triangle i descends from (children inherit their parent's index), so +// a caller can carry per-triangle data - e.g. a paint mask - across the topology change without a +// geometric remap. +// +// `refine_region` is a **bitmask** per input triangle, not a plain flag: +// bit 0 (REFINE_PAINTED) - inside the painted area: refine by the length baseline and, in feature +// mode, by the chord-error test. +// bit 1 (REFINE_BORDER) - inside the band straddling the paint's edge: refine by +// `border_edge_length_mm` alone. +// A value of 1 therefore means exactly what a plain 1 always meant, and 0 still means "never touch +// this triangle except through the conformal closure". +// +// The border band exists because the chord-error test is blind to the one discontinuity the bake +// actually creates. `make_combined_displacement_sampler()` evaluates the height field everywhere, +// with no per-point paint test, so where the paint *stops* it keeps reporting full relief - smooth +// and low-curvature - while the baked surface steps from full displacement to zero. The test sees no +// error there and leaves the transition at whatever density the input had, which is what turns the +// rim of an unpainted island into a ring of large, steeply tilted triangles. Refining that band by +// plain edge length is bounded (it is a thin ring, and a length target always terminates) and needs +// no paint-aware sampler. +// +// `color`, with a positive `color_edge_length_mm`, adds a third criterion inside the painted area: a +// triangle whose corners, edge midpoints and centroid do not all map to the *same* filament straddles +// a colour boundary, and is refined by plain edge length down to that target. Length rather than any +// error measure, for the same reason the border band uses length - the thing being fixed is the size +// of the triangles spanning a step, not the curvature of anything - and because a step's error never +// falls however fine the mesh gets, so only a length target (floored by min_edge_length_mm) is +// guaranteed to terminate. Without this a colour boundary lands on whatever triangles the *height* +// happened to need, which on a flat surface is none at all. +// +// `progress`, when given, is called with a 0..100 percentage of the triangle budget spent; returning +// false stops the refinement early. What it hands back then is still a complete, conformal mesh - the +// loop only ever finishes whole bisections - so a caller that wants to discard it has to do so itself. +indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh, + const std::vector &refine_region, + float target_edge_length_mm, int max_triangles = 1000000, + std::vector *out_source = nullptr, + const HeightFieldSampler &sampler = nullptr, + float chord_tolerance_mm = 0.f, float min_edge_length_mm = 0.f, + float border_edge_length_mm = 0.f, + const DisplacementProgressFn &progress = nullptr, + const ColorFieldSampler &color = nullptr, + float color_edge_length_mm = 0.f, + // Step mode, for a mesh that cut_mesh_at_steps() will cut + // next: a triangle one of whose edges crosses a *sharp* + // step of the height field (a jump of 40 % of the relief + // between two samples at the sample spacing, across the + // mid-level) gets a chord error of zero, since the step is + // the cutter's to reproduce and refining it only carpets + // the edge of the pattern. Features that no edge crosses + // yet still refine until one does. No effect outside + // feature mode. + bool split_multi_crossings = false); + +// Cuts `mesh` along the height field's mid-level contour wherever the field steps sharply across it, +// and doubles the seam, so that displacing the result produces a vertical wall at the step instead of +// a ramp across whichever triangle the step happened to fall in. +// +// This is what a binary height map - a grid, stripes, a knurl, wood grain as black-and-white bands - +// needs, and what refinement alone cannot give it. Refining a triangle that straddles a step never +// brings the chord error under any tolerance: the surface has a discontinuity, and a finer triangle only +// makes the ramp narrower. Every refinement level then leaves a band of its own size along every step, +// and the triangle budget ends up spent on carpeting the edges of the pattern, one to two orders of +// magnitude more triangles than the pattern needs, while the ramps are still visible. +// +// What it does, in order: +// - Samples the field at the vertices along the bake's normals and takes the mid-level as the contour. +// A vertex that sits inside a step's blend is nudged along the surface, down its own side's slope, +// until it samples a pure value, so no vertex bakes to a half height. +// - Marches every painted edge for crossings of the mid-level (sampled at half `step_width_mm`, then +// bisected). Both triangles at an edge see the same crossings, which keeps the result conformal. A +// crossing is *sharp* when the field changes by half its range within `step_width_mm`. +// - Traces the contour inside each triangle on a local lattice (marching triangles) and keeps the +// polylines that join one crossing to another; a closed loop inside a triangle is left to +// refinement. Each polyline is simplified and kept clear of its neighbours. +// - Doubles the seam: every polyline vertex and every crossing gets a copy on each side, moved +// `seam_gap_mm` apart across the contour to where each samples a pure value of its own side. The +// regions between the seams are ear-clipped; the strip between the two copies is triangulated flat +// and becomes the wall once the high side is displaced. A triangle whose contour could not be traced +// is split at its crossings without a seam, so the neighbours still meet it without a T-junction. +// +// Nothing is cut unless the texture is a step texture: most vertex heights sit at one of two levels, +// most crossings are sharp, and the features are wider than a couple of step widths (a noisy or a +// smooth relief is passed through unchanged, as is a mesh with nothing painted). If the cut would leave +// more than a trace of inverted triangles the input is returned unchanged as well. +// +// `region` flags the triangles that may be cut (the painted ones, same encoding as +// subdivide_mesh_adaptive(), any non-zero value). An edge may carry any number of crossings; two closer +// than `max(min_feature_mm, seam_gap_mm)` are a feature too thin to carry a seam and are dropped as a +// pair, leaving the surface flat there. `out_source` receives the input triangle each output triangle +// descends from, so paint carries over; `out_cut_count` the number of input triangles that were cut +// (0 means the mesh came back unchanged). +indexed_triangle_set cut_mesh_at_steps(const indexed_triangle_set &mesh, const std::vector ®ion, + const HeightFieldSampler &sampler, float step_width_mm, + float seam_gap_mm, float min_feature_mm = 0.f, + std::vector *out_source = nullptr, size_t *out_cut_count = nullptr); + +// The cutter's verdict alone: whether cut_mesh_at_steps() with the same arguments would cut anything, +// judged the same way (two levels, sharp crossings, features wider than the step) but without the +// vertex nudge or any tracing, so it is cheap enough to ask on the coarse mesh. The prepare path asks +// it *before* refining: a subdivision run in step mode leaves the steps alone for the cutter, which is +// only right if the cutter is then going to cut them. +bool texture_has_steps_to_cut(const indexed_triangle_set &mesh, const std::vector ®ion, + const HeightFieldSampler &sampler, float step_width_mm, float seam_gap_mm, + float min_feature_mm = 0.f); + +// The recipe for getting a mesh ready to receive displacement: even out the triangle density, then +// refine it where the texture bends. Either stage is skipped when its target is <= 0. Pure data, and +// the whole of it, so the preparation can be handed to a background job instead of running on the UI +// thread - see GLGizmoTextureDisplacement::prepare_mesh(). +struct TextureDisplacementPrepareParams +{ + // Isotropic remesh (CGAL). The target is clamped against the part's own surface area before it is + // used, so a value that would produce millions of triangles cannot be asked for by accident. + float remesh_edge_mm = 0.f; + float remesh_sharp_deg = 0.f; // 0 = do not protect sharp edges + + // Adaptive (Rivara) subdivision of the painted area. See subdivide_mesh_adaptive(). + float subdiv_target_mm = 0.f; // "Max edge": the length baseline, and the only criterion when + // subdiv_feature is off + float subdiv_detail_mm = 0.f; // "Detail": chord tolerance, feature mode only + float subdiv_min_edge_mm = 0.f; // "Min edge": the floor under both, feature mode only + float subdiv_border_mm = 0.f; // "Edge detail": the band straddling the paint's edge, 0 = off + bool subdiv_feature = false; // follow texture curvature, not just edge length + int subdiv_added_triangles = 0; // budget, *added* to the mesh's own count + // Edge length triangles straddling a *colour* boundary are refined to, 0 = do not look at colour. + // Separate from the height criteria because colour lands per facet: a flat surface carrying a + // sharp colour edge needs triangles along that edge even though its height is perfectly smooth. + float subdiv_color_edge_mm = 0.f; + // Cut the mesh along sharp steps in the texture after refining, so they bake as walls rather than + // ramps. See cut_mesh_at_steps(). Off leaves the old ramp behaviour. + bool cut_steps = false; +}; + +// What a preparation run produced. An empty `mesh` means there was nothing to do and the caller must +// commit nothing - which is not a failure: a mesh that is already even needs no remesh, and one that +// is already fine enough for the texture needs no subdivision. +struct TextureDisplacementPrepareResult +{ + indexed_triangle_set mesh; + TextureDisplacementFacetsData masks; + // The remesh landed but no layer's paint survived being carried onto it. Nothing is committed: + // everything downstream is driven by that paint, so baking on would bake a flat mesh. + bool paint_lost = false; +}; + +} // namespace Slic3r + +#endif // slic3r_TextureDisplacement_hpp_ diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index 3dfea4c0c7..4d6007530f 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -1519,9 +1519,11 @@ void TriangleSelector::get_facets(std::vector& facets_per_ } } -indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType state) const +indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType state, std::vector *out_source) const { indexed_triangle_set out; + if (out_source) + out_source->clear(); size_t num_vertices = 0; for (const Vertex &v : m_vertices) @@ -1535,8 +1537,13 @@ indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType sta out.vertices.emplace_back(v.v); } - for (int itriangle = 0; itriangle < m_orig_size_indices; ++ itriangle) + for (int itriangle = 0; itriangle < m_orig_size_indices; ++ itriangle) { this->get_facets_strict_recursive(m_triangles[itriangle], m_neighbors[itriangle], state, out.indices); + // Everything the recursion just appended came from this original triangle, whatever depth it + // was split to. Recording it here keeps the recursive helpers untouched. + if (out_source) + out_source->resize(out.indices.size(), itriangle); + } for (auto &triangle : out.indices) for (int i = 0; i < 3; ++ i) diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 2e6627039c..344e88cb99 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -362,7 +362,14 @@ public: // Get facets at a given state. Don't triangulate T-joints. indexed_triangle_set get_facets(EnforcerBlockerType state) const; // Get facets at a given state. Triangulate T-joints. - indexed_triangle_set get_facets_strict(EnforcerBlockerType state) const; + // Sub-triangles in `state`, with the *whole* mesh's referenced vertex array (only .indices is + // filtered by state, so two calls with different states share one indexing). + // + // `out_source`, when given, is filled parallel to the returned .indices with the index of the + // original mesh triangle each sub-triangle came from. That is what lets a caller carry partial + // paint - the pieces of a triangle a brush stroke only partly covered - across a refinement of + // the same surface, instead of having to round each source triangle to wholly painted or not. + indexed_triangle_set get_facets_strict(EnforcerBlockerType state, std::vector *out_source = nullptr) const; // Get edges around the selected area by seed fill. std::vector get_seed_fill_contour() const; diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index bf0047644c..b1557244b6 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -214,6 +214,8 @@ set(SLIC3R_GUI_SOURCES GUI/Gizmos/GLGizmosManager.hpp GUI/Gizmos/GLGizmoSVG.cpp GUI/Gizmos/GLGizmoSVG.hpp + GUI/Gizmos/GLGizmoTextureDisplacement.cpp + GUI/Gizmos/GLGizmoTextureDisplacement.hpp GUI/Gizmos/GLGizmoUtils.cpp GUI/Gizmos/GLGizmoUtils.hpp #GUI/Gizmos/GLGizmoText.cpp @@ -342,6 +344,14 @@ set(SLIC3R_GUI_SOURCES GUI/Jobs/SLAImportDialog.hpp GUI/Jobs/SLAImportJob.cpp GUI/Jobs/SLAImportJob.hpp + GUI/Jobs/TextureDisplacementBakeJob.cpp + GUI/Jobs/TextureDisplacementBakeJob.hpp + GUI/Jobs/TextureDisplacementPrepareJob.cpp + GUI/Jobs/TextureDisplacementPrepareJob.hpp + GUI/Jobs/TextureDisplacementDebugJob.cpp + GUI/Jobs/TextureDisplacementDebugJob.hpp + GUI/Jobs/TextureDisplacementPreviewJob.cpp + GUI/Jobs/TextureDisplacementPreviewJob.hpp GUI/Jobs/ThreadSafeQueue.hpp GUI/Jobs/UpgradeNetworkJob.cpp GUI/Jobs/UpgradeNetworkJob.hpp @@ -539,6 +549,10 @@ set(SLIC3R_GUI_SOURCES GUI/TaskManager.hpp GUI/TextLines.cpp GUI/TextLines.hpp + GUI/TextureLibrary.cpp + GUI/TextureLibrary.hpp + GUI/TextureProjectorFrame.cpp + GUI/TextureProjectorFrame.hpp GUI/TickCode.cpp GUI/TickCode.hpp GUI/TroubleshootDialog.cpp @@ -555,6 +569,8 @@ set(SLIC3R_GUI_SOURCES GUI/UserManager.hpp GUI/UserNotification.cpp GUI/UserNotification.hpp + GUI/UVEditorCanvas.cpp + GUI/UVEditorCanvas.hpp GUI/WebDownPluginDlg.cpp GUI/WebDownPluginDlg.hpp GUI/WebGuideDialog.cpp diff --git a/src/slic3r/GUI/AMSMaterialsSetting.cpp b/src/slic3r/GUI/AMSMaterialsSetting.cpp index f0fdf950a0..edd1827f16 100644 --- a/src/slic3r/GUI/AMSMaterialsSetting.cpp +++ b/src/slic3r/GUI/AMSMaterialsSetting.cpp @@ -1000,11 +1000,12 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi // update if nozzle_temperature_range is found + const int variant_index = get_filament_variant_index(*filament_it, nozzle_diameter_str); ConfigOption *opt_min = filament_it->config.option("nozzle_temperature_range_low"); if (opt_min) { ConfigOptionInts *opt_min_ints = dynamic_cast(opt_min); if (opt_min_ints) { - wxString text_nozzle_temp_min = wxString::Format("%d", opt_min_ints->get_at(0)); + wxString text_nozzle_temp_min = wxString::Format("%d", opt_min_ints->get_at(variant_index)); m_input_nozzle_min->GetTextCtrl()->SetValue(text_nozzle_temp_min); } } @@ -1012,7 +1013,7 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi if (opt_max) { ConfigOptionInts *opt_max_ints = dynamic_cast(opt_max); if (opt_max_ints) { - wxString text_nozzle_temp_max = wxString::Format("%d", opt_max_ints->get_at(0)); + wxString text_nozzle_temp_max = wxString::Format("%d", opt_max_ints->get_at(variant_index)); m_input_nozzle_max->GetTextCtrl()->SetValue(text_nozzle_temp_max); } } @@ -1218,6 +1219,21 @@ void AMSMaterialsSetting::on_select_cali_result(wxCommandEvent &evt) } } +int AMSMaterialsSetting::get_filament_variant_index(const Preset &filament, const std::string &nozzle_diameter_str) +{ + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + if (!obj || !preset_bundle) + return 0; + const std::set printer_names = + preset_bundle->get_printer_names_by_printer_type_and_nozzle(DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str); + const Preset *printer = printer_names.empty() ? nullptr : preset_bundle->printers.find_preset(*printer_names.begin()); + if (!printer) + return 0; + const int extruder_id = obj->get_extruder_id_by_ams_id(std::to_string(ams_id)); + return PresetBundle::get_filament_variant_index(filament.config, printer->config, obj->get_preset_extruder_index(extruder_id), + DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(extruder_id))); +} + void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt) { // Get the flag whether to open the filament setting dialog from the device page @@ -1249,11 +1265,12 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt) if (it->filament_id != filament_id) continue; // ) if nozzle_temperature_range is found + const int variant_index = get_filament_variant_index(*it, nozzle_diameter_str); ConfigOption* opt_min = it->config.option("nozzle_temperature_range_low"); if (opt_min) { ConfigOptionInts* opt_min_ints = dynamic_cast(opt_min); if (opt_min_ints) { - wxString text_nozzle_temp_min = wxString::Format("%d", opt_min_ints->get_at(0)); + wxString text_nozzle_temp_min = wxString::Format("%d", opt_min_ints->get_at(variant_index)); m_input_nozzle_min->GetTextCtrl()->SetValue(text_nozzle_temp_min); } } @@ -1261,7 +1278,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt) if (opt_max) { ConfigOptionInts* opt_max_ints = dynamic_cast(opt_max); if (opt_max_ints) { - wxString text_nozzle_temp_max = wxString::Format("%d", opt_max_ints->get_at(0)); + wxString text_nozzle_temp_max = wxString::Format("%d", opt_max_ints->get_at(variant_index)); m_input_nozzle_max->GetTextCtrl()->SetValue(text_nozzle_temp_max); } } diff --git a/src/slic3r/GUI/AMSMaterialsSetting.hpp b/src/slic3r/GUI/AMSMaterialsSetting.hpp index 2cbd58e9a9..4cfdde791d 100644 --- a/src/slic3r/GUI/AMSMaterialsSetting.hpp +++ b/src/slic3r/GUI/AMSMaterialsSetting.hpp @@ -150,6 +150,8 @@ protected: void update_widgets(); void update_filament_editing(bool is_printing); + // Orca: the variant index of the filament's per-variant options on the nozzle this tray feeds + int get_filament_variant_index(const Preset &filament, const std::string &nozzle_diameter_str); protected: StateColor m_btn_bg_green; diff --git a/src/slic3r/GUI/CAD/DesignOffer.hpp b/src/slic3r/GUI/CAD/DesignOffer.hpp index ad1998d5c5..fd15f537d3 100644 --- a/src/slic3r/GUI/CAD/DesignOffer.hpp +++ b/src/slic3r/GUI/CAD/DesignOffer.hpp @@ -1,5 +1,5 @@ // GENERATED FILE — DO NOT EDIT. -// Source: docs/ux/tool_atlas.json Generator: docs/ux/mockups/gen_offer_table.py +// Source: scripts/CAD/tool_atlas.json Generator: scripts/CAD/gen_offer_table.py // // The object-driven tool offer (charter 4.1): every verb has ONE row index, that index // is the same in every selection it appears in, and verbs that do not apply are shown diff --git a/src/slic3r/GUI/CAD/DesignPanel.cpp b/src/slic3r/GUI/CAD/DesignPanel.cpp index d247ddfd26..5f64025f2b 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.cpp +++ b/src/slic3r/GUI/CAD/DesignPanel.cpp @@ -1,7 +1,7 @@ #include "slic3r/GUI/CAD/DesignPanel.hpp" #include "slic3r/GUI/CAD/DesignCanvas.hpp" #include "slic3r/GUI/CAD/DesignSketchTool.hpp" -#include "slic3r/GUI/CAD/DesignOffer.hpp" // generated offer table — see docs/ux/tool_atlas.json +#include "slic3r/GUI/CAD/DesignOffer.hpp" // generated offer table — see scripts/CAD/tool_atlas.json #include "libslic3r/CAD/GeometryEngine.hpp" // face_by_index for face-extrude gizmo anchor #include "libslic3r/TriangleMesh.hpp" // mesh import: STL/OBJ -> indexed_triangle_set #include "libslic3r/Format/OBJ.hpp" @@ -6067,7 +6067,7 @@ bool DesignPanel::sketch_plane_target(wxString& what) const // --------------------------------------------------------------------------------------------- // The object-driven offer (charter §4.1). Right-click the geometry and get a vertical list in the // ratified row order, with the verbs that do not apply DISABLED IN PLACE carrying their reason. -// The rows come from DesignOffer.hpp, generated from docs/ux/tool_atlas.json — the same file the +// The rows come from DesignOffer.hpp, generated from scripts/CAD/tool_atlas.json — the same file the // mockups are drawn from, so a drawing and the product cannot drift apart. // --------------------------------------------------------------------------------------------- diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.cpp b/src/slic3r/GUI/CAD/DesignSketchTool.cpp index 716df658ed..c195d6a877 100644 --- a/src/slic3r/GUI/CAD/DesignSketchTool.cpp +++ b/src/slic3r/GUI/CAD/DesignSketchTool.cpp @@ -4034,7 +4034,7 @@ void DesignSketchTool::render_mate_connectors() // the information. The same bear rendered flat vs in relief gives 164 vs 210 lit pixels at 16 // degrees and 66 vs 120 at 6. So the glyph is a small shaded solid, not an outline. // -// The geometry is EMITTED from the real part by doc/design/mate-connectors/emit_glyph_table.py, +// The geometry is EMITTED from the real part by scripts/CAD/mate-glyph/emit_glyph_table.py, // not hand-drawn, so the glyph and the printed connector cannot drift apart. Two vertices stand // above the 3 mm plate in the actual B-rep, which is why the snout here is a tent with one crest // edge and four flanks drafted at 20 degrees rather than anything more elaborate. @@ -4042,7 +4042,7 @@ void DesignSketchTool::render_mate_connectors() // The cheek dot is the handedness mark. Without it the glyph differs from its own mirror by only // 5-9 % of its lit pixels, which is not enough to read; the dot roughly doubles that and, unlike // making the eyes uneven, identifies the side from that cheek alone instead of by comparison. -// Emitted by doc/design/mate-connectors/emit_glyph_table.py from bear.step — do not hand-edit. +// Emitted by scripts/CAD/mate-glyph/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}, diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index cc687ff84a..fa8e2e5f16 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -78,12 +78,12 @@ void ConfigManipulation::set_option_label(const std::string& opt_key, const wxSt cb_set_option_label(opt_key, label, opt_index); } -void ConfigManipulation::check_nozzle_recommended_temperature_range(DynamicPrintConfig *config) { +void ConfigManipulation::check_nozzle_recommended_temperature_range(DynamicPrintConfig *config, unsigned int variant_index) { if (is_msg_dlg_already_exist) return; int temperature_range_low, temperature_range_high; - if (!get_temperature_range(config, temperature_range_low, temperature_range_high)) return; + if (!get_temperature_range(config, temperature_range_low, temperature_range_high, variant_index)) return; // Get the selected filament type std::string filament_type = ""; @@ -123,16 +123,16 @@ void ConfigManipulation::check_nozzle_recommended_temperature_range(DynamicPrint } } -void ConfigManipulation::check_nozzle_temperature_range(DynamicPrintConfig *config) +void ConfigManipulation::check_nozzle_temperature_range(DynamicPrintConfig *config, unsigned int variant_index) { if (is_msg_dlg_already_exist) return; int temperature_range_low, temperature_range_high; - if (!get_temperature_range(config, temperature_range_low, temperature_range_high)) return; + if (!get_temperature_range(config, temperature_range_low, temperature_range_high, variant_index)) return; if (config->has("nozzle_temperature")) { - if (config->opt_int("nozzle_temperature", 0) < temperature_range_low || config->opt_int("nozzle_temperature", 0) > temperature_range_high) { + if (config->opt_int("nozzle_temperature", variant_index) < temperature_range_low || config->opt_int("nozzle_temperature", variant_index) > temperature_range_high) { wxString msg_text = _(L("The nozzle may become clogged when the temperature is out of the recommended range.\nPlease make sure whether to use this temperature to print.\n\n")); msg_text += wxString::Format(_L("The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius."), temperature_range_low, temperature_range_high); MessageDialog dialog(m_msg_dlg_parent, msg_text, "", wxICON_WARNING | wxOK); @@ -143,17 +143,17 @@ void ConfigManipulation::check_nozzle_temperature_range(DynamicPrintConfig *conf } } -void ConfigManipulation::check_nozzle_temperature_initial_layer_range(DynamicPrintConfig* config) +void ConfigManipulation::check_nozzle_temperature_initial_layer_range(DynamicPrintConfig* config, unsigned int variant_index) { if (is_msg_dlg_already_exist) return; int temperature_range_low, temperature_range_high; - if (!get_temperature_range(config, temperature_range_low, temperature_range_high)) return; + if (!get_temperature_range(config, temperature_range_low, temperature_range_high, variant_index)) return; if (config->has("nozzle_temperature_initial_layer")) { - if (config->opt_int("nozzle_temperature_initial_layer", 0) < temperature_range_low || - config->opt_int("nozzle_temperature_initial_layer", 0) > temperature_range_high) + if (config->opt_int("nozzle_temperature_initial_layer", variant_index) < temperature_range_low || + config->opt_int("nozzle_temperature_initial_layer", variant_index) > temperature_range_high) { wxString msg_text = _(L("The nozzle may become clogged when the temperature is out of the recommended range.\nPlease make sure whether to use this temperature to print.\n\n")); msg_text += wxString::Format(_L("The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius."), temperature_range_low, temperature_range_high); @@ -1295,15 +1295,15 @@ int ConfigManipulation::show_spiral_mode_settings_dialog(bool is_object_config) return answer; } -bool ConfigManipulation::get_temperature_range(DynamicPrintConfig *config, int &range_low, int &range_high) +bool ConfigManipulation::get_temperature_range(DynamicPrintConfig *config, int &range_low, int &range_high, unsigned int variant_index) { bool range_low_exist = false, range_high_exist = false; if (config->has("nozzle_temperature_range_low")) { - range_low = config->opt_int("nozzle_temperature_range_low", (unsigned int) 0); + range_low = config->opt_int("nozzle_temperature_range_low", variant_index); range_low_exist = true; } if (config->has("nozzle_temperature_range_high")) { - range_high = config->opt_int("nozzle_temperature_range_high", (unsigned int) 0); + range_high = config->opt_int("nozzle_temperature_range_high", variant_index); range_high_exist = true; } return range_low_exist && range_high_exist; diff --git a/src/slic3r/GUI/ConfigManipulation.hpp b/src/slic3r/GUI/ConfigManipulation.hpp index ac53ffb4bb..a7a1a7ef77 100644 --- a/src/slic3r/GUI/ConfigManipulation.hpp +++ b/src/slic3r/GUI/ConfigManipulation.hpp @@ -79,9 +79,9 @@ public: void apply_null_fff_config(DynamicPrintConfig *config, std::vector const &keys, std::map const & configs); //BBS: FFF filament nozzle temperature range - void check_nozzle_recommended_temperature_range(DynamicPrintConfig *config); - void check_nozzle_temperature_range(DynamicPrintConfig* config); - void check_nozzle_temperature_initial_layer_range(DynamicPrintConfig* config); + void check_nozzle_recommended_temperature_range(DynamicPrintConfig *config, unsigned int variant_index); + void check_nozzle_temperature_range(DynamicPrintConfig* config, unsigned int variant_index); + void check_nozzle_temperature_initial_layer_range(DynamicPrintConfig* config, unsigned int variant_index); void check_adaptive_pressure_advance_model(DynamicPrintConfig* config); void check_filament_max_volumetric_speed(DynamicPrintConfig *config); void check_chamber_temperature(DynamicPrintConfig* config); @@ -104,7 +104,7 @@ public: int show_spiral_mode_settings_dialog(bool is_object_config = false); private: - bool get_temperature_range(DynamicPrintConfig *config, int &range_low, int &range_high); + bool get_temperature_range(DynamicPrintConfig *config, int &range_low, int &range_high, unsigned int variant_index); }; } // GUI diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index 23aa9c4224..45c32b46c5 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -5701,7 +5701,7 @@ void MachineObject::update_filament_list() PresetBundle *preset_bundle = Slic3r::GUI::wxGetApp().preset_bundle; // custom filament - typedef std::map> map_pair; + typedef std::map, std::vector>> map_pair; std::map map_list; for (auto &pair : m_nozzle_filament_data) { map_list[pair.second.printer_preset_name] = map_pair{}; @@ -5713,19 +5713,13 @@ void MachineObject::update_filament_list() for (const std::string &printer_str : printer_strs->values) { if (map_list.find(printer_str) != map_list.end()) { auto & filament_list = map_list[printer_str]; - ConfigOption *opt_min = const_cast(preset).config.option("nozzle_temperature_range_low"); - int min_temp = -1; - if (opt_min) { - ConfigOptionInts *opt_min_ints = dynamic_cast(opt_min); - min_temp = opt_min_ints->get_at(0); - } - ConfigOption *opt_max = const_cast(preset).config.option("nozzle_temperature_range_high"); - int max_temp = -1; - if (opt_max) { - ConfigOptionInts *opt_max_ints = dynamic_cast(opt_max); - max_temp = opt_max_ints->get_at(0); - } - filament_list[preset.filament_id] = std::make_pair(min_temp, max_temp); + // Every variant's range, so a change to any of them rechecks the trays + std::vector min_temps{-1}, max_temps{-1}; + if (auto *opt_min = preset.config.option("nozzle_temperature_range_low")) + min_temps = opt_min->values; + if (auto *opt_max = preset.config.option("nozzle_temperature_range_high")) + max_temps = opt_max->values; + filament_list[preset.filament_id] = std::make_pair(min_temps, max_temps); break; } } @@ -5861,10 +5855,13 @@ void MachineObject::check_ams_filament_valid() need_checked_filament_id[nozzle_diameter_str].insert(curr_tray->setting_id); try { std::string preset_setting_id; + const int extruder_id = ams->GetExtruderId(); bool is_equation = preset_bundle->check_filament_temp_equation_by_printer_type_and_nozzle_for_mas_tray(printer_model, nozzle_diameter_str, curr_tray->setting_id, curr_tray->tag_uid, curr_tray->nozzle_temp_min, - curr_tray->nozzle_temp_max, preset_setting_id); + curr_tray->nozzle_temp_max, preset_setting_id, + get_preset_extruder_index(extruder_id), + DevNozzle::ToNozzleVolumeType(m_extder_system->GetNozzleFlowType(extruder_id))); if (!is_equation) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << " ams filament is not match min max temp and reset, ams_id: " << ams_id << " tray_id" << slot_id << "filament_id: " << curr_tray->setting_id; @@ -5929,7 +5926,9 @@ void MachineObject::check_ams_filament_valid() this->printer_type), nozzle_diameter_str, vt_tray.setting_id, vt_tray.tag_uid, vt_tray.nozzle_temp_min, - vt_tray.nozzle_temp_max, preset_setting_id); + vt_tray.nozzle_temp_max, preset_setting_id, + get_preset_extruder_index(index), + DevNozzle::ToNozzleVolumeType(m_extder_system->GetNozzleFlowType(index))); if (!is_equation) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << " vt_tray filament is not match min max temp and reset, filament_id: " << vt_tray.setting_id; diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index b69e2f8f5a..94f867b93d 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -341,6 +341,8 @@ public: bool is_main_extruder_on_left() const { return false; } // only means the extruder is on the left hand when extruder id is 0 bool is_multi_extruders() const; int get_extruder_id_by_ams_id(const std::string& ams_id); + // Orca: the printer preset's extruder index of device extruder extder_id + int get_preset_extruder_index(int extder_id) const { return is_multi_extruders() ? (is_main_extruder_on_left() ? extder_id : 1 - extder_id) : 0; } /* E3D has extra nozzle flow type info */ bool has_extra_flow_type{false}; @@ -957,7 +959,8 @@ public: { std::set checked_filament; std::string printer_preset_name; - std::map> filament_list; // filament_id, pair + // filament_id, pair, one per filament variant + std::map, std::vector>> filament_list; }; std::map m_nozzle_filament_data; void update_filament_list(); diff --git a/src/slic3r/GUI/GLShadersManager.cpp b/src/slic3r/GUI/GLShadersManager.cpp index b35d598829..be0c57c658 100644 --- a/src/slic3r/GUI/GLShadersManager.cpp +++ b/src/slic3r/GUI/GLShadersManager.cpp @@ -106,6 +106,11 @@ std::pair GLShadersManager::init() valid &= append_shader("mm_gouraud", { prefix + "mm_gouraud.vs", prefix + "mm_gouraud.fs" }, { "FLIP_TRIANGLE_NORMALS"sv }); else valid &= append_shader("mm_gouraud", { prefix + "mm_gouraud.vs", prefix + "mm_gouraud.fs" }); + // Fast shaded preview for the texture displacement gizmo (see libslic3r/TextureDisplacement.hpp). + valid &= append_shader("texture_displacement_shaded", { prefix + "texture_displacement_shaded.vs", prefix + "texture_displacement_shaded.fs" }); + // UV-check overlay for the same gizmo: a procedural checker or a distortion heatmap over the + // painted patch, to sanity-check the unwrap. + valid &= append_shader("texture_displacement_uvcheck", { prefix + "texture_displacement_uvcheck.vs", prefix + "texture_displacement_uvcheck.fs" }); return { valid, error }; } diff --git a/src/slic3r/GUI/GLTexture.cpp b/src/slic3r/GUI/GLTexture.cpp index 3df368de3f..1e8f253ea9 100644 --- a/src/slic3r/GUI/GLTexture.cpp +++ b/src/slic3r/GUI/GLTexture.cpp @@ -173,7 +173,8 @@ bool GLTexture::load_from_svg_file(const std::string& filename, bool use_mipmaps return false; } -bool GLTexture::load_from_raw_data(std::vector data, unsigned int w, unsigned int h, bool apply_anisotropy) +bool GLTexture::load_from_raw_data(std::vector data, unsigned int w, unsigned int h, bool apply_anisotropy, + bool use_mipmaps) { m_width = w; m_height = h; @@ -197,18 +198,51 @@ bool GLTexture::load_from_raw_data(std::vector data, unsigned int glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)m_width, (GLsizei)m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)data.data())); - bool use_mipmaps = true; if (use_mipmaps) { - // we manually generate mipmaps because glGenerateMipmap() function is not reliable on all graphics cards - int lod_w = m_width; - int lod_h = m_height; + // We generate the mipmap chain ourselves rather than calling glGenerateMipmap(), which this + // codebase has historically considered unreliable on some graphics cards. + // + // Each level is a 2x2 box filter of the level above it. Note this used to re-upload the + // *level-0* buffer at every level instead, which does not downscale anything - it just + // reinterprets the image's first lod_w * lod_h texels as the whole smaller level, i.e. every + // level below 0 held a crop of the top-left corner. It went unnoticed for as long as every + // caller drew these textures at roughly their native size (where only level 0 is ever + // sampled); it shows up the moment one is drawn small enough to select a lower level, as a + // texture that visibly turns into something else as it shrinks. + std::vector scratch; + const std::vector *src = &data; + int src_w = m_width; + int src_h = m_height; GLint level = 0; - while (lod_w > 1 || lod_h > 1) { + while (src_w > 1 || src_h > 1) { ++level; - lod_w = std::max(lod_w / 2, 1); - lod_h = std::max(lod_h / 2, 1); - n_pixels = lod_w * lod_h; - glsafe(::glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA, (GLsizei)lod_w, (GLsizei)lod_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)data.data())); + const int lod_w = std::max(src_w / 2, 1); + const int lod_h = std::max(src_h / 2, 1); + + std::vector lod(size_t(lod_w) * size_t(lod_h) * 4); + for (int y = 0; y < lod_h; ++y) { + // min() rather than a plain 2*y+1: an odd source extent leaves the last output texel + // with only one source row/column to average, not two. + const int y0 = std::min(2 * y, src_h - 1); + const int y1 = std::min(2 * y + 1, src_h - 1); + for (int x = 0; x < lod_w; ++x) { + const int x0 = std::min(2 * x, src_w - 1); + const int x1 = std::min(2 * x + 1, src_w - 1); + for (int c = 0; c < 4; ++c) { + const unsigned int sum = (*src)[(size_t(y0) * size_t(src_w) + size_t(x0)) * 4 + size_t(c)] + + (*src)[(size_t(y0) * size_t(src_w) + size_t(x1)) * 4 + size_t(c)] + + (*src)[(size_t(y1) * size_t(src_w) + size_t(x0)) * 4 + size_t(c)] + + (*src)[(size_t(y1) * size_t(src_w) + size_t(x1)) * 4 + size_t(c)]; + lod[(size_t(y) * size_t(lod_w) + size_t(x)) * 4 + size_t(c)] = (unsigned char)(sum / 4); + } + } + } + glsafe(::glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA, (GLsizei)lod_w, (GLsizei)lod_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)lod.data())); + + scratch = std::move(lod); + src = &scratch; + src_w = lod_w; + src_h = lod_h; } glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, level)); diff --git a/src/slic3r/GUI/GLTexture.hpp b/src/slic3r/GUI/GLTexture.hpp index 5e3d11984d..cf6f0f1d71 100644 --- a/src/slic3r/GUI/GLTexture.hpp +++ b/src/slic3r/GUI/GLTexture.hpp @@ -101,7 +101,10 @@ namespace GUI { bool load_from_file(const std::string& filename, bool use_mipmaps, ECompressionType compression_type, bool apply_anisotropy); bool load_from_svg_file(const std::string& filename, bool use_mipmaps, bool compress, bool apply_anisotropy, unsigned int max_size_px); //BBS load GLTexture from raw pixel data - bool load_from_raw_data(std::vector data, unsigned int w, unsigned int h, bool apply_anisotropy = false); + // `data` is RGBA, w * h * 4 bytes. With use_mipmaps, a real box-filtered mipmap chain is + // built, so the texture may safely be drawn smaller than its pixel size. + bool load_from_raw_data(std::vector data, unsigned int w, unsigned int h, bool apply_anisotropy = false, + bool use_mipmaps = true); // meanings of states: (std::pair) // first field (int): // 0 -> no changes diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 27135a44d7..da0be9228d 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -8882,6 +8882,11 @@ void GUI_App::load_current_presets(bool active_preset_combox/*= false*/, bool ch if (active_preset_combox) tab->reactive_preset_combo_box(); } + // Preset loading can resize the filament list without an extruder-count change event. + // Refresh the controls even when the list already matches the printer's nozzle count. + if (printer_technology == ptFFF) + this->plater()->on_filament_count_change(preset_bundle->filament_presets.size()); + // BBS: model config for (Tab *tab : model_tabs_list) if (tab->supports_printer_technology(printer_technology)) { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp index 9ba6065458..19856603fa 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp @@ -27,7 +27,8 @@ enum class PainterGizmoType { FDM_SUPPORTS, SEAM, MM_SEGMENTATION, - FUZZY_SKIN + FUZZY_SKIN, + TEXTURE_DISPLACEMENT }; class TriangleSelectorGUI : public TriangleSelector { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp new file mode 100644 index 0000000000..5c322a1f81 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp @@ -0,0 +1,6942 @@ +#include "GLGizmoTextureDisplacement.hpp" + +#include +#include + +#include "libslic3r/AABBTreeIndirect.hpp" +#include "libslic3r/Color.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/MeshBoolean.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/Utils.hpp" +#include "libslic3r/format.hpp" + +#include "slic3r/GUI/Camera.hpp" +#include "slic3r/GUI/CameraUtils.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GLToolbar.hpp" // GLToolbar::Default_Icons_Size, to match the toolbar's icon size +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/GUI/GuiColor.hpp" +#include "slic3r/GUI/ImGuiWrapper.hpp" +#include "slic3r/GUI/MainFrame.hpp" // wxGetApp().mainframe, as the projector window's parent +#include "slic3r/GUI/MsgDialog.hpp" +#include "slic3r/GUI/OpenGLManager.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/TextureLibrary.hpp" +#include "slic3r/GUI/TextureProjectorFrame.hpp" +#include "slic3r/GUI/UVEditorCanvas.hpp" +#include "slic3r/GUI/Jobs/TextureDisplacementBakeJob.hpp" +#include "slic3r/GUI/Jobs/TextureDisplacementPrepareJob.hpp" +#include "slic3r/GUI/Jobs/TextureDisplacementDebugJob.hpp" +#include "slic3r/GUI/Jobs/TextureDisplacementPreviewJob.hpp" +#include "slic3r/Utils/UndoRedo.hpp" +#include "GLGizmoUtils.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Slic3r::GUI { + +namespace { +// ImGuiWrapper::slider_float()'s trailing `power` parameter is forwarded straight into +// ImGui::SliderFloat()'s ImGuiSliderFlags argument - this ImGui (1.83) replaced the old float +// "power" curve API with a flags word but kept the parameter in the same position, and +// ImGuiWrapper never caught up with the rename. Passing the Logarithmic flag through it is +// therefore how a log-scaled slider gets requested here. +// +// Depth and tile size both want it: they span two to three orders of magnitude, and the values +// that need the most precision (a few hundredths of a millimetre of relief, a very fine tile) are +// all bunched into the very bottom of a linear range, where a single pixel of slider travel is a +// bigger jump than the whole useful region. +constexpr float ImGuiLogSlider = float(ImGuiSliderFlags_Logarithmic); + +// Uploads decoded 8-bit grayscale height data as an RGBA GPU texture for display in the panel. +// Shared by the per-layer thumbnail and the picker's library entries, which differ only in where +// their pixels came from. +// +// The source is downscaled to THUMBNAIL_MAX_PX first, with a box filter, and uploaded *without* +// mipmaps. Both halves of that matter, and together they are the fix for thumbnails rendering as +// garbage: +// - GLTexture's mipmap levels are not real downscales (see load_from_raw_data()'s header) - every +// level re-uploads the full-resolution buffer reinterpreted at a smaller size. A 512px texture +// drawn into a ~48px row is minified ~10x, which is exactly the regime where OpenGL picks those +// broken levels, so the thumbnail showed a scrambled crop of the image rather than the image. +// Turning mipmaps off makes it sample level 0, which is the real picture. +// - But level 0 at 512px sampled down to 48px with plain GL_LINEAR (a 2x2 tap) would alias badly +// on exactly the high-frequency patterns these textures are (knurl, hexagons, weave). Box- +// filtering down to roughly twice the displayed size first is what actually removes the +// frequencies that would alias, and it cuts the VRAM these hold by ~16x as a bonus. +constexpr int THUMBNAIL_MAX_PX = 128; + +// Everything above is about drawing a ~48 px panel row, and none of it applies to the height texture +// the fast-preview *shader* samples: that one is magnified across the model, not minified into a +// row, and every texel it loses is relief the preview cannot show. It gets its own upload at (up to) +// this size, so the shaded preview reads the same height field the bake does instead of a 128 px box +// blur of it - which is what made Fast look flatter and softer than the result it was previewing. +constexpr int HEIGHT_TEX_MAX_PX = 2048; + +// How many rings of vertex-adjacent triangles either side of the paint's edge join the border refine +// band (see collect_paint_region()). Two is enough to grade the size change without the band's own +// cost growing to matter: it is a ring around a perimeter, not an area. +constexpr int BORDER_BAND_RINGS = 2; + +// Is `p` inside triangle `t`, given that it already lies in the triangle's plane? Barycentric via the +// three sub-triangle cross products, compared against the whole triangle's normal. Used to carry a +// partly painted source triangle's coverage onto the children of a subdivision, which are coplanar +// with it by construction (subdivision only adds edge midpoints). +bool point_in_triangle_coplanar(const Vec3f &p, const std::array &t) +{ + const Vec3f n = (t[1] - t[0]).cross(t[2] - t[0]); + const float n2 = n.squaredNorm(); + if (n2 < 1e-20f) + return false; // degenerate: it covers no area, so nothing is inside it + // A small negative tolerance, scaled by the triangle, keeps a centroid sitting exactly on a shared + // edge from falling through the gap between two neighbouring pieces. + const float eps = -1e-4f * n2; + return (t[1] - t[0]).cross(p - t[0]).dot(n) >= eps && + (t[2] - t[1]).cross(p - t[1]).dot(n) >= eps && + (t[0] - t[2]).cross(p - t[2]).dot(n) >= eps; +} + +// Carries one texture-displacement paint mask from `src_mesh` onto `dst_mesh` - a different +// tessellation of the same surface (an isotropic remesh, in practice). +// +// TriangleSelector::remap_painting(), which ModelVolume::restore_painting() uses for the four +// standard paint channels, is not usable here. It runs one select_patch() flood fill per +// (painted sub-triangle x overlapping target facet) pair, and select_patch() splits the *target's* +// triangles down to a 0.02-0.05 mm edge limit along every cursor boundary, behind an +// O(target triangles) visited-set allocation per call. A coarse import painted with a fine brush +// carries tens of thousands of painted sub-triangles, so that is tens of millions of sub-triangles +// created on the target - which is what made Remesh, and Standard mode's Bake (which remeshes for +// you), appear to hang rather than finish. +// +// Each target triangle here asks one question instead: is the point on the source surface closest +// to my centroid inside a painted piece? One AABB-tree query per target triangle, whole facets +// only, no splitting - O(target log source). `src_tree` is built over `src_mesh.its` by the caller +// and shared across the layers; `dst_to_src` brings the target's vertices into the source's frame. +TriangleSelector::TriangleSplittingData remap_texture_paint_spatial( + const TriangleMesh &src_mesh, const TriangleSelector::TriangleSplittingData &src_data, + const AABBTreeIndirect::Tree3f &src_tree, const TriangleMesh &dst_mesh, const Vec3f &dst_to_src) +{ + const indexed_triangle_set &src = src_mesh.its; + const indexed_triangle_set &dst = dst_mesh.its; + const size_t ntri = src.indices.size(); + if (src_data.bitstream.empty() || ntri == 0 || dst.indices.empty() || src_tree.empty()) + return {}; + + // The painted patch, split into per-source-triangle pieces - the same shape + // collect_paint_region() builds, and for the same reason: a source triangle the brush only + // partly covered has to answer "is this point painted" geometrically rather than be rounded to + // painted-or-not, which leaves a ragged fringe along any curved brush boundary. + TriangleSelector src_sel(src_mesh); + src_sel.deserialize(src_data, false); + std::vector piece_src; + const indexed_triangle_set patch = src_sel.get_facets_strict(EnforcerBlockerType::ENFORCER, &piece_src); + if (patch.indices.empty()) + return {}; + + const auto tri_area2 = [](const Vec3f &a, const Vec3f &b, const Vec3f &c) { + return (b - a).cross(c - a).norm(); + }; + std::vector src_area2(ntri, 0.f), covered2(ntri, 0.f); + for (size_t i = 0; i < ntri; ++i) + src_area2[i] = tri_area2(src.vertices[size_t(src.indices[i][0])], src.vertices[size_t(src.indices[i][1])], + src.vertices[size_t(src.indices[i][2])]); + for (size_t j = 0; j < patch.indices.size() && j < piece_src.size(); ++j) { + if (size_t(piece_src[j]) >= ntri) + continue; + const stl_triangle_vertex_indices &t = patch.indices[j]; + covered2[size_t(piece_src[j])] += tri_area2(patch.vertices[size_t(t[0])], patch.vertices[size_t(t[1])], + patch.vertices[size_t(t[2])]); + } + std::vector full(ntri, 0); + for (size_t i = 0; i < ntri; ++i) + full[i] = (src_area2[i] > 0.f && covered2[i] >= 0.999f * src_area2[i]) ? 1 : 0; + + // CSR pieces, kept for partly covered sources only - a full one answers every query "painted". + std::vector part_start(ntri + 1, 0); + for (size_t j = 0; j < patch.indices.size() && j < piece_src.size(); ++j) + if (size_t(piece_src[j]) < ntri && !full[size_t(piece_src[j])]) + ++part_start[size_t(piece_src[j]) + 1]; + for (size_t i = 0; i < ntri; ++i) + part_start[i + 1] += part_start[i]; + std::vector> part; + part.resize(size_t(part_start[ntri])); // not a constructor call: `vector p(size_t(x[n]));` parses + // as a function declaration, and every use of `part` below + // then fails with something that does not mention the cause. + { + std::vector fill(part_start.begin(), part_start.begin() + ntri); + for (size_t j = 0; j < patch.indices.size() && j < piece_src.size(); ++j) { + const size_t S = size_t(piece_src[j]); + if (S >= ntri || full[S]) + continue; + const stl_triangle_vertex_indices &t = patch.indices[j]; + part[size_t(fill[S]++)] = { patch.vertices[size_t(t[0])], patch.vertices[size_t(t[1])], + patch.vertices[size_t(t[2])] }; + } + } + + // Classify in parallel, then write the mask serially - TriangleSelector is not thread safe. + std::vector painted(dst.indices.size(), 0); + tbb::parallel_for(tbb::blocked_range(0, dst.indices.size()), + [&](const tbb::blocked_range &range) { + for (size_t i = range.begin(); i < range.end(); ++i) { + const stl_triangle_vertex_indices &t = dst.indices[i]; + const Vec3f centroid = (dst.vertices[size_t(t[0])] + dst.vertices[size_t(t[1])] + + dst.vertices[size_t(t[2])]) / 3.f + dst_to_src; + size_t hit = 0; + Vec3f hit_pos = Vec3f::Zero(); + if (AABBTreeIndirect::squared_distance_to_indexed_triangle_set(src.vertices, src.indices, src_tree, + centroid, hit, hit_pos) < 0.f || + hit >= ntri) + continue; + if (full[hit]) { + painted[i] = 1; + continue; + } + for (int k = part_start[hit]; k < part_start[hit + 1]; ++k) + if (point_in_triangle_coplanar(hit_pos, part[size_t(k)])) { + painted[i] = 1; + break; + } + } + }); + + TriangleSelector dst_sel(dst_mesh); + for (size_t i = 0; i < painted.size(); ++i) + if (painted[i]) + dst_sel.set_facet(int(i), EnforcerBlockerType::ENFORCER); + return dst_sel.serialize(); +} + +// Edge of the RGB lookup cube make_palette_quantizer() builds. 24 gives 13824 cells - far finer than +// the difference between any two printable colours - and costs one DeltaE00 per cell per palette +// entry to fill. +constexpr int PALETTE_LUT_EDGE = 24; + +// Ceiling on the printable palette, which bounds that fill cost (and the shader's uniform array). +constexpr int PALETTE_MAX_ENTRIES = 64; +// Ceiling on the filaments the palette's entries can refer to (the shaded preview shader's filament_rgb[]); +// mmu segmentation stops at Extruder16 anyway. +constexpr int PALETTE_MAX_FILAMENTS = 16; + +// sRGB (0..1) <-> CIELAB, D65. Exactly what the preview shader's srgb_to_lab() computes, so the CPU +// quantizer, the mixed-palette entries and the per-fragment preview all match in the same space. +// Not slic3r/Utils/ColorSpaceConvert: its RGB2Lab wants 0..1 but its Lab2RGB hands back linear +// values on a 0..100 scale, and the earlier code fed the former 0..255 and divided the latter by 255 - +// self-consistent enough for the bake's nearest-entry search to rank sensibly, but the preview, which +// compares real Lab against those values, picked the darkest filament everywhere. +static Vec3f srgb_to_lab(const Vec3f &c) +{ + const auto lin = [](float v) { return v > 0.04045f ? std::pow((v + 0.055f) / 1.055f, 2.4f) : v / 12.92f; }; + const float r = lin(c.x()), g = lin(c.y()), b = lin(c.z()); + const float x = (0.4124f * r + 0.3576f * g + 0.1805f * b) / 0.95047f; + const float y = (0.2126f * r + 0.7152f * g + 0.0722f * b); + const float z = (0.0193f * r + 0.1192f * g + 0.9505f * b) / 1.08883f; + const auto f = [](float t) { return t > 0.008856f ? std::cbrt(t) : 7.787f * t + 16.f / 116.f; }; + const float fx = f(x), fy = f(y), fz = f(z); + return Vec3f(116.f * fy - 16.f, 500.f * (fx - fy), 200.f * (fy - fz)); +} + +static Vec3f lab_to_srgb(const Vec3f &lab) +{ + const float fy = (lab.x() + 16.f) / 116.f, fx = lab.y() / 500.f + fy, fz = fy - lab.z() / 200.f; + const auto finv = [](float t) { return t > 0.206893f ? t * t * t : (t - 16.f / 116.f) / 7.787f; }; + const float x = finv(fx) * 0.95047f, y = finv(fy), z = finv(fz) * 1.08883f; + const float r = 3.2406f * x - 1.5372f * y - 0.4986f * z; + const float g = -0.9689f * x + 1.8758f * y + 0.0415f * z; + const float b = 0.0557f * x - 0.2040f * y + 1.0570f * z; + const auto gam = [](float v) { + v = std::clamp(v, 0.f, 1.f); + return v > 0.0031308f ? 1.055f * std::pow(v, 1.f / 2.4f) - 0.055f : 12.92f * v; + }; + return Vec3f(gam(r), gam(g), gam(b)); +} + +std::unique_ptr upload_height_thumbnail(const DecodedHeightTexture &decoded, int max_px = THUMBNAIL_MAX_PX) +{ + if (decoded.empty()) + return nullptr; + + // Preserve aspect; never upscale a texture that is already small. + const int scale = std::max(1, (std::max(decoded.width, decoded.height) + max_px - 1) / max_px); + const int w = std::max(1, decoded.width / scale); + const int h = std::max(1, decoded.height / scale); + + // A colour texture is shown in colour, so it can be told apart from its grey neighbours in the picker; its + // height is the luminance of those same colours, so nothing about the relief is hidden by that. + const bool color = decoded.has_color(); + std::vector rgba(size_t(w) * size_t(h) * 4); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + // Average the source block this destination pixel covers. + const int x0 = x * decoded.width / w, x1 = std::max(x0 + 1, (x + 1) * decoded.width / w); + const int y0 = y * decoded.height / h, y1 = std::max(y0 + 1, (y + 1) * decoded.height / h); + unsigned int sum[3] = { 0, 0, 0 }; + unsigned int n = 0; + for (int sy = y0; sy < y1 && sy < decoded.height; ++sy) + for (int sx = x0; sx < x1 && sx < decoded.width; ++sx, ++n) { + const size_t si = size_t(sy) * size_t(decoded.width) + size_t(sx); + for (int c = 0; c < 3; ++c) + sum[c] += color ? decoded.rgb[si * 3 + size_t(c)] : decoded.pixels[si]; + } + + const size_t di = (size_t(y) * size_t(w) + size_t(x)) * 4; + for (int c = 0; c < 3; ++c) + rgba[di + size_t(c)] = (n > 0) ? static_cast(sum[c] / n) : 0; + rgba[di + 3] = 255; + } + + auto texture = std::make_unique(); + if (!texture->load_from_raw_data(std::move(rgba), (unsigned int) w, (unsigned int) h, false, /* use_mipmaps */ false)) + return nullptr; + return texture; +} + +// The same upload, of the texture's *colour* rather than its height, for the fast preview to quantize +// per fragment. Null for a grayscale texture - there is nothing to show. +// +// Box-filtered down like the height is, and for a sharper reason: the fast preview quantizes every +// fragment independently, so any texel-scale noise left in the image becomes a scatter of single-pixel +// colour flips on screen. Filtering on the way to the GPU is where that is cheapest to remove. +std::unique_ptr upload_color_texture(const DecodedHeightTexture &decoded, int max_px) +{ + if (!decoded.has_color()) + return nullptr; + + const int scale = std::max(1, (std::max(decoded.width, decoded.height) + max_px - 1) / max_px); + const int w = std::max(1, decoded.width / scale); + const int h = std::max(1, decoded.height / scale); + + std::vector rgba(size_t(w) * size_t(h) * 4); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const int x0 = x * decoded.width / w, x1 = std::max(x0 + 1, (x + 1) * decoded.width / w); + const int y0 = y * decoded.height / h, y1 = std::max(y0 + 1, (y + 1) * decoded.height / h); + unsigned int sum[3] = { 0, 0, 0 }; + unsigned int n = 0; + for (int sy = y0; sy < y1 && sy < decoded.height; ++sy) + for (int sx = x0; sx < x1 && sx < decoded.width; ++sx, ++n) { + const size_t si = (size_t(sy) * size_t(decoded.width) + size_t(sx)) * 3; + for (int c = 0; c < 3; ++c) + sum[c] += decoded.rgb[si + size_t(c)]; + } + const size_t di = (size_t(y) * size_t(w) + size_t(x)) * 4; + for (int c = 0; c < 3; ++c) + rgba[di + size_t(c)] = (n > 0) ? static_cast(sum[size_t(c)] / n) : 0; + rgba[di + 3] = 255; + } + + auto texture = std::make_unique(); + if (!texture->load_from_raw_data(std::move(rgba), (unsigned int) w, (unsigned int) h, false, false)) + return nullptr; + return texture; +} + +// Intersects the camera ray through `mouse_pos` (screen coords) with the plane passing through +// `plane_point_local`/`plane_normal_local` (mesh-local coords, transformed to world by `trafo`). +// Returns false if the ray is parallel to the plane or the plane is behind the camera. +bool ray_plane_hit(const Camera &camera, const Vec2d &mouse_pos, const Transform3d &trafo, + const Vec3f &plane_point_local, const Vec3f &plane_normal_local, Vec3d &out_world_hit) +{ + Vec3d ray_origin, ray_dir; + CameraUtils::ray_from_screen_pos(camera, mouse_pos, ray_origin, ray_dir); + + const Vec3d plane_point_world = trafo * plane_point_local.cast(); + const Vec3d plane_normal_world = (trafo.matrix().block(0, 0, 3, 3).inverse().transpose() * plane_normal_local.cast()).normalized(); + + const double denom = ray_dir.dot(plane_normal_world); + if (std::abs(denom) < 1e-8) + return false; + + const double t = (plane_point_world - ray_origin).dot(plane_normal_world) / denom; + if (t < 0.0) + return false; + + out_world_hit = ray_origin + ray_dir * t; + return true; +} +} // namespace + +GLGizmoTextureDisplacement::GLGizmoTextureDisplacement(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) + : GLGizmoPainterBase(parent, icon_filename, sprite_id) +{ +} + +bool GLGizmoTextureDisplacement::on_init() +{ + m_desc["cursor_size"] = _L("Brush size"); + m_desc["circle"] = _L("Circle"); + m_desc["sphere"] = _L("Sphere"); + m_desc["remove_layer"] = _L("Remove"); + m_desc["bake"] = _L("Bake"); + return true; +} + +std::string GLGizmoTextureDisplacement::on_get_name() const +{ + return _u8L("Texture displacement"); +} + +void GLGizmoTextureDisplacement::on_shutdown() +{ + m_parent.toggle_model_objects_visibility(true); + m_preview_glmodel.reset(); + m_shaded_preview_glmodel.reset(); + m_paint_overlay_glmodel.reset(); + m_paint_overlay_dirty = false; + // Any preview still in flight is superseded: raising the shared counter makes it abort at its next + // progress poll, and its completion handler then finds nothing to do. + m_preview_generation->fetch_add(1); + m_preview_job_pending = false; + m_uvcheck_glmodel.reset(); + m_wireframe_overlay_glmodel.reset(); + m_wireframe_overlay_vcount = 0; + m_seam_glmodel.reset(); + m_seam_hover_glmodel.reset(); + m_seam_hover_edge = { -1, -1 }; + m_seam_hover_vertex = -1; + m_seam_anchor_glmodel.reset(); + m_seam_path_mode = false; + m_seam_path_anchor = -1; + m_subdivide_editing = false; + m_subdivide_preview_tris = -1; + m_subdivide_preview_glmodel.reset(); + m_shaded_active_chart = -1; + m_shaded_active_face.clear(); + m_shaded_island_delta = Eigen::Matrix::Identity(); + m_island_drag_active = false; + m_island_move_set.clear(); + m_adjust_texture_mode = false; + m_seam_edit_mode = false; + m_adjust_drag_handle = AdjustHandle::None; + m_adjust_anchor_valid = false; + // The pane is request-only: closing the gizmo drops the request, so reopening it later doesn't + // silently reopen the pane too. + m_show_uv_editor = false; + wxGetApp().plater()->show_uv_editor(false); + + // Destroyed, not just hidden: unlike the UV pane (owned by Plater), this frame is owned here, and + // it holds a callback capturing `this`. Leaving it alive past the gizmo would leave that callback + // pointing at a gizmo that is no longer driving anything. + if (m_projector_frame != nullptr) { + m_projector_frame->Destroy(); + m_projector_frame = nullptr; + } + m_projector_tex_source = nullptr; // a rebuilt frame starts with no texture in it + m_projector_tex_smoothing = -1.f; +} + +PainterGizmoType GLGizmoTextureDisplacement::get_painter_type() const +{ + return PainterGizmoType::TEXTURE_DISPLACEMENT; +} + +wxString GLGizmoTextureDisplacement::handle_snapshot_action_name(bool shift_down, GLGizmoPainterBase::Button button_down) const +{ + // Right button does the opposite of the toggle; Shift always erases. + const bool erasing = shift_down || (m_erase_mode != (button_down == GLGizmoPainterBase::Button::Right)); + return erasing ? _L("Erase texture displacement paint") : _L("Paint texture displacement"); +} + +void GLGizmoTextureDisplacement::render_painter_gizmo() +{ + const Selection &selection = m_parent.get_selection(); + + glsafe(::glEnable(GL_BLEND)); + glsafe(::glEnable(GL_DEPTH_TEST)); + + // Once anything is painted, m_preview_glmodel holds the true displaced result (same algorithm + // Bake uses). The untouched original topology (what render_triangles() draws) coincides + // exactly with it everywhere except the painted/displaced area, so both are drawn: the real + // preview geometry first, then the usual selection-highlight overlay with a small depth bias + // so it wins the depth test on the coincident (unpainted) surface - keeping the familiar + // enforcer/blocker highlight for precise brush editing there. Where the surface has actually + // been displaced, the raised preview geometry legitimately occludes the flat overlay - that + // visible relief is itself the "this is painted" indicator in that area. + // + // The shaded preview is different: it never actually moves geometry (it only shades), so + // its depth is identical to the overlay's *everywhere*, not just in the unpainted area - the + // depth-biased opaque overlay would win the depth test across the whole surface and hide the relief + // shading entirely. So render_triangles() is skipped for it. What is *not* skipped is + // render_paint_overlay(): leaving the shading as the only paint feedback meant a stroke that + // erased paint, or added it with no texture picked, changed nothing on screen until the whole + // preview rebuilt at stroke end - and in the true-displacement view the opaque overlay is hidden + // by the raised surface for the same reason. The translucent tint covers both cases. + // Coalesced shaded-preview rebuild from an in-progress UV island drag (see on_island_edited): done here, at + // most once per drawn frame, rather than synchronously in the UV canvas's mouse-move handler. + if (m_use_shaded_preview && m_shaded_preview_dirty) { + rebuild_shaded_preview_mesh(); + m_shaded_preview_dirty = false; + } + // Same coalescing for the paint tint, but on its own flag: a stroke marks this every mouse move + // (see on_mouse()) and it only costs the painted patch + if (m_paint_overlay_dirty) { + rebuild_paint_overlay(); + m_paint_overlay_dirty = false; + } + rebuild_other_paint_overlay(); // a no-op unless another layer's paint, the active layer or the preview changed + // is_initialized() alone is not enough: render_shaded_preview_mesh() also needs an active layer + // with a decoded texture and a compiled shader, and bails silently without them. Hiding the real + // volume for a shaded pass that then draws nothing is what made the model vanish - most obviously + // with zero layers, but equally with a layer that has no texture picked yet. + const bool use_shaded = m_use_shaded_preview && m_shaded_preview_glmodel.is_initialized() && shaded_preview_ready(); + const bool use_true_preview = !use_shaded && m_preview_glmodel.is_initialized(); + // In Checker/Distortion mode the UV-check overlay *is* the surface visualization the user is + // looking at, so the opaque paint-selection highlight must not be drawn on top of it - same + // reasoning as skipping it for the shaded preview (see bug #12). Without this the painted area + // covers the checker/heatmap and it can't be seen. + const bool show_paint_overlay = m_uv_check_mode == UVCheckMode::None; + + // Hide the real volume only when something is actually going to be drawn in its place; otherwise + // put it back. Getting this wrong leaves an invisible model, so it is decided once, here, rather + // than per branch below. + m_parent.toggle_model_objects_visibility(true); + if (use_shaded || use_true_preview) { + if (ModelVolume *mv = texture_volume()) + m_parent.toggle_model_objects_visibility(false, m_c->selection_info()->model_object(), + m_c->selection_info()->get_active_instance(), mv); + } + + if (use_shaded) { + render_shaded_preview_mesh(); + } else if (use_true_preview) { + render_preview_mesh(); + + if (show_paint_overlay) { + glsafe(::glEnable(GL_POLYGON_OFFSET_FILL)); + glsafe(::glPolygonOffset(-1.0f, -1.0f)); + render_triangles(selection); + glsafe(::glDisable(GL_POLYGON_OFFSET_FILL)); + } + } else if (show_paint_overlay) { + render_triangles(selection); + } + + // Every other layer's paint, in muted grey, so all layers stay visible while one of them is edited. Drawn + // before the active layer's tint so that one reads on top where the two overlap. + if (show_paint_overlay) + render_paint_overlay(m_other_paint_glmodel); + + // The translucent paint tint. Needed in the shaded view because the opaque highlight above is + // skipped there, and in the true-displacement view because the displaced surface rises *above* + // the undisplaced overlay geometry and hides it exactly where the relief is strongest - in both + // cases leaving an erase stroke with no visible effect until the next full preview rebuild. + if (show_paint_overlay && (use_shaded || use_true_preview)) + render_paint_overlay(m_paint_overlay_glmodel); + + // The UV editor's island selection, shown on the model. Polled here rather than pushed: the pane + // changes its selection in its own mouse handling, and a compare of a few ints per frame is free. + { + const TextureDisplacementLayer *al = active_layer(); + const UVEditorCanvas *uv_canvas = wxGetApp().plater()->get_uv_editor_canvas(); + if (m_show_uv_editor && al != nullptr && al->projection_method == TextureProjectionMethod::LSCM && + uv_canvas != nullptr && !m_uv_editor_unwrap.empty()) { + if (uv_canvas->selected_islands() != m_island_overlay_selection) + rebuild_island_overlay(uv_canvas->selected_islands()); + render_island_overlay(); + } else if (m_island_overlay_glmodel.is_initialized()) { + m_island_overlay_glmodel.reset(); + m_island_overlay_selection.clear(); + } + } + + // Diagnostic overlays, drawn on top of whatever preview is active (both pull toward the camera + // with a polygon offset so they win the depth test against the coincident surface). + if (m_uv_check_mode != UVCheckMode::None) + render_uvcheck_mesh(); + // While previewing a subdivision, its wireframe stands in for the mesh wireframe - it shows the + // density the model *would* have. The normal wireframe toggle is left untouched underneath, so it + // returns to whatever it was once the preview ends (which is what keeps an already-on wireframe on). + if (m_subdivide_editing) + render_subdivide_preview(); + else if (m_wireframe_overlay) + render_wireframe_overlay(); + // Marked seams are always shown for an LSCM layer, so existing cuts are visible before entering + // seam-edit mode - but they matter most while marking. + render_seam_overlay(); + + m_c->object_clipper()->render_cut(); + m_c->instances_hider()->render_cut(); + if (m_adjust_texture_mode) + render_adjust_texture_gizmo(); + else if (!m_seam_edit_mode) // the brush cursor is meaningless while marking seams + render_cursor(); + + glsafe(::glDisable(GL_BLEND)); +} + +bool GLGizmoTextureDisplacement::on_mouse(const wxMouseEvent &mouse_event) +{ + if (m_seam_edit_mode) + return on_mouse_seam(mouse_event); + if (m_adjust_texture_mode) + return on_mouse_adjust_texture(mouse_event); + + const bool handled = GLGizmoPainterBase::on_mouse(mouse_event); + // A consumed drag/click is a paint (or erase) event: the base class has already updated the live + // TriangleSelector, but nothing is flushed to the model - and so nothing rebuilds - until the + // stroke ends. Mark the tint stale so it follows the brush from the first frame instead. Only the + // flag is set here; the rebuild is coalesced to once per drawn frame in render_painter_gizmo(). + if (handled && (mouse_event.Dragging() || mouse_event.LeftDown() || mouse_event.RightDown() || + mouse_event.LeftUp() || mouse_event.RightUp())) + m_paint_overlay_dirty = true; + return handled; +} + +bool GLGizmoTextureDisplacement::on_mouse_seam(const wxMouseEvent &mouse_event) +{ + // Hold Ctrl to orbit/pan the camera while in seam mode, exactly as the base painter lets you do + // while painting: with Ctrl down we consume nothing, so the canvas gets the drag and moves the + // view. Without this, seam mode swallowed every left-drag and the camera couldn't be rotated. + if (mouse_event.CmdDown()) + return false; + + // Left-click marks/unmarks an edge; swallow the rest of the left-button stream so a drag doesn't + // paint, but let everything else through so the camera still orbits/pans/zooms normally. + if (mouse_event.LeftDown()) { + const Vec2d pos(mouse_event.GetX(), mouse_event.GetY()); + if (m_seam_path_mode) { + // Two-click shortest-path seam: first click sets the start vertex, the next seams the whole + // path to it and becomes the new start (so a seam line chains click by click). + const int v = seam_vertex_at(pos); + if (v >= 0) { + if (m_seam_path_anchor < 0) + m_seam_path_anchor = v; + else { + mark_seam_path(m_seam_path_anchor, v); + m_seam_path_anchor = v; + } + rebuild_seam_anchor_overlay(); + m_parent.set_as_dirty(); + } + } else { + toggle_seam_at(pos); + } + return true; + } + // Live hover: highlight what a click would pick, so the clickable target is obvious (the "I don't + // know how it works" the user hit). In normal mode that is an edge; in shortest-path mode it is a + // vertex. Don't swallow the motion - the camera still needs it. + if (mouse_event.Moving()) { + const Vec2d pos(mouse_event.GetX(), mouse_event.GetY()); + if (m_seam_path_mode) { + const int v = seam_vertex_at(pos); + if (v != m_seam_hover_vertex) { + m_seam_hover_vertex = v; + m_seam_hover_edge = { -1, -1 }; + rebuild_seam_hover_overlay(); + m_parent.set_as_dirty(); + } + } else { + const std::pair edge = seam_edge_at(pos); + if (edge != m_seam_hover_edge) { + m_seam_hover_edge = edge; + m_seam_hover_vertex = -1; + rebuild_seam_hover_overlay(); + m_parent.set_as_dirty(); + } + } + } + if (mouse_event.LeftUp() || (mouse_event.Dragging() && mouse_event.LeftIsDown())) + return true; + return false; +} + +int GLGizmoTextureDisplacement::texture_volume_raycaster_index() const +{ + const ModelVolume *mv = texture_volume(); + const ModelObject *mo = m_c->selection_info()->model_object(); + if (mv == nullptr || mo == nullptr) + return -1; + + int idx = -1, count = 0; + for (const ModelVolume *v : mo->volumes) { + if (!v->is_model_part()) + continue; + if (v == mv) { idx = count; break; } + ++count; + } + return (idx >= 0 && idx < int(m_c->raycaster()->raycasters().size())) ? idx : -1; +} + +std::pair GLGizmoTextureDisplacement::seam_edge_at(const Vec2d &mouse_pos) const +{ + const ModelVolume *mv = texture_volume(); + const ModelObject *mo = m_c->selection_info()->model_object(); + if (mv == nullptr || mo == nullptr) + return { -1, -1 }; + + const int idx = texture_volume_raycaster_index(); + if (idx < 0) + return { -1, -1 }; + const auto &raycasters = m_c->raycaster()->raycasters(); + + const Selection &selection = m_parent.get_selection(); + const Transform3d trafo = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix(); + const Camera &camera = wxGetApp().plater()->get_camera(); + + Vec3f hit = Vec3f::Zero(), normal = Vec3f::Zero(); + size_t facet = 0; + if (!raycasters[size_t(idx)]->unproject_on_mesh(mouse_pos, trafo, camera, hit, normal, + m_c->object_clipper()->get_clipping_plane(), &facet)) + return { -1, -1 }; + + const indexed_triangle_set &its = mv->mesh().its; + if (facet >= its.indices.size()) + return { -1, -1 }; + const stl_triangle_vertex_indices &tri = its.indices[facet]; + + // The facet edge nearest the hit point (point-to-segment distance in mesh space). + const auto seg_dist = [](const Vec3f &p, const Vec3f &a, const Vec3f &b) { + const Vec3f ab = b - a; + const float l2 = ab.squaredNorm(); + const float t = (l2 > 1e-12f) ? std::clamp((p - a).dot(ab) / l2, 0.f, 1.f) : 0.f; + return (p - (a + ab * t)).norm(); + }; + int best_i = 0; + float best_d = std::numeric_limits::max(); + for (int i = 0; i < 3; ++i) { + const float d = seg_dist(hit, its.vertices[tri[i]], its.vertices[tri[(i + 1) % 3]]); + if (d < best_d) { best_d = d; best_i = i; } + } + const int a = tri[best_i], b = tri[(best_i + 1) % 3]; + return { std::min(a, b), std::max(a, b) }; +} + +void GLGizmoTextureDisplacement::toggle_seam_at(const Vec2d &mouse_pos) +{ + TextureDisplacementLayer *layer = active_layer(); + if (layer == nullptr) + return; + const std::pair edge = seam_edge_at(mouse_pos); + if (edge.first < 0) + return; + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Mark texture seam"), UndoRedo::SnapshotType::GizmoAction); + auto &seams = layer->lscm_seam_edges; + if (const auto it = std::find(seams.begin(), seams.end(), edge); it != seams.end()) + seams.erase(it); + else + seams.push_back(edge); + rebuild_preview(); +} + +int GLGizmoTextureDisplacement::seam_vertex_at(const Vec2d &mouse_pos) const +{ + const ModelVolume *mv = texture_volume(); + const ModelObject *mo = m_c->selection_info()->model_object(); + if (mv == nullptr || mo == nullptr) + return -1; + const int idx = texture_volume_raycaster_index(); + if (idx < 0) + return -1; + const auto &raycasters = m_c->raycaster()->raycasters(); + + const Selection &selection = m_parent.get_selection(); + const Transform3d trafo = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix(); + const Camera &camera = wxGetApp().plater()->get_camera(); + + Vec3f hit = Vec3f::Zero(), normal = Vec3f::Zero(); + size_t facet = 0; + if (!raycasters[size_t(idx)]->unproject_on_mesh(mouse_pos, trafo, camera, hit, normal, + m_c->object_clipper()->get_clipping_plane(), &facet)) + return -1; + const indexed_triangle_set &its = mv->mesh().its; + if (facet >= its.indices.size()) + return -1; + const stl_triangle_vertex_indices &tri = its.indices[facet]; + int best = tri[0]; + float best_d = std::numeric_limits::max(); + for (int i = 0; i < 3; ++i) { + const float d = (hit - its.vertices[tri[i]]).squaredNorm(); + if (d < best_d) { best_d = d; best = tri[i]; } + } + return best; +} + +void GLGizmoTextureDisplacement::mark_seam_path(int v_from, int v_to) +{ + TextureDisplacementLayer *layer = active_layer(); + const ModelVolume *mv = texture_volume(); + if (layer == nullptr || mv == nullptr || v_from < 0 || v_to < 0 || v_from == v_to) + return; + const indexed_triangle_set &its = mv->mesh().its; + const size_t n = its.vertices.size(); + if (size_t(v_from) >= n || size_t(v_to) >= n) + return; + + // Shortest path over the mesh's edge graph (Dijkstra, edge weight = length). Built on demand; one + // pass per click is fine even on a dense mesh. + std::vector>> adj(n); + for (const stl_triangle_vertex_indices &tri : its.indices) + for (int i = 0; i < 3; ++i) { + const int a = tri[i], b = tri[(i + 1) % 3]; + const float w = (its.vertices[size_t(a)] - its.vertices[size_t(b)]).norm(); + adj[size_t(a)].push_back({ b, w }); + adj[size_t(b)].push_back({ a, w }); + } + + std::vector dist(n, std::numeric_limits::infinity()); + std::vector prev(n, -1); + using QN = std::pair; + std::priority_queue, std::greater> pq; + dist[size_t(v_from)] = 0.f; + pq.push({ 0.f, v_from }); + while (!pq.empty()) { + const auto [d, u] = pq.top(); + pq.pop(); + if (d > dist[size_t(u)]) + continue; + if (u == v_to) + break; + for (const auto &[w, ew] : adj[size_t(u)]) { + const float nd = d + ew; + if (nd < dist[size_t(w)]) { + dist[size_t(w)] = nd; + prev[size_t(w)] = u; + pq.push({ nd, w }); + } + } + } + if (prev[size_t(v_to)] < 0) + return; // unreachable (disconnected components) + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Mark texture seam path"), UndoRedo::SnapshotType::GizmoAction); + auto &seams = layer->lscm_seam_edges; + for (int v = v_to; v != v_from && v >= 0; v = prev[size_t(v)]) { + const int p = prev[size_t(v)]; + if (p < 0) + break; + const std::pair e{ std::min(v, p), std::max(v, p) }; + if (std::find(seams.begin(), seams.end(), e) == seams.end()) + seams.push_back(e); + } + rebuild_preview(); +} + +void GLGizmoTextureDisplacement::rebuild_seam_anchor_overlay() +{ + m_seam_anchor_glmodel.reset(); + const ModelVolume *mv = texture_volume(); + if (!m_seam_edit_mode || !m_seam_path_mode || mv == nullptr || m_seam_path_anchor < 0) + return; + const indexed_triangle_set &its = mv->mesh().its; + if (size_t(m_seam_path_anchor) >= its.vertices.size()) + return; + + // The anchor's incident edges, so the path's start vertex is visible on the model. + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + unsigned nn = 0; + for (const stl_triangle_vertex_indices &tri : its.indices) + for (int i = 0; i < 3; ++i) { + const int a = tri[i], b = tri[(i + 1) % 3]; + if (a == m_seam_path_anchor || b == m_seam_path_anchor) { + init_data.add_vertex(its.vertices[size_t(a)]); + init_data.add_vertex(its.vertices[size_t(b)]); + init_data.add_line(nn, nn + 1); + nn += 2; + } + } + if (!init_data.is_empty()) + m_seam_anchor_glmodel.init_from(std::move(init_data)); +} + +void GLGizmoTextureDisplacement::rebuild_seam_hover_overlay() +{ + m_seam_hover_glmodel.reset(); + const ModelVolume *mv = texture_volume(); + if (!m_seam_edit_mode || mv == nullptr) + return; + const indexed_triangle_set &its = mv->mesh().its; + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + + if (m_seam_path_mode) { + // Highlight the hovered vertex as its ring of incident edges, so the click target is legible on + // a dense mesh (matching the green anchor's style, in the hover yellow render_seam_overlay uses). + if (m_seam_hover_vertex < 0 || size_t(m_seam_hover_vertex) >= its.vertices.size()) + return; + unsigned nn = 0; + for (const stl_triangle_vertex_indices &tri : its.indices) + for (int i = 0; i < 3; ++i) { + const int a = tri[i], b = tri[(i + 1) % 3]; + if (a == m_seam_hover_vertex || b == m_seam_hover_vertex) { + init_data.add_vertex(its.vertices[size_t(a)]); + init_data.add_vertex(its.vertices[size_t(b)]); + init_data.add_line(nn, nn + 1); + nn += 2; + } + } + if (!init_data.is_empty()) + m_seam_hover_glmodel.init_from(std::move(init_data)); + return; + } + + if (m_seam_hover_edge.first < 0 || size_t(m_seam_hover_edge.first) >= its.vertices.size() || + size_t(m_seam_hover_edge.second) >= its.vertices.size()) + return; + init_data.reserve_vertices(2); + init_data.reserve_indices(2); + init_data.add_vertex(its.vertices[size_t(m_seam_hover_edge.first)]); + init_data.add_vertex(its.vertices[size_t(m_seam_hover_edge.second)]); + init_data.add_line(0, 1); + m_seam_hover_glmodel.init_from(std::move(init_data)); +} + +void GLGizmoTextureDisplacement::rebuild_seam_overlay() +{ + m_seam_glmodel.reset(); + const ModelVolume *mv = texture_volume(); + const TextureDisplacementLayer *layer = active_layer(); + if (mv == nullptr || layer == nullptr || layer->lscm_seam_edges.empty()) + return; + const indexed_triangle_set &its = mv->mesh().its; + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(layer->lscm_seam_edges.size() * 2); + init_data.reserve_indices(layer->lscm_seam_edges.size() * 2); + unsigned n = 0; + for (const auto &[a, b] : layer->lscm_seam_edges) { + if (a < 0 || b < 0 || size_t(a) >= its.vertices.size() || size_t(b) >= its.vertices.size()) + continue; + init_data.add_vertex(its.vertices[size_t(a)]); + init_data.add_vertex(its.vertices[size_t(b)]); + init_data.add_line(n, n + 1); + n += 2; + } + if (!init_data.is_empty()) + m_seam_glmodel.init_from(std::move(init_data)); +} + +void GLGizmoTextureDisplacement::render_seam_overlay() +{ + const ModelObject *mo = m_c->selection_info()->model_object(); + const ModelVolume *mv = texture_volume(); + const bool have_marked = m_seam_glmodel.is_initialized(); + const bool have_hover = m_seam_edit_mode && m_seam_hover_glmodel.is_initialized(); + const bool have_anchor = m_seam_edit_mode && m_seam_anchor_glmodel.is_initialized(); + if (mo == nullptr || mv == nullptr || (!have_marked && !have_hover && !have_anchor)) + return; + GLShaderProgram *shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + const Selection &selection = m_parent.get_selection(); + const Transform3d trafo_matrix = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix(); + const Camera &camera = wxGetApp().plater()->get_camera(); + + shader->start_using(); + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * trafo_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + glsafe(::glEnable(GL_POLYGON_OFFSET_LINE)); + glsafe(::glPolygonOffset(-2.0f, -2.0f)); // pull further forward than the wireframe so seams read on top + // A seam edge is geometrically the same line as a wireframe edge, so a mere polygon offset is a + // fragile way to make the red seam beat the white wireframe - drivers apply GL_POLYGON_OFFSET_LINE + // inconsistently, and the two lines then z-fight and the wireframe wins. When the wireframe is on, + // or while actively marking, just draw the seams with depth testing off so they are unconditionally + // on top - being visible is the one thing this overlay has to guarantee. + const bool seams_on_top = m_wireframe_overlay || m_seam_edit_mode; + if (seams_on_top) + glsafe(::glDisable(GL_DEPTH_TEST)); +#if !SLIC3R_OPENGL_ES + const bool wide = !OpenGLManager::get_gl_info().is_core_profile(); + if (wide) + glsafe(::glLineWidth(4.0f)); +#endif // !SLIC3R_OPENGL_ES + if (have_marked) { + m_seam_glmodel.set_color(ColorRGBA(1.0f, 0.15f, 0.15f, 1.0f)); // Blender's seam red + m_seam_glmodel.render(); + } + // The edge a click would toggle, in yellow and pulled the furthest forward, so it is unmistakable + // which edge is being targeted while marking seams. + if (have_hover) { + glsafe(::glPolygonOffset(-3.0f, -3.0f)); + m_seam_hover_glmodel.set_color(ColorRGBA(1.0f, 0.9f, 0.15f, 1.0f)); + m_seam_hover_glmodel.render(); + } + // The shortest-path start vertex, shown as its ring of incident edges in green. + if (have_anchor) { + glsafe(::glPolygonOffset(-3.0f, -3.0f)); + m_seam_anchor_glmodel.set_color(ColorRGBA(0.2f, 1.0f, 0.4f, 1.0f)); + m_seam_anchor_glmodel.render(); + } +#if !SLIC3R_OPENGL_ES + if (wide) + glsafe(::glLineWidth(1.0f)); +#endif // !SLIC3R_OPENGL_ES + glsafe(::glDisable(GL_POLYGON_OFFSET_LINE)); + if (seams_on_top) + glsafe(::glEnable(GL_DEPTH_TEST)); + shader->stop_using(); +} + +void GLGizmoTextureDisplacement::render_preview_mesh() +{ + const ModelObject *mo = m_c->selection_info()->model_object(); + const ModelVolume *mv = texture_volume(); + if (mo == nullptr || mv == nullptr) + return; + + const Selection &selection = m_parent.get_selection(); + const Transform3d trafo_matrix = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix(); + + auto *shader = wxGetApp().get_shader("gouraud_light"); + if (shader == nullptr) + return; + shader->start_using(); + const Camera &camera = wxGetApp().plater()->get_camera(); + const Transform3d &view_matrix = camera.get_view_matrix(); + shader->set_uniform("view_model_matrix", view_matrix * trafo_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * trafo_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); + shader->set_uniform("view_normal_matrix", view_normal_matrix); + if (m_preview_color_runs.empty()) { + m_preview_glmodel.render(); + } else { + // set_color() writes the uniform the shader reads, so one call per group is all the + // per-triangle colour this needs. + for (const PreviewColorRun &run : m_preview_color_runs) { + m_preview_glmodel.set_color(run.color); + m_preview_glmodel.render(run.range, shader); + } + } + shader->stop_using(); +} + +float GLGizmoTextureDisplacement::layer_texture_aspect(const TextureDisplacementLayer &layer) +{ + // decode_height_texture() is cached on the image_data allocation, so this is a hash lookup rather + // than a PNG decode - cheap enough to call per rebuild. + const DecodedHeightTexture tex = decode_height_texture(layer); + return (tex.width > 0 && tex.height > 0) ? float(tex.width) / float(tex.height) : 1.f; +} + +indexed_triangle_set GLGizmoTextureDisplacement::patch_in_world(const indexed_triangle_set &patch) const +{ + const ModelVolume *mv = texture_volume(); + if (mv == nullptr) + return patch; + // The bake's own frame, so the projection this feeds agrees with what bakes. + const Transform3d to_world = texture_displacement_bake_frame(texture_displacement_volume_to_world(*mv)); + if (to_world.matrix().isApprox(Transform3d::Identity().matrix())) + return patch; + + indexed_triangle_set world = patch; + for (Vec3f &v : world.vertices) + v = (to_world * v.cast()).cast(); + return world; +} + +std::vector GLGizmoTextureDisplacement::compute_layer_vertex_uvs(const indexed_triangle_set &local_patch, + const TextureDisplacementLayer &layer) const +{ + // The bake maps the texture in world millimetres, so "Tile size (mm)" means the same thing on a + // scaled instance as it does on an untouched one (see build_texture_displacement()). Everything + // that has to agree with the bake - the fast preview's uvs, the checker/distortion overlays - + // therefore has to project from the same world positions, not from the volume's own. + const indexed_triangle_set patch = patch_in_world(local_patch); + const float aspect = layer_texture_aspect(layer); + if (layer.projection_method == TextureProjectionMethod::LSCM) { + // compute_lscm_uvs() returns the unwrap's own (raw, mm) coordinates with the island placement + // folded in - it does *not* apply the layer's tiling/rotation/offset. The bake applies those + // on top (sample_layer_height()'s lscm branch runs the result through sample_at()), so the + // shader's precomputed-uv path has to as well, or the fast preview samples millimetre-valued + // coordinates as if they were uv and shows the texture at a wildly wrong scale. + std::vector uv = compute_lscm_uvs(patch, layer); + for (Vec2f &p : uv) + p = apply_uv_transform(p, layer, aspect); + return uv; + } + if (layer.projection_method == TextureProjectionMethod::ViewProjected) { + std::vector uv(patch.vertices.size()); + for (size_t vi = 0; vi < patch.vertices.size(); ++vi) { + if (layer.view_project_projective) { + // Matches sample_layer_height()'s projective branch, including skipping + // apply_uv_transform(). A vertex behind the projector gets a uv far outside [0,1] so + // it samples as nothing, rather than the mirrored coordinate a blind divide gives. + if (!project_uv_projective(layer.view_project_matrix, patch.vertices[vi], uv[vi])) + uv[vi] = Vec2f(-1e6f, -1e6f); + continue; + } + const Vec2f planar(patch.vertices[vi].dot(layer.view_project_right), + patch.vertices[vi].dot(layer.view_project_up)); + uv[vi] = apply_uv_transform(planar, layer, aspect); + } + return uv; + } + return {}; // Triplanar / Cylindrical / Spherical: the shader projects on its own +} + +int GLGizmoTextureDisplacement::layer_projection_frame(const indexed_triangle_set &local_patch, + const TextureDisplacementLayer &layer, + Vec3f ¢er, Vec3f &axis) const +{ + center = Vec3f::Zero(); + axis = Vec3f::UnitZ(); + const ModelVolume *mv = texture_volume(); + if (mv == nullptr || (layer.projection_method != TextureProjectionMethod::Cylindrical && + layer.projection_method != TextureProjectionMethod::Spherical)) + return 0; + // The bake averages the *whole mesh's* vertex normals over the patch's corners, so this has to as + // well: a patch-only average would sometimes quantize to a different world axis and wrap the + // texture the other way round. Both meshes go through patch_in_world() first, which is the frame + // the shaders' tex_pos lives in. + Vec3f average_normal; + texture_displacement_patch_frame(patch_in_world(local_patch), + texture_displacement_vertex_normals(patch_in_world(mv->mesh().its)), + center, axis, average_normal); + return layer.projection_method == TextureProjectionMethod::Cylindrical ? 1 : 2; +} + +std::vector GLGizmoTextureDisplacement::compute_layer_corner_uvs(const indexed_triangle_set &local_patch, + const TextureDisplacementLayer &layer) const +{ + if (layer.projection_method == TextureProjectionMethod::LSCM) { + const indexed_triangle_set patch = patch_in_world(local_patch); + const float aspect = layer_texture_aspect(layer); + std::vector uv = compute_lscm_corner_uvs(patch, layer); + for (Vec2f &p : uv) + p = apply_uv_transform(p, layer, aspect); + return uv; + } + // Single-valued per point: fan the per-vertex result out over the corners. + const std::vector per_vertex = compute_layer_vertex_uvs(local_patch, layer); + if (per_vertex.size() != local_patch.vertices.size()) + return {}; + std::vector corner(local_patch.indices.size() * 3); + for (size_t f = 0; f < local_patch.indices.size(); ++f) + for (int k = 0; k < 3; ++k) + corner[f * 3 + size_t(k)] = per_vertex[size_t(local_patch.indices[f][k])]; + return corner; +} + +void GLGizmoTextureDisplacement::rebuild_shaded_preview_mesh() +{ + m_shaded_preview_glmodel.reset(); + + const ModelVolume *mv = texture_volume(); + if (mv == nullptr || m_triangle_selectors.empty()) + return; + + // Uses the *live* selector (not the flushed model facet data), so this reflects an in-progress + // stroke immediately rather than only once it ends - the point of this preview mode is to be + // the fast, no-CPU-meshing one. + const indexed_triangle_set patch = m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER); + if (patch.indices.empty()) + return; + + // No "patch.vertices.size() == mesh vertex count" check here, and that is the point: a *brush* + // stroke splits triangles, so the selector appends split vertices and the patch array is longer + // than the mesh's. An earlier version bailed out on that as "shouldn't happen", which meant the + // shaded model was never built while brushing and render_painter_gizmo() silently fell back to the + // Normal (true-displacement) preview - Fast looked broken for brush and fine for Face/Connected + // area, because only the brush splits. Everything below indexes the patch's own vertex array, so + // the extra vertices are simply carried through. + + // Unpainted triangles, so the surrounding surface still renders (the render path hides the real + // model in shaded mode). get_facets_strict() returns the same vertex array whatever state is asked. + const indexed_triangle_set rest = m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::NONE); + + // For LSCM we hand the shader the finished per-vertex texture uv (island placement + tiling/ + // rotation/offset already folded in, exactly what the bake samples), because it cannot be + // reconstructed in the fragment shader the way a triplanar projection can. This is also what + // makes the fast preview follow the UV editor: the uvs move when an island is dragged, so this + // mesh rebuilds (on drag end) with them. The other projections keep projecting in-shader. + // Per *corner*, not per vertex: the mesh below is flat (unshared) anyway, so each triangle can + // carry its own chart's UVs - see compute_layer_corner_uvs(). + const TextureDisplacementLayer *active = active_layer(); + std::vector corner_uv = active != nullptr ? compute_layer_corner_uvs(patch, *active) : std::vector{}; + m_shaded_preview_uses_vertex_uv = corner_uv.size() == patch.indices.size() * 3; + if (!m_shaded_preview_uses_vertex_uv) + corner_uv.clear(); + m_shaded_projection_mode = (active != nullptr && !m_shaded_preview_uses_vertex_uv) ? + layer_projection_frame(patch, *active, m_shaded_patch_center, m_shaded_patch_axis) : 0; + + // Which triangles the in-flight UV drag moves. Computed here, against the very patch this mesh is + // built from, so the flags can never be indexed by a different triangle count than they were sized + // for (the drag starts from the flushed facet data, a brush stroke changes the live selector). + compute_shaded_active_faces(m_shaded_active_chart >= 0 ? m_island_move_set : std::vector{}, patch.indices.size()); + + // Colour is quantized per *fragment* in the shader now (see the .fs), so this mesh carries no + // colour of its own - the palette and the colour texture are uniforms, and every pixel matches the + // image rather than the facet it landed on. What the *bake* will produce, at facet resolution, is + // what the Normal view shows. + m_shaded_preview_palette = (active != nullptr && active->color_enabled) ? cached_palette() + : std::vector{}; + + GLModel::Geometry init_data; + // P3N3T2: normal.x carries the paint weight, tex_coord the precomputed uv (see the vertex shader). + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3T2 }; + + // Per-triangle weighting via a *flat* (unshared-vertex) mesh: every corner of a painted triangle + // gets weight 1, every corner of an unpainted one weight 0. This is what a coarse mesh needs - one + // painted face of a raw cube has no strictly-interior vertex (all 8 are shared), so per-vertex + // weighting would either bleed onto the neighbours (boundary weight 1) or vanish outright (boundary + // weight 0, which is what made a single face show nothing). Duplicating vertices costs no shading + // quality here because the preview shader takes its surface normal from screen-space derivatives of + // position (dFdx/dFdy), not from a per-vertex normal. normal.y flags the UV-editor island being + // dragged so the shader can move just that island via the island_delta uniform. + const size_t tri_total = patch.indices.size() + rest.indices.size(); + init_data.reserve_vertices(tri_total * 3); + init_data.reserve_indices(tri_total * 3); + unsigned vcount = 0; + const auto emit_triangles = [&](const indexed_triangle_set &its, float weight, bool painted) { + for (size_t f = 0; f < its.indices.size(); ++f) { + const stl_triangle_vertex_indices &tri = its.indices[f]; + // One value for the whole triangle: island_active is an interpolated varying, so the three + // corners have to agree or the shader moves part of a triangle and not the rest. + const float act = (painted && f < m_shaded_active_face.size() && m_shaded_active_face[f]) ? 1.f : 0.f; + for (int i = 0; i < 3; ++i) { + const int idx = tri[i]; + const Vec2f uv = (painted && m_shaded_preview_uses_vertex_uv) ? corner_uv[f * 3 + size_t(i)] + : Vec2f::Zero(); + init_data.add_vertex(its.vertices[size_t(idx)], Vec3f(weight, act, 0.f), uv); + } + init_data.add_triangle(vcount, vcount + 1, vcount + 2); + vcount += 3; + } + }; + emit_triangles(patch, 1.f, true); // painted -> shaded as relief, and coloured by the shader + // Untouched surface: flat, so it still shows but carries no relief - and uncoloured, which is what the + // bake leaves it as (EnforcerBlockerType::NONE, i.e. the volume's own filament). + emit_triangles(rest, 0.f, false); + + m_shaded_preview_glmodel.init_from(std::move(init_data)); + // GLModel::render() unconditionally re-sets the shader's "uniform_color" from this internal + // color field right before drawing (see GLModel.cpp) - setting the uniform manually in + // render_shaded_preview_mesh() would just get overwritten by it, so it must be set here instead. + // GLModel::Geometry defaults to BLACK, which is exactly what showed up before this was added. + m_shaded_preview_glmodel.set_color(GLVolume::NEUTRAL_COLOR); + + // The mesh now reflects the islands' current placement, so any live drag delta is measured from + // here: reset it to identity and record the dragged island's baked transform. + m_shaded_island_delta = Eigen::Matrix::Identity(); + const TextureDisplacementLayer *al = active_layer(); + if (m_shaded_active_chart >= 0 && al != nullptr) { + const std::vector> xf = uv_editor_island_transforms(*al); + m_shaded_baked_active_xf = (size_t(m_shaded_active_chart) < xf.size()) ? xf[size_t(m_shaded_active_chart)] + : Eigen::Matrix::Identity(); + } else { + m_shaded_baked_active_xf = Eigen::Matrix::Identity(); + } +} + +void GLGizmoTextureDisplacement::compute_shaded_active_faces(const std::vector &charts, size_t patch_face_count) +{ + m_shaded_active_face.clear(); + if (charts.empty() || patch_face_count == 0) + return; + const PatchUnwrap &u = m_uv_editor_unwrap; + if (u.source_face.size() != u.indices.size()) + return; + m_shaded_active_face.assign(patch_face_count, 0); + // Flag every triangle of every chart being moved. For a group/multi move that is more than one + // chart, but since such a move is a pure translation the shader applies the same delta to them all + // (see on_island_edited) - exactly the "joined islands move together" behaviour. + for (size_t t = 0; t < u.indices.size(); ++t) { + const int f = u.source_face[t]; + const int v0 = u.indices[t][0]; // a triangle lies in one chart, so any corner names it + if (f < 0 || size_t(f) >= m_shaded_active_face.size() || v0 < 0 || size_t(v0) >= u.vertex_chart.size()) + continue; + if (std::find(charts.begin(), charts.end(), u.vertex_chart[size_t(v0)]) != charts.end()) + m_shaded_active_face[size_t(f)] = 1; + } +} + +int GLGizmoTextureDisplacement::island_group_of(const std::vector &groups, int c) +{ + return (c >= 0 && size_t(c) < groups.size() && groups[size_t(c)] >= 0) ? groups[size_t(c)] : c; +} + +void GLGizmoTextureDisplacement::join_island_groups(std::vector &groups, int a, int b, int chart_count) +{ + if (a < 0 || b < 0 || chart_count <= 0) + return; + // Materialise to a full explicit table first, so singletons (which were implicit) get a concrete id + // that the relabel loop below can match on. + if (int(groups.size()) < chart_count) { + const size_t old = groups.size(); + groups.resize(size_t(chart_count)); + for (size_t i = old; i < groups.size(); ++i) + groups[i] = int(i); + } + for (size_t i = 0; i < groups.size(); ++i) + if (groups[i] < 0) + groups[i] = int(i); + const int ga = groups[size_t(a)], gb = groups[size_t(b)]; + if (ga == gb) + return; + const int g = std::min(ga, gb); + for (int &x : groups) + if (x == ga || x == gb) + x = g; +} + +std::vector GLGizmoTextureDisplacement::build_island_move_set(const TextureDisplacementLayer &layer, int primary) const +{ + std::vector set; + const int chart_count = std::max(m_uv_editor_unwrap.chart_count, 0); + + // Seed with the pane's multi-selection (falling back to just the primary if the canvas has none). + std::vector seeds; + if (const UVEditorCanvas *canvas = wxGetApp().plater()->get_uv_editor_canvas()) + seeds = canvas->selected_islands(); + if (seeds.empty() && primary >= 0) + seeds.push_back(primary); + + const auto add = [&set](int c) { + if (c >= 0 && std::find(set.begin(), set.end(), c) == set.end()) + set.push_back(c); + }; + for (int s : seeds) { + add(s); + // Pull in every chart sharing s's join group, so a joined pair moves as one. + const int gs = island_group_of(layer.island_groups, s); + for (int c = 0; c < chart_count; ++c) + if (island_group_of(layer.island_groups, c) == gs) + add(c); + } + add(primary); // never leave the primary out, whatever the selection state + return set; +} + +void GLGizmoTextureDisplacement::render_shaded_preview_mesh() +{ + const ModelObject *mo = m_c->selection_info()->model_object(); + const ModelVolume *mv = texture_volume(); + if (mo == nullptr || mv == nullptr || !m_shaded_preview_glmodel.is_initialized()) + return; + + const TextureDisplacementLayer *layer = active_layer(); + if (layer == nullptr || layer->empty()) + return; + + // Full-resolution height upload (smoothing-aware), whose grayscale value lives in the R channel + // exactly as the shader samples it. Deliberately *not* the layer-list panel's thumbnail: that one + // is box-filtered down to 128 px for a ~48 px row, and feeding it to the shader cost the preview + // three quarters of the height map's detail - and, since height_tex_texel is derived from it, also + // flattened the shading gradient and made the parallax march skip itself at angles where it should + // run. Cached on the image_data pointer + smoothing, so no PNG is decoded per frame. + GLTexture *tex = get_layer_height_texture(*layer); + if (tex == nullptr || tex->get_width() <= 0 || tex->get_height() <= 0) + return; + + GLShaderProgram *shader = wxGetApp().get_shader("texture_displacement_shaded"); + if (shader == nullptr) + return; + + const Selection &selection = m_parent.get_selection(); + const Transform3d trafo_matrix = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix(); + const Camera &camera = wxGetApp().plater()->get_camera(); + + shader->start_using(); + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * trafo_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + shader->set_uniform("volume_world_matrix", trafo_matrix); + const ClippingPlaneDataWrapper clp_data = this->get_clipping_plane_data(); + shader->set_uniform("clipping_plane", clp_data.clp_dataf); + shader->set_uniform("z_range", clp_data.z_range); + // The shader works on world-space normals (it projects the texture in world millimetres, as the + // bake does), so the normal matrix is the view rotation alone - no model part. + const Matrix3d view_normal_matrix = camera.get_view_matrix().matrix().block(0, 0, 3, 3); + shader->set_uniform("view_normal_matrix", view_normal_matrix); + shader->set_uniform("volume_mirrored", trafo_matrix.matrix().determinant() < 0.0); + glsafe(::glActiveTexture(GL_TEXTURE0)); + glsafe(::glBindTexture(GL_TEXTURE_2D, tex->get_id())); + // Match DecodedHeightTexture::sample()'s tiling. The sampler's wrap mode is the only place the + // GPU path can express this, and nothing ever set it - so it sat at GL_REPEAT no matter what the + // layer said: a MirroredRepeat layer previewed as a plain repeat, and a layer with tiling *off* + // previewed as an endless tiling where the bake produces one placement and nothing around it. + // CLAMP_TO_BORDER with a zero border is the exact analogue of sample()'s "outside [0,1) is 0". + // GL_CLAMP_TO_BORDER is desktop-GL only; on ES the nearest thing is CLAMP_TO_EDGE, which smears + // the border row instead of vanishing - still much closer to the bake than an endless repeat. +#if SLIC3R_OPENGL_ES + const GLint no_tile_wrap = GL_CLAMP_TO_EDGE; +#else + const GLint no_tile_wrap = GL_CLAMP_TO_BORDER; +#endif + const GLint wrap = !layer->tile_enabled ? no_tile_wrap : + (layer->tile_method == TextureTileMethod::MirroredRepeat) ? GL_MIRRORED_REPEAT : + GL_REPEAT; + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap)); +#if !SLIC3R_OPENGL_ES + if (wrap == GL_CLAMP_TO_BORDER) { + static const GLfloat border[4] = { 0.f, 0.f, 0.f, 0.f }; + glsafe(::glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border)); + } +#endif // !SLIC3R_OPENGL_ES + shader->set_uniform("height_tex", 0); + shader->set_uniform("height_tex_texel", Vec2f(1.f / float(tex->get_width()), 1.f / float(tex->get_height()))); + shader->set_uniform("depth_mm", layer->depth_mm); + shader->set_uniform("tiling_scale", layer->tiling_scale); + // Read off the uploaded texture rather than the decoded one: they are the same image, and this is + // the aspect the sampler will actually see. + shader->set_uniform("tex_aspect", float(tex->get_width()) / float(tex->get_height())); + shader->set_uniform("rotation_rad", layer->rotation_deg * float(M_PI) / 180.f); + shader->set_uniform("uv_offset", layer->offset); + shader->set_uniform("invert", layer->invert); + // The parallax step in the triplanar path needs the real height, not just its gradient, so it + // needs the midlevel the bake subtracts - and the camera position in the volume's own local space, + // to build the view ray it walks along. Without the parallax the pattern is welded to the base + // surface: it does not slide as the camera orbits and does not deepen with depth_mm, which is + // exactly when the fast preview stops looking like geometry. + shader->set_uniform("midlevel", layer->midlevel); + // The texture frame is world space anchored at the volume's origin (texture_displacement_bake_frame()): + // the shader subtracts the anchor from the world position, and the eye is handed over the same way. + const Vec3d tex_anchor = trafo_matrix.translation(); + shader->set_uniform("tex_anchor", Vec3f(tex_anchor.cast())); + shader->set_uniform("eye_model_pos", Vec3f((camera.get_position() - tex_anchor).cast())); + // When set, the shader samples at the per-vertex uv baked into the mesh (LSCM) rather than + // projecting; see rebuild_shaded_preview_mesh(). + shader->set_uniform("use_vertex_uv", m_shaded_preview_uses_vertex_uv); + // Cylindrical/Spherical wrap around the painted patch's own centre, which no fragment can derive: + // captured with the mesh (see rebuild_shaded_preview_mesh()) and handed over here. 0 is the planar + // projection every other in-shader path uses. + shader->set_uniform("projection_mode", m_shaded_projection_mode); + shader->set_uniform("patch_center", m_shaded_patch_center); + shader->set_uniform("patch_axis", m_shaded_patch_axis); + + // The filament palette the mesh's per-triangle indices refer to. Count 0 means "no layer is + // colouring", and the shader keeps the model's own colour for every fragment. + // The printable palette, in RGB for display and in Lab for the match. Uploaded rather than + // matched on the CPU because the quantization is per fragment here. + const GLTexture *color_tex = get_layer_color_texture(*layer); + const int palette_count = + (color_tex != nullptr) ? int(std::min(m_shaded_preview_palette.size(), size_t(PALETTE_MAX_ENTRIES))) : 0; + shader->set_uniform("palette_count", palette_count); + shader->set_uniform("has_color_tex", color_tex != nullptr); + // A flat-colour image is matched against single filaments only, as the bake does. + shader->set_uniform("pure_only", color_tex != nullptr && analyze_texture_detail(*layer).flat_colors); + for (int i = 0; i < palette_count; ++i) { + const PaletteEntry &e = m_shaded_preview_palette[size_t(i)]; + const std::string idx = "[" + std::to_string(i) + "]"; + shader->set_uniform(("palette_rgb" + idx).c_str(), e.rgb); + shader->set_uniform(("palette_lab" + idx).c_str(), srgb_to_lab(e.rgb)); + // How the entry prints: its filament, or for a mix the two it interleaves and in what ratio. + shader->set_uniform(("palette_a" + idx).c_str(), e.a); + shader->set_uniform(("palette_b" + idx).c_str(), e.b); + shader->set_uniform(("palette_num" + idx).c_str(), e.num); + shader->set_uniform(("palette_den" + idx).c_str(), e.den); + } + // The filaments those indices refer to, and the interleave the shader resolves a mix with - the + // same inputs make_mix_resolver() gets, so the preview shows the pattern that prints rather than + // the mix's smooth average colour. m_palette_filaments is what m_shaded_preview_palette was built from. + const int filament_count = + (palette_count > 0) ? int(std::min(m_palette_filaments.size(), size_t(PALETTE_MAX_FILAMENTS))) : 0; + shader->set_uniform("filament_count", filament_count); + for (int i = 0; i < filament_count; ++i) { + const ColorRGBA &c = m_palette_filaments[size_t(i)]; + shader->set_uniform(("filament_rgb[" + std::to_string(i) + "]").c_str(), Vec3f(c.r(), c.g(), c.b())); + } + shader->set_uniform("mix_mode", int(mv->texture_displacement_options.color_mix_mode)); + shader->set_uniform("layer_height", color_band_mm(*mv)); // as color_settings_for() + shader->set_uniform("dither_cell", std::max(m_subdivide_color_mm, 0.05f) * 2.f); // as color_settings_for() + if (color_tex != nullptr) { + shader->set_uniform("color_tex", 1); + glsafe(::glActiveTexture(GL_TEXTURE1)); + glsafe(::glBindTexture(GL_TEXTURE_2D, (GLuint) color_tex->get_id())); + glsafe(::glActiveTexture(GL_TEXTURE0)); + } + // The live UV-editor island drag rides this 2x3 affine (identity except mid-drag); only the flagged + // island's vertices apply it, so a drag is a uniform update rather than a mesh rebuild. + const Eigen::Matrix &d = m_shaded_island_delta; + shader->set_uniform("island_delta_lin", std::array{ d(0, 0), d(0, 1), d(1, 0), d(1, 1) }); + shader->set_uniform("island_delta_tr", Vec2f(d(0, 2), d(1, 2))); + m_shaded_preview_glmodel.render(); + glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); + shader->stop_using(); +} + +bool GLGizmoTextureDisplacement::shaded_preview_ready() const +{ + // Mirrors render_shaded_preview_mesh()'s own preconditions. Kept as a separate query because the + // caller has to know whether the shaded pass will draw *before* it hides the real volume for it. + if (m_c->selection_info() == nullptr || m_c->selection_info()->model_object() == nullptr) + return false; + if (texture_volume() == nullptr) + return false; + const TextureDisplacementLayer *layer = active_layer(); + if (layer == nullptr || layer->empty()) + return false; + // The same texture render_shaded_preview_mesh() will bind, not the panel thumbnail - the two are + // separate caches and either can fail on its own. + const GLTexture *tex = const_cast(this)->get_layer_height_texture(*layer); + if (tex == nullptr || tex->get_width() <= 0 || tex->get_height() <= 0) + return false; + return wxGetApp().get_shader("texture_displacement_shaded") != nullptr; +} + +// Appends a painted patch to an overlay, lifted onto the displaced surface where that has the base mesh's +// topology (see rebuild_paint_overlay()). +static void append_paint_patch(GLModel::Geometry &out, const indexed_triangle_set &patch, const std::vector *displaced) +{ + unsigned n = unsigned(out.vertices_count()); + for (const stl_triangle_vertex_indices &tri : patch.indices) { + for (int i = 0; i < 3; ++i) { + const size_t idx = size_t(tri[i]); + out.add_vertex((displaced != nullptr && idx < displaced->size()) ? (*displaced)[idx] : patch.vertices[idx]); + } + out.add_triangle(n, n + 1, n + 2); + n += 3; + } +} + +void GLGizmoTextureDisplacement::rebuild_other_paint_overlay() +{ + const ModelVolume *mv = texture_volume(); + // What it depends on: the volume, which layer is active, every other layer's paint (by its timestamp) and the + // displaced positions it is lifted onto. Compared every frame, rebuilt only when it differs. + std::string key; + if (mv != nullptr) { + key = std::to_string(mv->id().id) + ":" + std::to_string(m_active_layer_slot) + (m_use_shaded_preview ? ":b:" : ":t:") + + std::to_string(reinterpret_cast(m_preview_its.vertices.data())) + ":" + + std::to_string(m_preview_its.vertices.size()); + for (const TextureDisplacementLayer &l : mv->texture_displacement_layers) + if (l.slot != m_active_layer_slot && l.slot >= 0 && l.slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS)) + key += "|" + std::to_string(l.slot) + "@" + std::to_string(mv->texture_displacement_facet(l.slot).timestamp()); + } + if (key == m_other_paint_key) + return; + m_other_paint_key = std::move(key); + m_other_paint_glmodel.reset(); + if (mv == nullptr) + return; + + const std::vector *displaced = nullptr; + if (!m_use_shaded_preview && m_preview_its.vertices.size() == mv->mesh().its.vertices.size() && + !m_preview_its.vertices.empty()) + displaced = &m_preview_its.vertices; + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + for (const TextureDisplacementLayer &l : mv->texture_displacement_layers) { + if (l.slot == m_active_layer_slot || l.slot < 0 || l.slot >= int(TEXTURE_DISPLACEMENT_MAX_LAYERS) || + mv->texture_displacement_facet(l.slot).empty()) + continue; + TriangleSelector selector(mv->mesh()); + selector.deserialize(mv->texture_displacement_facet(l.slot).get_data(), false); + append_paint_patch(init_data, selector.get_facets_strict(EnforcerBlockerType::ENFORCER), displaced); + } + if (init_data.is_empty()) + return; + m_other_paint_glmodel.init_from(std::move(init_data)); + // Neutral grey: painted, but not the layer the brush is working on. + m_other_paint_glmodel.set_color(ColorRGBA(0.55f, 0.58f, 0.60f, 0.35f)); +} + +void GLGizmoTextureDisplacement::rebuild_paint_overlay() +{ + m_paint_overlay_glmodel.reset(); + const ModelVolume *mv = texture_volume(); + if (mv == nullptr || m_triangle_selectors.empty()) + return; + + // The *live* selector, so an in-progress stroke shows immediately - which is the whole point: + // this is the only feedback that a brush actually added or erased anything until the (much more + // expensive) preview catches up at stroke end. + const indexed_triangle_set patch = m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER); + if (patch.indices.empty()) + return; + + // In the true-displacement view the surface on screen is the *raised* one, and a tint built on + // the flat base mesh would sink underneath it wherever the relief is deepest - which is precisely + // where the user is looking. The bake is topology-preserving (patch vertex i is mesh vertex i, see + // build_texture_displacement()), so the displaced positions can be read straight across. Vertices + // the brush split live past the end of that array and keep their flat position; they sit on the + // patch boundary, where the displacement is smallest anyway. + const std::vector *displaced = nullptr; + if (!m_use_shaded_preview && m_preview_its.vertices.size() == mv->mesh().its.vertices.size() && + !m_preview_its.vertices.empty()) + displaced = &m_preview_its.vertices; + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(patch.indices.size() * 3); + init_data.reserve_indices(patch.indices.size() * 3); + append_paint_patch(init_data, patch, displaced); + m_paint_overlay_glmodel.init_from(std::move(init_data)); + // GLModel::render() re-sets "uniform_color" from this field just before drawing, so the colour + // has to be set here rather than as a uniform at draw time. + m_paint_overlay_glmodel.set_color(ColorRGBA(0.16f, 0.79f, 0.35f, 0.38f)); +} + +void GLGizmoTextureDisplacement::render_paint_overlay(GLModel &overlay) +{ + const ModelObject *mo = m_c->selection_info()->model_object(); + const ModelVolume *mv = texture_volume(); + if (mo == nullptr || mv == nullptr || !overlay.is_initialized()) + return; + GLShaderProgram *shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + const Selection &selection = m_parent.get_selection(); + const Transform3d trafo_matrix = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix(); + const Camera &camera = wxGetApp().plater()->get_camera(); + + shader->start_using(); + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * trafo_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + // Translucent, and pulled toward the camera so it wins the depth test against the coincident + // shaded surface. Depth writes are off: this is a tint, and letting it own the depth buffer would + // make the wireframe and seam overlays drawn after it fight with geometry that is not really + // there. Blending is already enabled by render_painter_gizmo(). + glsafe(::glEnable(GL_POLYGON_OFFSET_FILL)); + glsafe(::glPolygonOffset(-1.5f, -1.5f)); + glsafe(::glDepthMask(GL_FALSE)); + overlay.render(); + glsafe(::glDepthMask(GL_TRUE)); + glsafe(::glDisable(GL_POLYGON_OFFSET_FILL)); + shader->stop_using(); +} + +void GLGizmoTextureDisplacement::rebuild_island_overlay(const std::vector &selection) +{ + m_island_overlay_glmodel.reset(); + m_island_overlay_selection = selection; + const ModelVolume *mv = texture_volume(); + if (mv == nullptr || selection.empty() || m_uv_editor_unwrap.empty()) + return; + // The unwrap was made from the painted patch in the bake frame; the same extraction on the + // volume's own mesh gives the same triangles and vertex order in local coordinates, which is the + // frame the overlay is drawn in (with the volume's transform, like the paint tint). + const indexed_triangle_set patch = extract_painted_patch(mv->mesh().its, m_uv_editor_state.facets); + const PatchUnwrap &uw = m_uv_editor_unwrap; + std::vector chosen(size_t(std::max(uw.chart_count, 0)), 0); + for (const int c : selection) + if (c >= 0 && size_t(c) < chosen.size()) + chosen[size_t(c)] = 1; + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + unsigned n = 0; + for (const stl_triangle_vertex_indices &tri : uw.indices) { + const int v0 = tri[0]; + if (v0 < 0 || size_t(v0) >= uw.vertex_chart.size()) + continue; + const int c = uw.vertex_chart[size_t(v0)]; + if (c < 0 || size_t(c) >= chosen.size() || !chosen[size_t(c)]) + continue; + bool ok = true; + for (int k = 0; k < 3 && ok; ++k) { + const int u = tri[k]; + ok = u >= 0 && size_t(u) < uw.source_vertex.size() && uw.source_vertex[size_t(u)] >= 0 && + size_t(uw.source_vertex[size_t(u)]) < patch.vertices.size(); + } + if (!ok) + continue; + for (int k = 0; k < 3; ++k) + init_data.add_vertex(patch.vertices[size_t(uw.source_vertex[size_t(tri[k])])]); + init_data.add_triangle(n, n + 1, n + 2); + n += 3; + } + if (n == 0) + return; + m_island_overlay_glmodel.init_from(std::move(init_data)); + m_island_overlay_glmodel.set_color(ColorRGBA(0.10f, 0.55f, 0.95f, 0.45f)); // the pane's selection blue +} + +void GLGizmoTextureDisplacement::render_island_overlay() +{ + const ModelObject *mo = m_c->selection_info()->model_object(); + const ModelVolume *mv = texture_volume(); + if (mo == nullptr || mv == nullptr || !m_island_overlay_glmodel.is_initialized()) + return; + GLShaderProgram *shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + const Selection &selection = m_parent.get_selection(); + const Transform3d trafo_matrix = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix(); + const Camera &camera = wxGetApp().plater()->get_camera(); + shader->start_using(); + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * trafo_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + // Above the paint tint (a larger offset), translucent, no depth writes - a marker, not geometry. + glsafe(::glEnable(GL_POLYGON_OFFSET_FILL)); + glsafe(::glPolygonOffset(-2.0f, -2.0f)); + glsafe(::glDepthMask(GL_FALSE)); + m_island_overlay_glmodel.render(); + glsafe(::glDepthMask(GL_TRUE)); + glsafe(::glDisable(GL_POLYGON_OFFSET_FILL)); + shader->stop_using(); +} + +void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh() +{ + m_uvcheck_glmodel.reset(); + if (m_uv_check_mode == UVCheckMode::None) + return; + + const ModelVolume *mv = texture_volume(); + if (mv == nullptr || m_triangle_selectors.empty()) + return; + const indexed_triangle_set patch = m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER); + if (patch.indices.empty()) + return; + // Everything below works in the *patch's* vertex space, not the mesh's. Those agree only until a + // brush stroke splits a triangle, after which the patch array is longer - and since patch triangle + // indices are used to index it, reading the mesh's array instead would run off the end. An earlier + // version guarded that by bailing out, which quietly disabled the Checker and Distortion overlays + // for anything painted with the brush. + const TextureDisplacementLayer *layer = active_layer(); + if (layer == nullptr) + return; + + // The checker samples wherever the projection puts it; the projections the shader can't + // reconstruct (LSCM, ViewProjected) get a precomputed per-vertex uv, the rest project in-shader. + std::vector uv = compute_layer_vertex_uvs(patch, *layer); + const bool have_uvs = uv.size() == patch.vertices.size(); + m_uvcheck_uses_vertex_uv = have_uvs; + m_uvcheck_projection_mode = have_uvs ? 0 : + layer_projection_frame(patch, *layer, m_uvcheck_patch_center, m_uvcheck_patch_axis); + // Per corner as well: under LSCM a seam vertex has a + // different uv in each island it borders, so the shared-vertex form drew one triangle per face from + // a neighbouring island's placement. Only the *drawing* needs this; the distortion metric below is + // a per-vertex average by construction and keeps using `uv`. + const std::vector corner_uv = compute_layer_corner_uvs(patch, *layer); + const bool have_corner_uvs = have_uvs && corner_uv.size() == patch.indices.size() * 3; + + // Per-vertex area distortion in [0,1] (0.5 == ideal), only when both requested and possible. + std::vector distortion(patch.vertices.size(), 0.5f); + if (m_uv_check_mode == UVCheckMode::Distortion && have_uvs) { + std::vector tri_log(patch.indices.size(), 0.f); + for (size_t f = 0; f < patch.indices.size(); ++f) { + const stl_triangle_vertex_indices &t = patch.indices[f]; + const float a3 = 0.5f * (patch.vertices[t[1]] - patch.vertices[t[0]]).cross(patch.vertices[t[2]] - patch.vertices[t[0]]).norm(); + const Vec2f e0 = uv[t[1]] - uv[t[0]]; + const Vec2f e1 = uv[t[2]] - uv[t[0]]; + const float a2 = 0.5f * std::abs(e0.x() * e1.y() - e0.y() * e1.x()); + tri_log[f] = (a3 > 1e-12f && a2 > 1e-12f) ? std::log2(a2 / a3) : 0.f; + } + // Centre the heatmap on the patch's own median stretch, so a globally-scaled unwrap reads as + // uniformly "ideal" and only *relative* stretching (the thing that matters) shows up as colour. + std::vector sorted = tri_log; + float median = 0.f; + if (!sorted.empty()) { + std::nth_element(sorted.begin(), sorted.begin() + sorted.size() / 2, sorted.end()); + median = sorted[sorted.size() / 2]; + } + std::vector sum(patch.vertices.size(), 0.f); + std::vector cnt(patch.vertices.size(), 0); + for (size_t f = 0; f < patch.indices.size(); ++f) { + // +/- 2 stops (4x stretch either way) spans the full blue->red range. + const float d = std::clamp(0.5f + (tri_log[f] - median) / 4.f, 0.f, 1.f); + for (int k = 0; k < 3; ++k) { + sum[patch.indices[f][k]] += d; + ++cnt[patch.indices[f][k]]; + } + } + for (size_t v = 0; v < distortion.size(); ++v) + if (cnt[v] > 0) + distortion[v] = sum[v] / float(cnt[v]); + } + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3T2 }; + // Flat (one vertex per triangle corner), so each triangle can carry its own island's uv - see + // have_corner_uvs above. Costs nothing in shading quality: the overlay shades from uv and the + // interpolated distortion value alone, never from a per-vertex normal. + init_data.reserve_vertices(patch.indices.size() * 3); + init_data.reserve_indices(patch.indices.size() * 3); + unsigned vcount = 0; + for (size_t f = 0; f < patch.indices.size(); ++f) { + const stl_triangle_vertex_indices &tri = patch.indices[f]; + for (int k = 0; k < 3; ++k) { + const size_t vi = size_t(tri[k]); + init_data.add_vertex(patch.vertices[vi], Vec3f(distortion[vi], 0.f, 0.f), + have_corner_uvs ? corner_uv[f * 3 + size_t(k)] : + (have_uvs ? uv[vi] : Vec2f::Zero())); + } + init_data.add_triangle(vcount, vcount + 1, vcount + 2); + vcount += 3; + } + + m_uvcheck_glmodel.init_from(std::move(init_data)); +} + +void GLGizmoTextureDisplacement::render_uvcheck_mesh() +{ + const ModelObject *mo = m_c->selection_info()->model_object(); + const ModelVolume *mv = texture_volume(); + if (mo == nullptr || mv == nullptr || !m_uvcheck_glmodel.is_initialized()) + return; + const TextureDisplacementLayer *layer = active_layer(); + if (layer == nullptr) + return; + GLShaderProgram *shader = wxGetApp().get_shader("texture_displacement_uvcheck"); + if (shader == nullptr) + return; + + const Selection &selection = m_parent.get_selection(); + const Transform3d trafo_matrix = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix(); + const Camera &camera = wxGetApp().plater()->get_camera(); + + shader->start_using(); + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * trafo_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + shader->set_uniform("volume_world_matrix", trafo_matrix); + const ClippingPlaneDataWrapper clp_data = this->get_clipping_plane_data(); + shader->set_uniform("clipping_plane", clp_data.clp_dataf); + shader->set_uniform("z_range", clp_data.z_range); + // The shader works on world-space normals (it projects the texture in world millimetres, as the + // bake does), so the normal matrix is the view rotation alone - no model part. + const Matrix3d view_normal_matrix = camera.get_view_matrix().matrix().block(0, 0, 3, 3); + shader->set_uniform("view_normal_matrix", view_normal_matrix); + shader->set_uniform("volume_mirrored", trafo_matrix.matrix().determinant() < 0.0); + shader->set_uniform("tex_anchor", Vec3f(trafo_matrix.translation().cast())); // see the preview shader + shader->set_uniform("mode", m_uv_check_mode == UVCheckMode::Distortion ? 1 : 0); + shader->set_uniform("checker_freq", 4.f); // squares per texture tile + shader->set_uniform("tiling_scale", layer->tiling_scale); + shader->set_uniform("rotation_rad", layer->rotation_deg * float(M_PI) / 180.f); + shader->set_uniform("uv_offset", layer->offset); + shader->set_uniform("use_vertex_uv", m_uvcheck_uses_vertex_uv); + shader->set_uniform("projection_mode", m_uvcheck_projection_mode); + shader->set_uniform("patch_center", m_uvcheck_patch_center); + shader->set_uniform("patch_axis", m_uvcheck_patch_axis); + // Was never uploaded, so the checker disagreed with the bake for any non-square height map. + shader->set_uniform("tex_aspect", layer_texture_aspect(*layer)); + + // Coincident with the base surface, so pull it toward the camera to win the depth test. + glsafe(::glEnable(GL_POLYGON_OFFSET_FILL)); + glsafe(::glPolygonOffset(-1.0f, -1.0f)); + m_uvcheck_glmodel.render(); + glsafe(::glDisable(GL_POLYGON_OFFSET_FILL)); + shader->stop_using(); +} + +void GLGizmoTextureDisplacement::build_wireframe_from_its(const indexed_triangle_set &its) +{ + m_wireframe_overlay_glmodel.reset(); + m_wireframe_overlay_vcount = its.vertices.size(); + if (its.indices.empty()) + return; + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(its.vertices.size()); + init_data.reserve_indices(its.indices.size() * 6); + for (const Vec3f &v : its.vertices) + init_data.add_vertex(v); + // One segment per triangle edge; shared edges drawn twice, harmless for a wireframe and far + // cheaper than deduplicating a million of them. + for (const stl_triangle_vertex_indices &tri : its.indices) + for (int i = 0; i < 3; ++i) + init_data.add_line(unsigned(tri[i]), unsigned(tri[(i + 1) % 3])); + + if (!init_data.is_empty()) + m_wireframe_overlay_glmodel.init_from(std::move(init_data)); +} + +void GLGizmoTextureDisplacement::rebuild_wireframe_overlay() +{ + if (!m_wireframe_overlay) { + m_wireframe_overlay_glmodel.reset(); + m_wireframe_overlay_vcount = 0; + return; + } + const ModelVolume *mv = texture_volume(); + if (mv == nullptr) + return; + const indexed_triangle_set &its = mv->mesh().its; + if (its.indices.empty()) + return; + + // Building from the base mesh (shaded/paint mode); its topology only changes on bake/subdivide, and + // this runs on every rebuild_preview(), so rebuild only when the vertex count actually changes. + if (m_wireframe_overlay_glmodel.is_initialized() && m_wireframe_overlay_vcount == its.vertices.size()) + return; + build_wireframe_from_its(its); +} + +void GLGizmoTextureDisplacement::refresh_wireframe() +{ + if (!m_wireframe_overlay) { + m_wireframe_overlay_glmodel.reset(); + m_wireframe_overlay_vcount = 0; + return; + } + // The wireframe has to sit on whatever mesh is actually on screen. In the true-displacement view + // that is the raised preview geometry (m_preview_its) - drawing the flat base mesh's edges there + // leaves them buried inside the relief, which is why the wireframe "didn't show in real mode". In + // Fast (shaded) mode or with nothing painted, the surface is the undisplaced base mesh. + if (!m_use_shaded_preview && !m_preview_its.indices.empty()) + build_wireframe_from_its(m_preview_its); + else + rebuild_wireframe_overlay(); +} + +void GLGizmoTextureDisplacement::render_wireframe_overlay() +{ + const ModelObject *mo = m_c->selection_info()->model_object(); + const ModelVolume *mv = texture_volume(); + if (mo == nullptr || mv == nullptr || !m_wireframe_overlay_glmodel.is_initialized()) + return; + GLShaderProgram *shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + const Selection &selection = m_parent.get_selection(); + const Transform3d trafo_matrix = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix(); + const Camera &camera = wxGetApp().plater()->get_camera(); + + shader->start_using(); + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * trafo_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + // Pull the lines toward the camera so they sit on the surface rather than z-fighting into it. + glsafe(::glEnable(GL_POLYGON_OFFSET_LINE)); + glsafe(::glPolygonOffset(-1.0f, -1.0f)); + m_wireframe_overlay_glmodel.set_color(ColorRGBA(1.0f, 1.0f, 1.0f, 0.6f)); // white, so it reads on any material + m_wireframe_overlay_glmodel.render(); + glsafe(::glDisable(GL_POLYGON_OFFSET_LINE)); + shader->stop_using(); +} + +void GLGizmoTextureDisplacement::rebuild_preview() +{ + // Raised first: any in-flight job's result (captured generation from before this call) will + // now compare unequal to m_preview_generation and be discarded when it completes, even if it + // finishes after the job queued below - and, since the counter is shared with the worker, that + // job also notices mid-run and aborts rather than computing a result nobody will use. + m_preview_generation->fetch_add(1); + update_uv_editor(); + rebuild_shaded_preview_mesh(); + rebuild_paint_overlay(); + rebuild_uvcheck_mesh(); + rebuild_seam_overlay(); + // The adaptive subdivision preview is driven by the painted area, so it has to follow the paint + // while it is open - a plain stroke changes which triangles would be refined. The uniform preview + // depends only on the mesh, so it is left to its own controls. + if (m_subdivide_editing && m_subdivide_adaptive) + rebuild_subdivide_preview(); + + // The debug view owns m_preview_glmodel while it is up. A stage is a snapshot of a run that has + // already finished, so letting the live preview overwrite it would replace the thing being + // inspected with something else; leaving the view is explicit (its Close button). + if (m_debug_stage >= 0) + return; + + const ModelVolume *mv = texture_volume(); + if (mv == nullptr || !mv->is_texture_displacement_painted()) { + m_preview_glmodel.reset(); + m_preview_its = indexed_triangle_set{}; // no displaced mesh; wireframe falls back to the base + m_preview_job_pending = false; + refresh_wireframe(); + return; + } + // In Fast/paint modes the wireframe follows the base mesh and can be built now; the true-displacement + // view's wireframe needs the displaced mesh, which only exists once the job below completes. + if (m_use_shaded_preview) { + refresh_wireframe(); + // Fast view: the shader *is* the preview, and m_preview_glmodel is never drawn. Running the + // full CPU displacement anyway - which is what happened on every stroke and slider release - + // was the single largest cost in the gizmo, and it bought nothing. The switch back to the + // true-displacement view queues it (see the View row in on_render_input_window()). + m_preview_job_pending = false; + return; + } + + queue_preview_job(); +} + +void GLGizmoTextureDisplacement::queue_preview_job() +{ + // A job in flight when the gizmo closes still runs its completion handler, which would otherwise + // happily queue the follow-up run it was holding - against a gizmo nobody is looking at any more. + if (m_state != On) + return; + const ModelVolume *mv = texture_volume(); + if (mv == nullptr || !mv->is_texture_displacement_painted()) + return; + + // One in flight at a time; everything requested meanwhile collapses into a single follow-up run + // issued from the completion handler. See m_preview_job_running. + if (m_preview_job_running) { + m_preview_job_pending = true; + return; + } + + const uint64_t generation = m_preview_generation->load(); + + TextureDisplacementPreviewInput input; + input.base_mesh = mv->mesh().its; + input.layers = mv->texture_displacement_layers; + input.options = mv->texture_displacement_options; + // The preview is looked at, not printed or sliced: skip the simplification and the repair that + // follows it, which are most of the one-run pipeline's time. The relief is the same. + if (input.options.pipeline_v2) + input.options.v2_max_triangles_k = 0; + input.volume_to_world = texture_displacement_volume_to_world(*mv); + for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) + input.facets_data[size_t(i)] = mv->texture_displacement_facet(i).get_data(); + // Captured here rather than read in the handler: get_extruders_colors() is main-thread state and + // the preview has to be grouped against the same palette it was computed with, not whatever is + // loaded by the time it lands. + input.color = color_settings_for(*mv); + // The filament list the result's indices refer to, captured with the job rather than read back + // when it lands - loading a filament meanwhile must not recolour a preview computed against a + // different list. + const std::vector filaments = m_palette_filaments; + + m_preview_job_running = true; + auto &worker = wxGetApp().plater()->get_ui_job_worker(); + queue_job(worker, std::make_unique(std::move(input), generation, m_preview_generation, + [this, filaments](TextureDisplacementPreviewResult result, uint64_t result_generation) { + indexed_triangle_set its = std::move(result.mesh); + m_preview_job_running = false; + if (result_generation != m_preview_generation->load()) { + // Superseded while this was computing (it will have aborted early and come back + // empty). Whatever the newest state is, it still needs a run. + m_preview_job_pending = true; + } else if (its.indices.empty()) { + // Aborted or cancelled rather than finished - the handler runs on every outcome so + // the in-flight latch above always clears. Keep whatever preview is already on screen + // rather than blanking it; there is no new result to show, not a new empty one. + } else { + m_preview_glmodel.reset(); + m_preview_color_runs.clear(); + if (result.triangle_color.size() == its.indices.size() && !filaments.empty()) { + // Group by *filament*, not by palette entry: what the bake wrote is the resolved + // filament, interleaving already applied, so this shows the real banding rather + // than the flat average the eye will turn it into. + indexed_triangle_set sorted; + sorted.vertices = its.vertices; + sorted.indices.reserve(its.indices.size()); + for (int want = 0; want <= int(filaments.size()); ++want) { + const size_t first = sorted.indices.size(); + for (size_t i = 0; i < its.indices.size(); ++i) + if (int(result.triangle_color[i]) == want) + sorted.indices.push_back(its.indices[i]); + if (sorted.indices.size() == first) + continue; + m_preview_color_runs.push_back( + { { first * 3, sorted.indices.size() * 3 }, + want == 0 ? GLVolume::NEUTRAL_COLOR : filaments[size_t(want - 1)] }); + } + m_preview_glmodel.init_from(sorted); + } else { + m_preview_glmodel.init_from(its); + } + m_preview_glmodel.set_color(GLVolume::NEUTRAL_COLOR); + // Keep the displaced mesh so the wireframe overlay can be drawn on it (the + // true-displacement view), then refresh the wireframe from it. + m_preview_its = std::move(its); + refresh_wireframe(); + // The paint tint rides the displaced surface in this view, so it follows the new mesh. + m_paint_overlay_dirty = true; + } + if (m_preview_job_pending) { + m_preview_job_pending = false; + if (!m_use_shaded_preview) + queue_preview_job(); // no-ops if the gizmo has closed in the meantime + } + m_parent.set_as_dirty(); + })); +} + +void GLGizmoTextureDisplacement::update_uv_editor() +{ + Plater *plater = wxGetApp().plater(); + UVEditorCanvas *uv_canvas = plater->get_uv_editor_canvas(); + if (uv_canvas == nullptr) + return; + // Wired on every call rather than with the first unwrap: Unwrap itself is one of the pane's commands. + uv_canvas->set_command_callback([this](UVEditorCanvas::Command cmd, float value) { on_uv_command(int(cmd), value); }); + + const ModelVolume *mv = texture_volume(); + TextureDisplacementLayer *layer = active_layer(); + // The pane is opened only on the user's explicit request (m_show_uv_editor), and only for an LSCM + // layer - never automatically just because something is painted. Keep the cached state/unwrap so + // that switching the toggle back on re-shows instantly (and re-solves if the paint changed while + // it was hidden, via the state comparison below). + if (!m_show_uv_editor || mv == nullptr || layer == nullptr || + layer->projection_method != TextureProjectionMethod::LSCM) { + m_uv_editor_bg = UVBackground::None; // the next time it is shown, upload the background afresh + push_uv_pane_state(); + plater->show_uv_editor(false); + return; + } + + // A vertex/edge edit committing (or an undo reverting one) changes the per-vertex UV overrides + // without going through the Unwrap button. Detect that and force a re-solve, so the pane's geometry + // stays in step with what will bake - the one exception to "only re-solve on Unwrap". + { + size_t sig = 1469598103934665603ull; // FNV-1a seed + const auto mix = [&sig](uint64_t x) { sig = (sig ^ x) * 1099511628211ull; }; + mix(layer->lscm_uv_overrides.size()); + for (const auto &[v, uv] : layer->lscm_uv_overrides) { + mix(uint64_t(uint32_t(v))); + mix(uint64_t(uint32_t(int32_t(std::llround(uv.x() * 1024.f))))); + mix(uint64_t(uint32_t(int32_t(std::llround(uv.y() * 1024.f))))); + } + if (sig != m_uv_overrides_sig) { + m_uv_overrides_sig = sig; + m_uv_unwrap_pending = true; + } + } + + UVEditorState state; + state.slot = m_active_layer_slot; + state.image_data = layer->image_data.get(); + state.seam_angle = layer->lscm_seam_angle_deg; + state.padding = layer->island_padding_mm; + state.facets = mv->texture_displacement_facet(m_active_layer_slot).get_data(); + state.seam_edges = layer->lscm_seam_edges; + + // The re-solve happens only when the user pressed "Unwrap" (m_uv_unwrap_pending). Every other call + // into here - a paint stroke ending, a slider release, the check mode changing - must not pay for + // a fresh LSCM solve; it just re-applies the cheap affine transforms over whatever unwrap already + // exists. If the paint changed underneath but the user hasn't asked to re-unwrap, the pane keeps + // showing the last unwrap on purpose (that is the whole point of making it an explicit action). + bool unwrap_changed = false; + if (m_uv_unwrap_pending) { + m_uv_unwrap_pending = false; + // World millimetres, the space the bake unwraps in - otherwise the pane would lay the islands + // out at the volume's own scale and show the texture at a different size than it bakes at. + const indexed_triangle_set patch = patch_in_world(extract_painted_patch(mv->mesh().its, state.facets)); + if (patch.indices.empty()) { + // Nothing painted to unwrap: clear what the pane showed but keep it open - its status line says what to do. + m_uv_editor_state = UVEditorState{}; + m_uv_editor_unwrap = PatchUnwrap{}; + m_uv_editor_distortion_colors.clear(); + uv_canvas->set_islands({}); + push_uv_pane_state(); + plater->show_uv_editor(true); + return; + } + // Padding disabled (0): the user asked to pack islands with no gap between them. + m_uv_editor_unwrap = compute_patch_unwrap(patch, layer->lscm_seam_angle_deg, 0.f, layer->lscm_seam_edges); + m_island_overlay_glmodel.reset(); // the islands were renumbered: rebuilt from the pane's selection next frame + m_island_overlay_selection.clear(); + // Re-apply any stored UV edits onto the fresh unwrap, so the pane shows exactly what + // compute_lscm_uvs() will bake (which applies the same overrides). + apply_lscm_uv_overrides(m_uv_editor_unwrap, layer->lscm_uv_overrides); + m_uv_editor_state = std::move(state); + unwrap_changed = true; + // Precompute the distortion heatmap now, while the patch is in hand - relative stretch doesn't + // change when islands are only moved, so this need not be redone on a drag. It is fed to the + // canvas below only while the Distortion check mode is on. + compute_uv_editor_distortion_colors(patch); + } + + // The pane background either mirrors the height texture (default) or shows a UV checker (#7). It is uploaded + // before, and independently of, the unwrap - the texture belongs on screen before anything is unwrapped - and + // only when the choice, the layer's image or its smoothing changed, because the height image is large. + const UVBackground desired_bg = (m_uv_check_mode == UVCheckMode::Checker) ? UVBackground::Checker : UVBackground::Height; + const bool bg_smoothing_changed = (desired_bg == UVBackground::Height) && (m_uv_editor_bg_smoothing != layer->smoothing); + if (desired_bg != m_uv_editor_bg || layer->image_data.get() != m_uv_editor_bg_image || bg_smoothing_changed) { + m_uv_editor_bg_smoothing = layer->smoothing; + m_uv_editor_bg_image = layer->image_data.get(); + m_uv_editor_bg = desired_bg; + if (desired_bg == UVBackground::Checker) { + // An even squares-per-axis count so the pattern tiles seamlessly across the UV unit + // boundary (texcoord == position repeats it once per tile). Softened grays, not pure + // black/white, so it doesn't fight the island wires drawn over it. + constexpr int tex = 512, squares = 8, cell = tex / squares; + std::vector checker(size_t(tex) * size_t(tex)); + for (int y = 0; y < tex; ++y) + for (int x = 0; x < tex; ++x) + checker[size_t(y) * tex + x] = ((x / cell + y / cell) & 1) ? 205 : 70; + uv_canvas->set_background_texture(checker, tex, tex); + } else { + const DecodedHeightTexture height = decode_height_texture(*layer); + if (!height.empty()) { + uv_canvas->set_background_texture(height.pixels, height.width, height.height); + } else { + // Not recorded as uploaded, so the next update tries again rather than keeping a blank backdrop. + uv_canvas->set_background_texture({}, 0, 0); + m_uv_editor_bg = UVBackground::None; + } + } + } + // The backdrop's extent and repeat follow the tile settings, with or without an unwrap. + uv_canvas->set_uv_transform(layer->tiling_scale, layer->rotation_deg, layer->tile_enabled, + layer->tile_method == TextureTileMethod::MirroredRepeat); + + if (m_uv_editor_unwrap.empty()) { + // Nothing unwrapped yet: the pane opens anyway, because Unwrap itself lives in it. Its canvas shows the + // texture and its status line says to paint the area and press Unwrap. + push_uv_pane_state(); + plater->show_uv_editor(true); + return; + } + + // Grow (never shrink) the layer's island list to cover every chart. Shrinking would throw away a + // hand placement the moment a stroke temporarily merged two islands, and a stale extra entry is + // harmless - island_transform_matrix() only looks up the charts that actually exist. + if (layer->islands.size() < size_t(m_uv_editor_unwrap.chart_count)) + layer->islands.resize(size_t(m_uv_editor_unwrap.chart_count)); + + // Connected-net layout (on by default): a *fresh* unwrap is unfolded so adjacent charts sit + // edge-to-edge (cube -> a net), rather than as separately packed squares. Only when the user pressed + // Unwrap (m_uv_apply_connected_net) - a re-segmentation renumbers charts anyway, so any hand + // placement from before is already meaningless. A *refresh* re-solve (a committed vertex edit, or an + // undo) must NOT relayout, or it would throw away every island placement on every vertex edit. + if (unwrap_changed && m_uv_apply_connected_net && layer->auto_connect_islands) { + std::vector net = compute_connected_net(m_uv_editor_unwrap); + if (net.size() == size_t(m_uv_editor_unwrap.chart_count)) { + if (layer->islands.size() < net.size()) + layer->islands.resize(net.size()); + for (size_t i = 0; i < net.size(); ++i) + layer->islands[i] = net[i]; + } + } + if (unwrap_changed) + m_uv_apply_connected_net = false; // consumed; a refresh re-solve leaves placements alone + + // The geometry goes over in the unwrap's *raw* mm coordinates and is only re-uploaded when the + // unwrap itself changed. Everything a slider or a drag can touch - island placement, tiling, + // rotation, offset - is an affine map on top of that, so it goes over as one 2x3 matrix per + // island instead. That is the whole reason dragging an island is now free: a patch of a million + // triangles has a million UVs to re-transform and re-upload otherwise, and it was doing exactly + // that on every single mouse-move event. + if (unwrap_changed) { + UVEditorCanvas::Islands view; + view.uvs = m_uv_editor_unwrap.uvs; + view.indices = m_uv_editor_unwrap.indices; + view.vertex_island = m_uv_editor_unwrap.vertex_chart; + view.boundary_edges = m_uv_editor_unwrap.boundary_edges; + view.island_count = m_uv_editor_unwrap.chart_count; + + uv_canvas->set_island_edit_callback( + [this](int island, const Vec2f &offset_delta, float rotation_delta, float scale_factor, bool finished) { + on_island_edited(island, offset_delta, rotation_delta, scale_factor, finished); + }); + uv_canvas->set_vertex_edit_callback( + [this](const std::vector> &edits) { on_uv_vertex_edited(edits); }); + uv_canvas->set_islands(std::move(view)); + } + + uv_canvas->set_select_mode(static_cast(m_uv_select_mode)); + uv_canvas->set_island_transforms(uv_editor_island_transforms(*layer)); + // The distortion heatmap tints the island fills only while its check mode is on; otherwise the + // canvas falls back to its default light-green wash. + if (m_uv_check_mode == UVCheckMode::Distortion) + uv_canvas->set_island_fill_colors(m_uv_editor_distortion_colors); + else + uv_canvas->set_island_fill_colors({}); + + push_uv_pane_state(); + plater->show_uv_editor(true); +} + +void GLGizmoTextureDisplacement::compute_uv_editor_distortion_colors(const indexed_triangle_set &patch) +{ + m_uv_editor_distortion_colors.clear(); + const PatchUnwrap &u = m_uv_editor_unwrap; + if (u.empty() || u.chart_count <= 0) + return; + + // log2(uv area / 3D area) per unwrap triangle - the same measure the 3D distortion overlay uses. + std::vector tri_log(u.indices.size(), 0.f); + std::vector tri_chart(u.indices.size(), -1); + for (size_t f = 0; f < u.indices.size(); ++f) { + const stl_triangle_vertex_indices &t = u.indices[f]; + if (t[0] < 0 || size_t(t[0]) >= u.vertex_chart.size()) + continue; + tri_chart[f] = u.vertex_chart[size_t(t[0])]; + const auto p3 = [&](int uv_idx) -> Vec3f { + const int sv = (size_t(uv_idx) < u.source_vertex.size()) ? u.source_vertex[size_t(uv_idx)] : -1; + return (sv >= 0 && size_t(sv) < patch.vertices.size()) ? patch.vertices[size_t(sv)] : Vec3f::Zero(); + }; + const float a3 = 0.5f * (p3(t[1]) - p3(t[0])).cross(p3(t[2]) - p3(t[0])).norm(); + const Vec2f e0 = u.uvs[size_t(t[1])] - u.uvs[size_t(t[0])]; + const Vec2f e1 = u.uvs[size_t(t[2])] - u.uvs[size_t(t[0])]; + const float a2 = 0.5f * std::abs(e0.x() * e1.y() - e0.y() * e1.x()); + tri_log[f] = (a3 > 1e-12f && a2 > 1e-12f) ? std::log2(a2 / a3) : 0.f; + } + + // Centre on the median stretch, so a globally scaled unwrap reads as uniform and only *relative* + // stretching shows up - matching the 3D overlay's convention. + std::vector sorted = tri_log; + float median = 0.f; + if (!sorted.empty()) { + std::nth_element(sorted.begin(), sorted.begin() + sorted.size() / 2, sorted.end()); + median = sorted[sorted.size() / 2]; + } + + std::vector chart_sum(size_t(u.chart_count), 0.0); + std::vector chart_cnt(size_t(u.chart_count), 0); + for (size_t f = 0; f < tri_log.size(); ++f) { + const int c = tri_chart[f]; + if (c >= 0 && c < u.chart_count) { + chart_sum[size_t(c)] += tri_log[f]; + ++chart_cnt[size_t(c)]; + } + } + + // Blue (compressed) -> green (ideal) -> red (stretched), +/- 2 stops spanning the full range. + const auto heat = [](float t) -> ColorRGBA { + t = std::clamp(t, 0.f, 1.f); + const ColorRGBA blue{ 0.15f, 0.35f, 1.0f, 0.5f }, green{ 0.2f, 0.9f, 0.3f, 0.5f }, red{ 1.0f, 0.2f, 0.15f, 0.5f }; + if (t < 0.5f) { const float s = t * 2.f; return blue * (1.f - s) + green * s; } + const float s = (t - 0.5f) * 2.f; return green * (1.f - s) + red * s; + }; + + m_uv_editor_distortion_colors.resize(size_t(u.chart_count), heat(0.5f)); + for (int c = 0; c < u.chart_count; ++c) + if (chart_cnt[size_t(c)] > 0) { + const float avg = float(chart_sum[size_t(c)] / chart_cnt[size_t(c)]); + m_uv_editor_distortion_colors[size_t(c)] = heat(std::clamp(0.5f + (avg - median) / 4.f, 0.f, 1.f)); + } +} + +void GLGizmoTextureDisplacement::on_uv_command(int cmd, float value) +{ + m_uv_command_queue.emplace_back(cmd, value); + m_parent.set_as_dirty(); // the panel render that runs it +} + +void GLGizmoTextureDisplacement::process_uv_commands() +{ + std::vector> queue; + queue.swap(m_uv_command_queue); + for (const auto &[cmd, value] : queue) + run_uv_command(cmd, value); +} + +void GLGizmoTextureDisplacement::apply_view_mode(int mode) +{ + m_use_shaded_preview = (mode == 1); + m_uv_check_mode = (mode == 2) ? UVCheckMode::Checker : + (mode == 3) ? UVCheckMode::Distortion : UVCheckMode::None; + rebuild_uvcheck_mesh(); + if (m_use_shaded_preview) + rebuild_shaded_preview_mesh(); + else + // The Fast view skips the CPU displacement entirely (see rebuild_preview()), so leaving it means + // m_preview_glmodel may be stale or absent - ask for it now. + queue_preview_job(); + // ...and the paint tint between the base and the displaced surface, for the same reason. + m_paint_overlay_dirty = true; + refresh_wireframe(); // Normal<->Fast swaps the wireframe between displaced and base mesh + update_uv_editor(); // mirror the checker / distortion heatmap into the UV pane too + m_parent.set_as_dirty(); +} + +void GLGizmoTextureDisplacement::push_uv_pane_state() +{ + UVEditorCanvas *canvas = wxGetApp().plater()->get_uv_editor_canvas(); + if (canvas == nullptr) + return; + + UVEditorCanvas::PaneState st; + const ModelVolume *mv = texture_volume(); + const TextureDisplacementLayer *layer = active_layer(); + if (mv != nullptr && layer != nullptr && layer->projection_method == TextureProjectionMethod::LSCM) { + st.has_layer = true; + st.layer_name = from_u8(layer->name.empty() ? Slic3r::format(_u8L("Layer %1%"), layer->slot + 1) : layer->name); + st.tile_mm = layer->tiling_scale; + st.seam_angle_deg = layer->lscm_seam_angle_deg; + st.connect_islands = layer->auto_connect_islands; + st.has_seams = !layer->lscm_seam_edges.empty(); + st.has_uv_edits = !layer->lscm_uv_overrides.empty(); + + // Retried while empty: the image may not decode yet, and a blank thumbnail must not stick. + if (layer->image_data.get() != m_uv_thumb_source || m_uv_thumb_rgb.empty()) { + m_uv_thumb_source = nullptr; + m_uv_thumb_rgb.clear(); + const DecodedHeightTexture tex = decode_height_texture(*layer); + if (!tex.empty()) { + m_uv_thumb_source = layer->image_data.get(); + // Box-filtered, so a fine pattern such as bricks averages out instead of aliasing into noise. + const int n = UV_THUMB_PX; + const bool color = tex.has_color(); + m_uv_thumb_rgb.resize(size_t(n) * n * 3); + for (int y = 0; y < n; ++y) { + const int y0 = y * tex.height / n, y1 = std::max(y0 + 1, (y + 1) * tex.height / n); + for (int x = 0; x < n; ++x) { + const int x0 = x * tex.width / n, x1 = std::max(x0 + 1, (x + 1) * tex.width / n); + uint64_t sum[3]{}; + for (int sy = y0; sy < y1; ++sy) + for (int sx = x0; sx < x1; ++sx) { + const size_t src = size_t(sy) * size_t(tex.width) + size_t(sx); + for (int ch = 0; ch < 3; ++ch) + sum[ch] += color ? tex.rgb[src * 3 + ch] : tex.pixels[src]; + } + const uint64_t count = uint64_t(y1 - y0) * uint64_t(x1 - x0); + for (int ch = 0; ch < 3; ++ch) + m_uv_thumb_rgb[size_t(y * n + x) * 3 + ch] = (unsigned char) (sum[ch] / count); + } + } + } + } + if (!m_uv_thumb_rgb.empty()) { + st.thumbnail_px = UV_THUMB_PX; + st.thumbnail_rgb = m_uv_thumb_rgb; + } + + if (!m_uv_editor_unwrap.empty()) { + // Stale when the unwrap on screen was made from another layer, other paint, other seams or another + // seam angle than this layer has now - the same things update_uv_editor() re-solves for. + UVEditorState now; + now.slot = m_active_layer_slot; + now.image_data = layer->image_data.get(); + now.seam_angle = layer->lscm_seam_angle_deg; + now.padding = layer->island_padding_mm; + now.facets = mv->texture_displacement_facet(m_active_layer_slot).get_data(); + now.seam_edges = layer->lscm_seam_edges; + st.unwrapped = true; + st.unwrap_stale = !(now == m_uv_editor_state); + st.island_count = m_uv_editor_unwrap.chart_count; + st.face_count = m_uv_editor_unwrap.indices.size(); + } + } + st.mark_seams = m_seam_edit_mode; + st.seam_path = m_seam_path_mode; + st.background = m_uv_check_mode == UVCheckMode::Checker ? UVEditorCanvas::Background::Checker : + m_uv_check_mode == UVCheckMode::Distortion ? UVEditorCanvas::Background::Distortion : + UVEditorCanvas::Background::Height; + canvas->set_pane_state(std::move(st)); +} + +void GLGizmoTextureDisplacement::run_uv_command(int cmd, float value) +{ + using Command = UVEditorCanvas::Command; + if (cmd == int(Command::PaneClosed)) { + // Closed with the pane's own X: keep it closed until asked again, and upload the background afresh then. + m_show_uv_editor = false; + m_uv_editor_bg = UVBackground::None; + // The seam tool belongs to the pane: left on with the pane gone, every stroke on the model would + // be swallowed as a seam click and nothing would paint. + if (m_seam_edit_mode) { + m_seam_edit_mode = false; + m_seam_hover_edge = { -1, -1 }; + m_seam_hover_vertex = -1; + m_seam_hover_glmodel.reset(); + m_seam_path_anchor = -1; + m_seam_anchor_glmodel.reset(); + push_uv_pane_state(); + } + return; + } + if (cmd == int(Command::SetBackground)) { + // Height goes back to whichever of Normal / Fast was showing; Checker and Distortion are views of their own. + const int background = std::clamp(int(std::lround(value)), 0, 2); + apply_view_mode(background == 1 ? 2 : background == 2 ? 3 : (m_use_shaded_preview ? 1 : 0)); + return; + } + + TextureDisplacementLayer *layer = active_layer(); + if (layer == nullptr) + return; + + if (cmd == int(UVEditorCanvas::Command::AverageScale)) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Average island scale"), UndoRedo::SnapshotType::GizmoAction); + average_island_scales(layer->islands); + rebuild_preview(); + } else if (cmd == int(UVEditorCanvas::Command::CutSelectedIsland)) { + UVEditorCanvas *canvas = wxGetApp().plater()->get_uv_editor_canvas(); + const int chart = canvas != nullptr ? canvas->selected_island() : -1; + if (chart < 0) { + show_error(nullptr, _u8L("Select an island in the UV editor first.")); + return; + } + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Cut texture island"), UndoRedo::SnapshotType::GizmoAction); + cut_island(*layer, chart); + rebuild_preview(); + } else if (cmd == int(UVEditorCanvas::Command::JoinSelected)) { + UVEditorCanvas *canvas = wxGetApp().plater()->get_uv_editor_canvas(); + const int chart = canvas != nullptr ? canvas->selected_island() : -1; + if (chart < 0) { + show_error(nullptr, _u8L("Select an island in the UV editor first.")); + return; + } + // Join to whichever neighbouring island (one it shares an edge with) is currently placed + // nearest - i.e. the one it was dragged up against. + const Eigen::Matrix sel_m = island_transform_matrix(chart, m_uv_editor_unwrap, layer->islands); + const Vec2f sel_c = sel_m.block<2, 2>(0, 0) * m_uv_editor_unwrap.chart_centroid[size_t(chart)] + sel_m.col(2); + int best_parent = -1; + float best_d2 = std::numeric_limits::max(); + TextureIsland best_place, cand; + for (int p = 0; p < m_uv_editor_unwrap.chart_count; ++p) { + if (p == chart) + continue; + if (!join_chart_placement(m_uv_editor_unwrap, layer->islands, chart, p, cand)) + continue; // not a neighbour + const Eigen::Matrix pm = island_transform_matrix(p, m_uv_editor_unwrap, layer->islands); + const Vec2f pc = pm.block<2, 2>(0, 0) * m_uv_editor_unwrap.chart_centroid[size_t(p)] + pm.col(2); + const float d2 = (pc - sel_c).squaredNorm(); + if (d2 < best_d2) { best_d2 = d2; best_parent = p; best_place = cand; } + } + if (best_parent < 0) { + show_error(nullptr, _u8L("This island has no neighbour it shares an edge with.")); + return; + } + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Join texture island"), UndoRedo::SnapshotType::GizmoAction); + if (layer->islands.size() <= size_t(chart)) + layer->islands.resize(size_t(chart) + 1); + layer->islands[size_t(chart)] = best_place; + // Record the join so the two (and anything already grouped with either) move together from now + // on, not just visually snap once. + join_island_groups(layer->island_groups, chart, best_parent, m_uv_editor_unwrap.chart_count); + rebuild_preview(); + } else if (cmd == int(UVEditorCanvas::Command::UnjoinSelected)) { + UVEditorCanvas *canvas = wxGetApp().plater()->get_uv_editor_canvas(); + const int chart = canvas != nullptr ? canvas->selected_island() : -1; + if (chart < 0 || size_t(chart) >= layer->islands.size()) { + show_error(nullptr, _u8L("Select an island in the UV editor first.")); + return; + } + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Unjoin texture island"), UndoRedo::SnapshotType::GizmoAction); + layer->islands[size_t(chart)] = TextureIsland{}; // back to its own packed position + // Break its join link too, so it stops moving with the others (the rest stay grouped). + if (size_t(chart) < layer->island_groups.size()) + layer->island_groups[size_t(chart)] = chart; + rebuild_preview(); + } + else if (cmd == int(Command::PickTexture)) { + // The same texture library the layer card opens; the panel renders it this frame. + m_picker_slot = m_active_layer_slot; + m_picker_open_request = true; + } else if (cmd == int(Command::Unwrap)) { + // The solve runs only on this - painting, the seam angle and seam marking never trigger it. + m_uv_unwrap_pending = true; + m_uv_apply_connected_net = true; // a genuine re-unwrap may relayout the islands + m_show_uv_editor = true; + update_uv_editor(); + } else if (cmd == int(Command::SetSeamAngle)) { + const float angle = std::clamp(value, 5.f, 90.f); + if (angle != layer->lscm_seam_angle_deg) { + layer->lscm_seam_angle_deg = angle; + rebuild_preview(); // the bake unwraps with it; this also marks the pane's unwrap stale + } + } else if (cmd == int(Command::SetConnectIslands)) { + const bool connect = value != 0.f; + if (connect != layer->auto_connect_islands) { + layer->auto_connect_islands = connect; + // Apply (or, when turned off, just stop re-applying) right away rather than waiting for the next re-unwrap. + if (connect && !m_uv_editor_unwrap.empty()) { + std::vector net = compute_connected_net(m_uv_editor_unwrap); + if (net.size() == size_t(m_uv_editor_unwrap.chart_count)) { + if (layer->islands.size() < net.size()) + layer->islands.resize(net.size()); + for (size_t i = 0; i < net.size(); ++i) + layer->islands[i] = net[i]; + } + } + rebuild_preview(); + } + } else if (cmd == int(Command::SetSelectMode)) { + m_uv_select_mode = std::clamp(int(std::lround(value)), 0, 2); // the canvas has already switched + } else if (cmd == int(Command::SetMarkSeams)) { + m_seam_edit_mode = value != 0.f; + if (m_seam_edit_mode) + m_adjust_texture_mode = false; // the two click modes are mutually exclusive + else { + m_seam_hover_edge = { -1, -1 }; // drop the hover highlight when leaving the mode + m_seam_hover_vertex = -1; + m_seam_hover_glmodel.reset(); + m_seam_path_anchor = -1; + m_seam_anchor_glmodel.reset(); + } + push_uv_pane_state(); + } else if (cmd == int(Command::SetSeamPath)) { + m_seam_path_mode = value != 0.f; + m_seam_path_anchor = -1; + m_seam_hover_edge = { -1, -1 }; // hover target type changes with the mode + m_seam_hover_vertex = -1; + m_seam_hover_glmodel.reset(); + m_seam_anchor_glmodel.reset(); + push_uv_pane_state(); + } else if (cmd == int(Command::ClearSeams)) { + if (!layer->lscm_seam_edges.empty()) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Clear texture seams"), UndoRedo::SnapshotType::GizmoAction); + layer->lscm_seam_edges.clear(); + rebuild_preview(); + } + } else if (cmd == int(Command::ClearUVEdits)) { + if (!layer->lscm_uv_overrides.empty()) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Clear texture UV edits"), UndoRedo::SnapshotType::GizmoAction); + layer->lscm_uv_overrides.clear(); + m_uv_unwrap_pending = true; // re-solve so the pane drops the edited coords + update_uv_editor(); + rebuild_preview(); + } + } + // FrameAll/ToggleSnap are handled inside the canvas; ProjectFromView is not wired yet. + m_parent.set_as_dirty(); +} + +void GLGizmoTextureDisplacement::capture_view_projection(TextureDisplacementLayer &layer) +{ + const ModelVolume *mv = texture_volume(); + const ModelObject *mo = m_c->selection_info()->model_object(); + if (mv == nullptr || mo == nullptr) + return; + + const Camera &camera = wxGetApp().plater()->get_camera(); + const Selection &selection = m_parent.get_selection(); + const Transform3d trafo = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix(); + + // The view matrix's rotation rows are the camera axes in world space; bring them into the + // volume's local frame (where the mesh vertices live) so the projector rides along with the part. + const Matrix3d view_rot = camera.get_view_matrix().matrix().block<3, 3>(0, 0); + const Matrix3d trafo_rot = trafo.matrix().block<3, 3>(0, 0); + const Matrix3d world_to_local = trafo_rot.inverse(); + const Vec3d local_right = world_to_local * Vec3d(view_rot.row(0).transpose()); + const Vec3d local_up = world_to_local * Vec3d(view_rot.row(1).transpose()); + + // Unit axes: the projected planar coordinate must stay in mm so tiling_scale keeps meaning mm. + layer.view_project_right = local_right.norm() > 1e-9 ? Vec3f(local_right.normalized().cast()) : Vec3f::UnitX(); + layer.view_project_up = local_up.norm() > 1e-9 ? Vec3f(local_up.normalized().cast()) : Vec3f::UnitY(); +} + +void GLGizmoTextureDisplacement::show_projector(bool show) +{ + if (!show) { + if (m_projector_frame != nullptr) + m_projector_frame->Hide(); + return; + } + + if (m_projector_frame == nullptr) { + // Parented to the main frame, not to Plater: Plater is a wxPanel, and wxFRAME_FLOAT_ON_PARENT + // wants a real top-level window to float above. + m_projector_frame = new TextureProjectorFrame(wxGetApp().mainframe); + m_projector_tex_source = nullptr; // fresh window, nothing uploaded into it yet + m_projector_tex_smoothing = -1.f; + m_projector_frame->set_opacity(m_projector_opacity); + + // Opened centred over the 3D canvas, at about half its size: the frame is meant to be + // dragged onto part of the model, so starting somewhere on top of it beats the OS's default + // cascade position, which is often off over the sidebar. + if (const wxGLCanvas *cnv = m_parent.get_wxglcanvas(); cnv != nullptr) { + const wxRect area = cnv->GetScreenRect(); + const wxSize size(std::max(160, area.width / 2), std::max(160, area.height / 2)); + m_projector_frame->SetSize(wxRect(area.GetTopLeft() + wxPoint((area.width - size.x) / 2, + (area.height - size.y) / 2), + size)); + } + } + + m_projector_frame->Show(); + m_projector_frame->Raise(); + update_projector(); +} + +void GLGizmoTextureDisplacement::update_projector() +{ + if (m_projector_frame == nullptr || !m_projector_frame->IsShown()) + return; + + const TextureDisplacementLayer *layer = active_layer(); + if (layer == nullptr || layer->projection_method != TextureProjectionMethod::ViewProjected) { + m_projector_frame->set_texture({}, 0, 0); + m_projector_tex_source = nullptr; + m_projector_tex_smoothing = -1.f; + return; + } + + // Only re-upload when the pixels actually changed - this is reached from the panel's per-edit + // flush, and rebuilding the bitmap from identical bytes every time would be pure waste. + if (m_projector_tex_source != layer->image_data.get() || m_projector_tex_smoothing != layer->smoothing) { + const DecodedHeightTexture height = decode_height_texture(*layer); + if (height.empty()) + m_projector_frame->set_texture({}, 0, 0); + else + m_projector_frame->set_texture(height.pixels, height.width, height.height); + m_projector_tex_source = layer->image_data.get(); + m_projector_tex_smoothing = layer->smoothing; + } +} + +int GLGizmoTextureDisplacement::apply_projection_frame() +{ + TextureDisplacementLayer *layer = active_layer(); + const ModelVolume *mv = texture_volume(); + const ModelObject *mo = m_c->selection_info()->model_object(); + wxGLCanvas *cnv = m_parent.get_wxglcanvas(); + if (layer == nullptr || mv == nullptr || mo == nullptr || cnv == nullptr || m_projector_frame == nullptr || + !m_projector_frame->IsShown()) + return -1; + + // The frame's gate, brought from screen coordinates into the GL viewport's pixel space. Two + // conversions, both necessary: ScreenToClient() because the viewport's origin is the canvas's + // top-left, not the desktop's, and the retina scale because the viewport is sized in physical + // pixels (see GLCanvas3D::get_canvas_size()) while wx hands out logical ones. + const wxRect gate = m_projector_frame->client_rect_on_screen(); + const wxPoint tl = cnv->ScreenToClient(gate.GetTopLeft()); + const double scale = double(m_parent.get_scale()); + const double rx = double(tl.x) * scale, ry = double(tl.y) * scale; + const double rw = double(gate.width) * scale, rh = double(gate.height) * scale; + if (rw < 1.0 || rh < 1.0) + return -1; + + const Camera &camera = wxGetApp().plater()->get_camera(); + const std::array &vp = camera.get_viewport(); + const Selection &sel = m_parent.get_selection(); + const Transform3d trafo = mo->instances[sel.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix(); + + // Local space -> clip space, the same product the renderer uses, so the mapping agrees with what + // is actually on screen rather than with an idealisation of it. + const Eigen::Matrix4d K = camera.get_projection_matrix().matrix() * camera.get_view_matrix().matrix() * trafo.matrix(); + + // Window coordinates follow igl::project's convention (as CameraUtils::project does): + // win_x = vp.x + vp.w * (ndc.x + 1) / 2, and y measured downward as vp.h - win_y_gl. + // Turning those into uv = (win - rect_origin) / rect_size gives u and v as affine functions of + // ndc.x and ndc.y, and since ndc = clip.xyz / clip.w, multiplying through by clip.w leaves a + // plain linear combination of K's rows - i.e. one 3x4 matrix carrying the perspective divide. + const double A = double(vp[2]) / (2.0 * rw); + const double B = (double(vp[0]) + double(vp[2]) * 0.5 - rx) / rw; + const double C = -double(vp[3]) / (2.0 * rh); + const double D = (double(vp[3]) * 0.5 - double(vp[1]) - ry) / rh; + + const Eigen::Vector4d row_u = A * K.row(0).transpose() + B * K.row(3).transpose(); + const Eigen::Vector4d row_v = C * K.row(1).transpose() + D * K.row(3).transpose(); + const Eigen::Vector4d row_w = K.row(3).transpose(); + + std::array m{}; + for (int i = 0; i < 4; ++i) { + m[size_t(i)] = float(row_u[i]); + m[size_t(4 + i)] = float(row_v[i]); + m[size_t(8 + i)] = float(row_w[i]); + } + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Apply texture projection frame"), + UndoRedo::SnapshotType::GizmoAction); + + layer->projection_method = TextureProjectionMethod::ViewProjected; + layer->view_project_projective = true; + layer->view_project_matrix = m; + // Off, so the height sampler returns 0 outside [0,1) and the frame's border becomes a hard edge + // of the displacement rather than the first seam of an endless repeat. + layer->tile_enabled = false; + // The affine axes are kept up to date too, so turning the projective mapping off later leaves a + // sane flat projection from this same viewpoint instead of whatever was captured long ago. + capture_view_projection(*layer); + + const int painted = select_visible_faces(&m); + m_preview_params_dirty = true; + rebuild_preview(); + m_parent.set_as_dirty(); + return painted; +} + +void GLGizmoTextureDisplacement::cut_island(TextureDisplacementLayer &layer, int chart) +{ + const ModelVolume *mv = texture_volume(); + if (mv == nullptr) + return; + const std::vector &verts = mv->mesh().its.vertices; + const PatchUnwrap &u = m_uv_editor_unwrap; + + // Collect the chart's triangles back in mesh-vertex space, and its 3D bounding box. + std::vector> tris; + Vec3f lo(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); + Vec3f hi(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); + for (const stl_triangle_vertex_indices &t : u.indices) { + if (t[0] < 0 || size_t(t[0]) >= u.vertex_chart.size() || u.vertex_chart[size_t(t[0])] != chart) + continue; + std::array bt{}; + bool ok = true; + for (int k = 0; k < 3; ++k) { + const int uvv = t[k]; + if (uvv < 0 || size_t(uvv) >= u.source_vertex.size()) { ok = false; break; } + const int base = u.source_vertex[size_t(uvv)]; + if (base < 0 || size_t(base) >= verts.size()) { ok = false; break; } + bt[k] = base; + lo = lo.cwiseMin(verts[size_t(base)]); + hi = hi.cwiseMax(verts[size_t(base)]); + } + if (ok) + tris.push_back(bt); + } + if (tris.empty()) + return; + + // Cut perpendicular to the longest axis, through the centroid - a long thin island is split + // across its narrow middle, which is exactly the "islands might be very long" case. + const Vec3f ext = hi - lo; + const int axis = (ext.x() >= ext.y() && ext.x() >= ext.z()) ? 0 : (ext.y() >= ext.z() ? 1 : 2); + const float mid = 0.5f * (lo[axis] + hi[axis]); + + std::set> seams(layer.lscm_seam_edges.begin(), layer.lscm_seam_edges.end()); + for (const std::array &bt : tris) + for (int i = 0; i < 3; ++i) { + const int a = bt[i], b = bt[(i + 1) % 3]; + // Endpoints on opposite sides of the plane -> this edge crosses it -> make it a seam. + if ((verts[size_t(a)][axis] < mid) != (verts[size_t(b)][axis] < mid)) + seams.insert({ std::min(a, b), std::max(a, b) }); + } + layer.lscm_seam_edges.assign(seams.begin(), seams.end()); +} + +// unwrap mm -> texture uv, per island: the island's own hand placement, then the layer's +// tiling/rotation/offset. Both are affine, so they compose into one matrix the canvas can hand +// straight to a shader. +std::vector> +GLGizmoTextureDisplacement::uv_editor_island_transforms(const TextureDisplacementLayer &layer) +{ + const float uv_scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f; + const float rad = layer.rotation_deg * float(M_PI) / 180.f; + const float cs = std::cos(rad) * uv_scale; + const float sn = std::sin(rad) * uv_scale; + + Eigen::Matrix2f uv_linear; + uv_linear << cs, -sn, + sn, cs; + + m_uv_editor_bbox_min = Vec2f(std::numeric_limits::max(), std::numeric_limits::max()); + m_uv_editor_bbox_max = Vec2f(std::numeric_limits::lowest(), std::numeric_limits::lowest()); + + std::vector> transforms(size_t(std::max(m_uv_editor_unwrap.chart_count, 0))); + for (int c = 0; c < m_uv_editor_unwrap.chart_count; ++c) { + const Eigen::Matrix island = island_transform_matrix(c, m_uv_editor_unwrap, layer.islands); + Eigen::Matrix &m = transforms[size_t(c)]; + m.block<2, 2>(0, 0) = uv_linear * island.block<2, 2>(0, 0); + m.col(2) = uv_linear * island.col(2) + layer.offset; + } + + // Only for the panel's readout; the canvas computes its own bounds. + for (size_t i = 0; i < m_uv_editor_unwrap.uvs.size(); ++i) { + const int c = m_uv_editor_unwrap.vertex_chart[i]; + if (c < 0 || size_t(c) >= transforms.size()) + continue; + const Vec2f uv = transforms[size_t(c)].block<2, 2>(0, 0) * m_uv_editor_unwrap.uvs[i] + transforms[size_t(c)].col(2); + m_uv_editor_bbox_min = m_uv_editor_bbox_min.cwiseMin(uv); + m_uv_editor_bbox_max = m_uv_editor_bbox_max.cwiseMax(uv); + } + return transforms; +} + +void GLGizmoTextureDisplacement::on_island_edited(int island, const Vec2f &offset_delta, float rotation_delta, + float scale_factor, bool finished) +{ + TextureDisplacementLayer *layer = active_layer(); + if (layer == nullptr || island < 0) + return; + if (size_t(island) >= layer->islands.size()) + layer->islands.resize(size_t(island) + 1); + + // A pure translation (no rotation, no scale) is the only gesture that moves a whole group/selection + // together; rotate and scale stay on the primary island alone. This split is what makes flagging a + // group's worth of vertices on the GPU safe: the shared shader delta is a pure translation, so the + // same offset is correct for every flagged island. + const bool is_move = rotation_delta == 0.f && scale_factor == 1.f; + + if (!m_island_drag_active) { + // Taken before the first delta lands, so one undo reverts the whole drag rather than just + // its final mouse-move. + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Move texture island"), UndoRedo::SnapshotType::GizmoAction); + m_island_drag_active = true; + // Decide the moved set once, at drag start: the whole selection + join groups for a move, or + // just the primary for a rotate/scale. + m_island_move_set = is_move ? build_island_move_set(*layer, island) : std::vector{ island }; + // Set up the GPU drag: bake the mesh once (via the dirty flag), which is also what flags the + // moved islands' triangles. From then on the drag is a uniform update, no rebuild - see + // render_shaded_preview_mesh(). + m_shaded_active_chart = island; + m_shaded_island_delta = Eigen::Matrix::Identity(); + m_shaded_preview_dirty = true; + } + + // Apply the edit. A move goes to every island in the moved set (same offset -> they translate as + // one); a rotate/scale goes only to the primary, about its own centroid. + const std::vector single{ island }; + const std::vector &targets = (is_move && !m_island_move_set.empty()) ? m_island_move_set : single; + for (int c : targets) { + if (c < 0) + continue; + if (size_t(c) >= layer->islands.size()) + layer->islands.resize(size_t(c) + 1); + TextureIsland &target = layer->islands[size_t(c)]; + target.offset += offset_delta; + if (c == island) { + target.rotation_deg += rotation_delta; + // Guarded: a scale that reaches zero is unrecoverable (every subsequent factor multiplies it) + // and would collapse the island to a point - exactly the failure this feature hit once already. + target.scale = std::clamp(target.scale * scale_factor, 0.001f, 1000.f); + } + } + + if (finished) { + m_island_drag_active = false; + m_shaded_active_chart = -1; + m_shaded_active_face.clear(); + m_island_move_set.clear(); + m_shaded_island_delta = Eigen::Matrix::Identity(); + rebuild_preview(); // the real displaced geometry moved: recompute it once, at the end + } else { + const std::vector> xf = uv_editor_island_transforms(*layer); + if (UVEditorCanvas *uv_canvas = wxGetApp().plater()->get_uv_editor_canvas()) { + // Live feedback in the pane, and deliberately *just* the transforms: nothing about the + // unwrap changed, so none of the vertex buffers need touching. This is what makes a drag + // interactive on a patch with a million triangles. + uv_canvas->set_island_transforms(xf); + } + // Move the island on the model live through the shader's island_delta uniform - no mesh + // rebuild. delta = F_current * F_baked^-1 in final-uv space. + // The one rebuild that bakes the flags is + // scheduled at drag start above and consumed once per frame by render_painter_gizmo(). + if (m_use_shaded_preview && m_shaded_active_chart == island && size_t(island) < xf.size()) { + Eigen::Matrix3f cur = Eigen::Matrix3f::Identity(); + cur.topRows<2>() = xf[size_t(island)]; + Eigen::Matrix3f bak = Eigen::Matrix3f::Identity(); + bak.topRows<2>() = m_shaded_baked_active_xf; + m_shaded_island_delta = (cur * bak.inverse()).topRows<2>(); + m_parent.set_as_dirty(); + } + } +} + +void GLGizmoTextureDisplacement::on_uv_vertex_edited(const std::vector> &edits) +{ + TextureDisplacementLayer *layer = active_layer(); + if (layer == nullptr || edits.empty() || m_uv_editor_unwrap.empty()) + return; + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Edit texture UV"), UndoRedo::SnapshotType::GizmoAction); + + for (const auto &[unwrapped, raw_uv] : edits) { + if (unwrapped < 0 || size_t(unwrapped) >= m_uv_editor_unwrap.source_vertex.size()) + continue; + // Keep the gizmo's own unwrap copy in step, so the pane and the bake agree without a re-solve. + if (size_t(unwrapped) < m_uv_editor_unwrap.uvs.size()) + m_uv_editor_unwrap.uvs[size_t(unwrapped)] = raw_uv; + + // Keyed by this one unwrapped copy, not by its mesh vertex: a seam vertex has a copy in each island it + // borders, and only the one that was dragged may move. Small list, linear scan is fine. + const int key = lscm_uv_override_key(unwrapped); + auto it = std::find_if(layer->lscm_uv_overrides.begin(), layer->lscm_uv_overrides.end(), + [key](const std::pair &p) { return p.first == key; }); + if (it != layer->lscm_uv_overrides.end()) + it->second = raw_uv; + else + layer->lscm_uv_overrides.emplace_back(key, raw_uv); + } + + rebuild_preview(); // the baked displacement samples the moved uv now +} + +bool GLGizmoTextureDisplacement::update_adjust_anchor() +{ + const ModelVolume *mv = texture_volume(); + m_adjust_anchor_valid = mv != nullptr && + compute_layer_paint_anchor(mv->mesh().its, mv->texture_displacement_facet(m_active_layer_slot).get_data(), + m_adjust_anchor_pos, m_adjust_anchor_normal); + return m_adjust_anchor_valid; +} + +TextureDisplacementLayer *GLGizmoTextureDisplacement::active_layer() +{ + ModelVolume *mv = texture_volume(); + if (mv == nullptr) + return nullptr; + for (TextureDisplacementLayer &l : mv->texture_displacement_layers) + if (l.slot == m_active_layer_slot) + return &l; + return nullptr; +} + +const TextureDisplacementLayer *GLGizmoTextureDisplacement::active_layer() const +{ + return const_cast(this)->active_layer(); +} + +Vec3f GLGizmoTextureDisplacement::adjust_plane_point() const +{ + const ModelVolume *mv = texture_volume(); + const float bbox_size = (mv != nullptr) ? float(mv->mesh().bounding_box().size().norm()) : 1.f; + const float lift = bbox_size * 0.01f + 0.2f; // clear of the surface, to avoid z-fighting + return m_adjust_anchor_pos + m_adjust_anchor_normal * lift; +} + +Vec3f GLGizmoTextureDisplacement::adjust_handle_center(const TextureDisplacementLayer &layer) const +{ + // apply_uv_transform() maps a planar mm coordinate p to uv = R(p / tiling_scale) + offset, and + // on_mouse_adjust_texture() drives offset by offset = offset_start - R(delta / tiling_scale). + // Inverting that, the handle's displacement from the anchor is - R^-1(offset) * tiling_scale - + // which, substituted into the drag equation, moves the handle by exactly `delta`. So the handle + // follows the cursor precisely, and is back on the anchor exactly when offset is zero. + const float rad = layer.rotation_deg * float(M_PI) / 180.f; + const float cs = std::cos(rad), sn = std::sin(rad); + // Undo the non-square v scaling first - it is the last thing apply_uv_transform() does, so it is + // the first thing to come off on the way back. + const float aspect = layer_texture_aspect(layer); + const Vec2f o(layer.offset.x(), (aspect > 0.f) ? layer.offset.y() / aspect : layer.offset.y()); + const Vec2f unrotated(o.x() * cs + o.y() * sn, -o.x() * sn + o.y() * cs); + const Vec2f planar = -unrotated * layer.tiling_scale; + + Vec3f u_axis, v_axis; + adjust_tangent_basis(u_axis, v_axis); + return adjust_plane_point() + u_axis * planar.x() + v_axis * planar.y(); +} + +void GLGizmoTextureDisplacement::adjust_tangent_basis(Vec3f &u_axis, Vec3f &v_axis) const +{ + // Mirrors project_planar()'s own dominant-axis choice exactly, so the ring's "0 degrees" and + // the offset handle's plane always agree with what project_texture_displacement_uv() does. + const Vec3f n = m_adjust_anchor_normal.cwiseAbs(); + if (n.x() >= n.y() && n.x() >= n.z()) { + u_axis = Vec3f::UnitY(); + v_axis = Vec3f::UnitZ(); + } else if (n.y() >= n.x() && n.y() >= n.z()) { + u_axis = Vec3f::UnitX(); + v_axis = Vec3f::UnitZ(); + } else { + u_axis = Vec3f::UnitX(); + v_axis = Vec3f::UnitY(); + } +} + +void GLGizmoTextureDisplacement::render_adjust_texture_gizmo() +{ + if (!m_adjust_anchor_valid) + return; + + const ModelObject *mo = m_c->selection_info()->model_object(); + const ModelVolume *mv = texture_volume(); + const TextureDisplacementLayer *layer = active_layer(); + if (mo == nullptr || mv == nullptr || layer == nullptr) + return; + + const Selection &selection = m_parent.get_selection(); + const Transform3d trafo_matrix = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix(); + + // Handle sizes scale with the volume so they stay usable on both tiny and huge models. + const float bbox_size = float(mv->mesh().bounding_box().size().norm()); + const float panel_half = bbox_size * 0.04f + 1.f; + const float arrow_length = panel_half * 2.2f; + + // Tracks the layer's offset, so the handle actually travels with the texture as it is dragged. + const Vec3f handle_center_local = adjust_handle_center(*layer); + + GLShaderProgram *shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + glsafe(::glDisable(GL_DEPTH_TEST)); + glsafe(::glEnable(GL_BLEND)); + shader->start_using(); + + const Camera &camera = wxGetApp().plater()->get_camera(); + + Vec3f u_axis, v_axis; + adjust_tangent_basis(u_axis, v_axis); + Transform3d plane_transform = Transform3d::Identity(); + plane_transform.linear().col(0) = u_axis.cast(); + plane_transform.linear().col(1) = v_axis.cast(); + plane_transform.linear().col(2) = m_adjust_anchor_normal.cast(); + plane_transform.translation() = handle_center_local.cast(); + + // Pan panel: a flat square lying in the patch's own tangent plane. Dragging anywhere on it + // moves the texture freely along both axes at once. + if (!m_adjust_panel_glmodel.is_initialized()) { + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(4); + init_data.reserve_indices(6); + init_data.add_vertex(Vec3f(-1.f, -1.f, 0.f)); + init_data.add_vertex(Vec3f(1.f, -1.f, 0.f)); + init_data.add_vertex(Vec3f(1.f, 1.f, 0.f)); + init_data.add_vertex(Vec3f(-1.f, 1.f, 0.f)); + init_data.add_triangle(0, 1, 2); + init_data.add_triangle(0, 2, 3); + m_adjust_panel_glmodel.init_from(std::move(init_data)); + } + Transform3d view_model_matrix = camera.get_view_matrix() * trafo_matrix * plane_transform * + Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), Vec3d(panel_half, panel_half, panel_half)); + shader->set_uniform("view_model_matrix", view_model_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + ColorRGBA panel_color = m_adjust_drag_handle == AdjustHandle::Pan ? ColorRGBA::YELLOW() : ColorRGBA::ORANGE(); + panel_color.a(0.45f); + m_adjust_panel_glmodel.set_color(panel_color); + m_adjust_panel_glmodel.render(); + + // Axis arrows: a shaft plus a small V-shaped arrowhead, both along local +X. Reused for both + // the U and V axes below by swapping which world direction local +X is transformed to. + if (!m_adjust_arrow_glmodel.is_initialized()) { + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(4); + init_data.reserve_indices(6); + init_data.add_vertex(Vec3f(0.f, 0.f, 0.f)); + init_data.add_vertex(Vec3f(1.f, 0.f, 0.f)); + init_data.add_vertex(Vec3f(0.82f, 0.08f, 0.f)); + init_data.add_vertex(Vec3f(0.82f, -0.08f, 0.f)); + init_data.add_line(0, 1); + init_data.add_line(1, 2); + init_data.add_line(1, 3); + m_adjust_arrow_glmodel.init_from(std::move(init_data)); + } +#if !SLIC3R_OPENGL_ES + if (!OpenGLManager::get_gl_info().is_core_profile()) + glsafe(::glLineWidth(2.0f)); +#endif // !SLIC3R_OPENGL_ES + + auto render_arrow = [&](const Vec3f &axis, const Vec3f &other_axis, bool is_active) { + Transform3d arrow_transform = Transform3d::Identity(); + arrow_transform.linear().col(0) = axis.cast(); + arrow_transform.linear().col(1) = other_axis.cast(); + arrow_transform.linear().col(2) = m_adjust_anchor_normal.cast(); + arrow_transform.translation() = handle_center_local.cast(); + + const Transform3d vmm = camera.get_view_matrix() * trafo_matrix * arrow_transform * + Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), Vec3d(arrow_length, arrow_length, arrow_length)); + shader->set_uniform("view_model_matrix", vmm); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + m_adjust_arrow_glmodel.set_color(is_active ? ColorRGBA::YELLOW() : ColorRGBA::ORANGE()); + m_adjust_arrow_glmodel.render(); + }; + render_arrow(u_axis, v_axis, m_adjust_drag_handle == AdjustHandle::AxisU); + render_arrow(v_axis, u_axis, m_adjust_drag_handle == AdjustHandle::AxisV); + + shader->stop_using(); + glsafe(::glDisable(GL_BLEND)); + glsafe(::glEnable(GL_DEPTH_TEST)); +} + +bool GLGizmoTextureDisplacement::on_mouse_adjust_texture(const wxMouseEvent &mouse_event) +{ + if (!m_adjust_anchor_valid) + return false; + + ModelVolume *mv = texture_volume(); + ModelObject *mo = m_c->selection_info()->model_object(); + TextureDisplacementLayer *layer = active_layer(); + if (mv == nullptr || mo == nullptr || layer == nullptr) + return false; + + const Selection &selection = m_parent.get_selection(); + const Transform3d trafo_matrix = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix(); + const Camera &camera = wxGetApp().plater()->get_camera(); + + const float bbox_size = float(mv->mesh().bounding_box().size().norm()); + const float panel_half = bbox_size * 0.04f + 1.f; + const float arrow_length = panel_half * 2.2f; + // Where the handle is drawn (moves with the layer's offset) vs. the plane the drag is measured + // against (fixed at the anchor). Keeping them apart is what stops the handle's own motion from + // feeding back into the delta that produced it. + const Vec3f handle_center_local = adjust_handle_center(*layer); + const Vec3f drag_plane_local = adjust_plane_point(); + const Vec3d handle_center_world = trafo_matrix * handle_center_local.cast(); + const Vec2d mouse_pos(mouse_event.GetX(), mouse_event.GetY()); + + Vec3f u_axis, v_axis; + adjust_tangent_basis(u_axis, v_axis); + + const Point handle_screen = CameraUtils::project(camera, handle_center_world); + const Vec2d handle_screen_d(double(handle_screen.x()), double(handle_screen.y())); + + // Point-to-segment distance in screen space, for the arrow shafts. + auto dist_to_segment_px = [](const Vec2d &p, const Vec2d &a, const Vec2d &b) { + const Vec2d ab = b - a; + const double len2 = ab.squaredNorm(); + const double t = (len2 > 1e-9) ? std::clamp((p - a).dot(ab) / len2, 0.0, 1.0) : 0.0; + return (p - (a + ab * t)).norm(); + }; + + if (mouse_event.LeftDown()) { + const Vec3d u_tip_world = trafo_matrix * (handle_center_local + u_axis * arrow_length).cast(); + const Vec3d v_tip_world = trafo_matrix * (handle_center_local + v_axis * arrow_length).cast(); + const Point u_tip_screen = CameraUtils::project(camera, u_tip_world); + const Point v_tip_screen = CameraUtils::project(camera, v_tip_world); + const Vec2d u_tip_screen_d(double(u_tip_screen.x()), double(u_tip_screen.y())); + const Vec2d v_tip_screen_d(double(v_tip_screen.x()), double(v_tip_screen.y())); + + // Panel screen-space "radius", approximated from one corner (a loose circle around the + // square is close enough for hit-testing purposes). + const Vec3d panel_corner_world = trafo_matrix * (handle_center_local + (u_axis + v_axis) * panel_half).cast(); + const Point panel_corner_screen = CameraUtils::project(camera, panel_corner_world); + const double panel_screen_radius = (Vec2d(double(panel_corner_screen.x()), double(panel_corner_screen.y())) - handle_screen_d).norm(); + + constexpr double pick_tolerance_px = 8.0; + const double dist_to_u = dist_to_segment_px(mouse_pos, handle_screen_d, u_tip_screen_d); + const double dist_to_v = dist_to_segment_px(mouse_pos, handle_screen_d, v_tip_screen_d); + const double dist_to_panel = (handle_screen_d - mouse_pos).norm(); + + // Arrows take priority over the panel (their tips extend past it), then the panel covers + // the broader central area. + if (dist_to_u <= pick_tolerance_px && dist_to_u <= dist_to_v) + m_adjust_drag_handle = AdjustHandle::AxisU; + else if (dist_to_v <= pick_tolerance_px) + m_adjust_drag_handle = AdjustHandle::AxisV; + else if (dist_to_panel <= panel_screen_radius) + m_adjust_drag_handle = AdjustHandle::Pan; + else { + m_adjust_drag_handle = AdjustHandle::None; + return false; + } + + m_adjust_drag_start_offset = layer->offset; + + Vec3d world_hit; + if (ray_plane_hit(camera, mouse_pos, trafo_matrix, drag_plane_local, m_adjust_anchor_normal, world_hit)) { + const Vec3f local_hit = (trafo_matrix.inverse() * world_hit).cast(); + m_adjust_drag_start_planar = project_planar(local_hit, m_adjust_anchor_normal); + } + return true; + } + + if (mouse_event.Dragging() && m_adjust_drag_handle != AdjustHandle::None) { + Vec3d world_hit; + if (!ray_plane_hit(camera, mouse_pos, trafo_matrix, drag_plane_local, m_adjust_anchor_normal, world_hit)) + return true; + const Vec3f local_hit = (trafo_matrix.inverse() * world_hit).cast(); + const Vec2f current_planar = project_planar(local_hit, m_adjust_anchor_normal); + + Vec2f delta_planar = current_planar - m_adjust_drag_start_planar; + // project_planar()'s (x, y) axes are exactly u_axis/v_axis (see adjust_tangent_basis()), + // so zeroing one component constrains the drag to only the other axis. + if (m_adjust_drag_handle == AdjustHandle::AxisU) + delta_planar.y() = 0.f; + else if (m_adjust_drag_handle == AdjustHandle::AxisV) + delta_planar.x() = 0.f; + + const float scale = (layer->tiling_scale > 1e-6f) ? (1.f / layer->tiling_scale) : 1.f; + const Vec2f delta_scaled = delta_planar * scale; + const float rad = layer->rotation_deg * float(M_PI) / 180.f; + const float cs = std::cos(rad), sn = std::sin(rad); + Vec2f delta_rotated(delta_scaled.x() * cs - delta_scaled.y() * sn, delta_scaled.x() * sn + delta_scaled.y() * cs); + // ...and the same v scaling apply_uv_transform() applies for a non-square texture, so the + // handle keeps tracking the cursor exactly instead of drifting on the v axis. + delta_rotated.y() *= layer_texture_aspect(*layer); + // Increasing `offset` shifts which texel is sampled at a fixed world position, which + // visually slides the pattern the *opposite* way - subtracting is this session's + // best-effort reasoning about the direction that feels like "dragging the texture", + // unverified against an actual render (see header comment). + layer->offset = m_adjust_drag_start_offset - delta_rotated; + + m_preview_params_dirty = true; + m_parent.set_as_dirty(); + return true; + } + + if (mouse_event.LeftUp() && m_adjust_drag_handle != AdjustHandle::None) { + m_adjust_drag_handle = AdjustHandle::None; + rebuild_preview(); + m_preview_params_dirty = false; + return true; + } + + return false; +} + +ModelVolume* GLGizmoTextureDisplacement::texture_volume() +{ + ModelObject *mo = m_c->selection_info()->model_object(); + if (!mo) + return nullptr; + for (ModelVolume *mv : mo->volumes) + if (mv->is_model_part()) + return mv; + return nullptr; +} + +const ModelVolume* GLGizmoTextureDisplacement::texture_volume() const +{ + return const_cast(this)->texture_volume(); +} + +void GLGizmoTextureDisplacement::update_model_object() +{ + bool updated = false; + ModelObject *mo = m_c->selection_info()->model_object(); + int idx = -1; + for (ModelVolume *mv : mo->volumes) { + if (!mv->is_model_part()) + continue; + ++idx; + FacetsAnnotation &facet = mv->texture_displacement_facet(m_active_layer_slot); + // See m_selectors_stale: the mask could not be loaded into this selector, so an empty selector + // here is "failed to load", not "nothing painted", and writing it back would erase the paint. + // A selector that does hold something is the user's own work and must be flushed as usual. + if (m_selectors_stale && !facet.empty() && m_triangle_selectors[idx]->serialize().triangles_to_split.empty()) + continue; + updated |= facet.set(*m_triangle_selectors[idx]); + } + + // The fast (shaded) preview reads the live selector, so it has to be rebuilt after any stroke that + // flushes here - not only when set() reports a change. Rebuilding it via rebuild_preview() below + // is gated on `updated`, which misses e.g. the first paint into a slot; marking it dirty makes the + // render loop (render_painter_gizmo) rebuild it next frame regardless. Without this, fast preview - + // now the default view - stayed blank until a full reload (select-whole-model / reopen). + m_shaded_preview_dirty = true; + + if (updated) { + const ModelObjectPtrs &mos = wxGetApp().model().objects; + wxGetApp().obj_list()->update_info_items(std::find(mos.begin(), mos.end(), mo) - mos.begin()); + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); + rebuild_preview(); + } +} + +void GLGizmoTextureDisplacement::update_from_model_object(bool first_update) +{ + wxBusyCursor wait; + + const ModelObject *mo = m_c->selection_info()->model_object(); + m_triangle_selectors.clear(); + + std::vector ebt_colors; + ebt_colors.push_back(GLVolume::NEUTRAL_COLOR); + ebt_colors.push_back(TriangleSelectorGUI::enforcers_color); + ebt_colors.push_back(TriangleSelectorGUI::blockers_color); + m_selectors_stale = false; + for (const ModelVolume *mv : mo->volumes) { + if (!mv->is_model_part()) + continue; + + const TriangleMesh *mesh = &mv->mesh(); + const TriangleSelector::TriangleSplittingData &data = + mv->texture_displacement_facet(m_active_layer_slot).get_data(); + // The same bound TriangleSelector::deserialize() checks before it gives up - silently, with a + // void return and no way to report it. A mask recorded before the mesh was replaced indexes + // triangles that no longer exist, and the selector then comes back empty even though the mask + // is not. That has to be caught here, because the next update_model_object() would otherwise + // write the empty selector back over the mask: the paint would vanish, and the bake would + // report "nothing is painted" about the very data the flush had just deleted. + const size_t facet_count = mesh->its.indices.size(); + for (const TriangleSelector::TriangleBitStreamMapping &m : data.triangles_to_split) + if (m.triangle_idx < 0 || size_t(m.triangle_idx) >= facet_count) { + m_selectors_stale = true; + break; + } + m_triangle_selectors.emplace_back(std::make_unique(*mesh, ebt_colors)); + m_triangle_selectors.back()->deserialize(data, false); + m_triangle_selectors.back()->request_update_render_data(); + } + + // Start a freshly opened, never-textured volume with one layer already in place, so the panel is + // ready to paint straight away rather than showing an empty layer list. Only on first open, and + // only when there are none - never during an undo/redo or layer-switch reload (which also come + // through here), where silently adding a layer would be wrong. + if (first_update) + if (ModelVolume *tv = texture_volume(); tv != nullptr && tv->texture_displacement_layers.empty()) { + add_texture_layer(); // takes its own snapshot and rebuilds the preview + return; + } + + rebuild_preview(); +} + +void GLGizmoTextureDisplacement::set_active_layer(int slot) +{ + if (slot == m_active_layer_slot) + return; + // Flush edits made while the previous layer was active before switching what the selectors + // reflect - otherwise they would be silently lost. + update_model_object(); + m_active_layer_slot = slot; + update_from_model_object(false); + // The on-canvas gizmo (if on) is anchored to whichever layer is active - keep it in sync + // instead of leaving it pointing at the previous layer's (now stale) paint patch. + if (m_adjust_texture_mode) + update_adjust_anchor(); + // Refresh every preview/overlay (shaded, UV editor, seams, ...) for the newly active layer. + rebuild_preview(); +} + +void GLGizmoTextureDisplacement::ensure_panel_icons() +{ + if (m_panel_icons_tried) + return; + m_panel_icons_tried = true; + + // Order is irrelevant; the map keys by file name. Loaded with color_wite_gray so each icon has both + // a monochrome ("normal", grey in the current theme) and an original-colour variant. + static const std::vector names = { + "texture_displacement_brush.svg", "texture_displacement_face.svg", "texture_displacement_connected_area.svg", + "texture_displacement_real_preview.svg", "texture_displacement_fast_preview.svg", + "texture_displacement_checker.svg", "texture_displacement_distortion.svg", + "texture_displacement_wireframe.svg", "texture_displacement_cross.svg", + "texture_displacement_uv_select_island.svg", "texture_displacement_uv_select_edge.svg", + "texture_displacement_uv_select_vertex.svg", + // Paint / Erase, brush cursor shape, mapping, tiling, placement and the dock toggle. + "texture_displacement_add.svg", "texture_displacement_add_negative.svg", "circle_paint.svg", + "menu_obj_sphere.svg", "menu_obj_cube.svg", "menu_obj_cylinder.svg", "texture_displacement_map_unwrap.svg", + "texture_displacement_map_view.svg", "texture_displacement_tile_repeat.svg", "menu_mirror_x.svg", + "texture_displacement_adjust.svg", "canvas_drag.svg", "texture_displacement_move_up.svg", + "texture_displacement_move_down.svg", "texture_displacement_drag.svg", + "texture_displacement_select_all.svg", "texture_displacement_erase_all.svg", + }; + std::vector paths; + paths.reserve(names.size()); + for (const std::string &n : names) + paths.push_back(resources_dir() + "/images/" + n); + + // Runs from the panel render, i.e. with a GL context current, so the upload is safe here. + // + // Rasterized at twice GLToolbar::Default_Icons_Size rather than at it: these are drawn at the + // toolbar's icon size, which is that constant scaled by DPI (toolbar_icon_scale() folds in + // em_unit), so on a 200% display the draw size reaches 80 px. Rasterizing at 40 would upscale a + // 40 px bitmap there, which is what actually reads as blurry - downscaling does not. The icon set + // is rasterized once for the gizmo's lifetime, so it cannot re-raster on a DPI change; sizing for + // the larger case and letting ImGui shrink it is the version that looks right on both. + const std::vector icons = + m_panel_icons.init(paths, ImVec2(2 * GLToolbar::Default_Icons_Size, 2 * GLToolbar::Default_Icons_Size), + IconManager::RasterType::color_wite_gray); + for (size_t i = 0; i < names.size() && i < icons.size(); ++i) + m_panel_icon_map[names[i]] = icons[i]; +} + +void GLGizmoTextureDisplacement::add_texture_layer() +{ + ModelVolume *mv = texture_volume(); + if (!mv) + return; + + std::array used{}; + for (const TextureDisplacementLayer &l : mv->texture_displacement_layers) + if (l.slot >= 0 && size_t(l.slot) < used.size()) + used[size_t(l.slot)] = true; + int free_slot = -1; + for (size_t i = 0; i < used.size(); ++i) + if (!used[i]) { free_slot = int(i); break; } + if (free_slot < 0) { + show_error(nullptr, _u8L("Maximum number of texture displacement layers reached.")); + return; + } + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Add texture displacement layer"), UndoRedo::SnapshotType::GizmoAction); + + TextureDisplacementLayer layer; + layer.slot = free_slot; + // Start the layer off on the first library texture rather than on nothing at all: a textureless + // layer looks broken (painting on it appears to do nothing, because there is no height map to + // displace by). The user swaps it for another from the layer's own picker. + const std::vector &library = texture_library(); + if (!library.empty()) + if (const LibraryTexture *tex = get_library_texture(library.front().path)) { + layer.name = library.front().name; + layer.path = library.front().path; + layer.image_data = tex->image_data; + } + mv->texture_displacement_layers.push_back(std::move(layer)); + // A new layer opens with every setting showing; there is nothing on it yet to hide settings from. + m_layer_expanded[size_t(free_slot)] = true; + + set_active_layer(free_slot); + // set_active_layer() is a no-op when the new slot happens to be the one already active (slot 0, + // for the very first layer added), so the preview would not pick the new texture up on its own. + rebuild_preview(); + m_parent.set_as_dirty(); +} + +const GLGizmoTextureDisplacement::LibraryTexture *GLGizmoTextureDisplacement::get_library_texture(const std::string &path) +{ + if (auto it = m_library_textures.find(path); it != m_library_textures.end()) + return it->second.image_data ? &it->second : nullptr; + + LibraryTexture entry; + std::string error; + entry.image_data = load_texture_image_data(path, error); + if (entry.image_data) { + TextureDisplacementLayer probe; + probe.image_data = entry.image_data; + entry.thumbnail = upload_height_thumbnail(decode_height_texture(probe)); + if (!entry.thumbnail) + entry.image_data.reset(); // decoded to nothing usable - treat it as a failed load + } else { + BOOST_LOG_TRIVIAL(error) << "Texture displacement: could not load texture " << path << ": " << error; + } + + // Cached whether it loaded or not: a file that failed is remembered as unusable, so the picker + // does not retry (and re-log) it on every frame it is on screen. + const auto [pos, inserted] = m_library_textures.emplace(path, std::move(entry)); + return pos->second.image_data ? &pos->second : nullptr; +} + +void GLGizmoTextureDisplacement::set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry) +{ + const LibraryTexture *tex = get_library_texture(entry.path); + if (tex == nullptr) { + show_error(nullptr, _u8L("Could not load the selected texture.")); + return; + } + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Change texture displacement texture"), UndoRedo::SnapshotType::GizmoAction); + layer.name = entry.name; + layer.path = entry.path; + // Handing over the library's own buffer (rather than a copy) is what lets decode_height_texture() + // - whose cache is keyed by exactly this pointer - hit straight away instead of re-decoding + // the PNG the first time the layer is previewed or baked. + layer.image_data = tex->image_data; + // The user's own textures are listed most recently used first. + if (entry.is_user) + touch_user_texture(entry.path); + + rebuild_preview(); + m_parent.set_as_dirty(); +} + +void GLGizmoTextureDisplacement::import_custom_texture(TextureDisplacementLayer &layer) +{ + const wxString wildcard = "Images (*.png;*.jpg;*.jpeg;*.bmp)|*.png;*.jpg;*.jpeg;*.bmp"; + wxFileDialog dialog(nullptr, _L("Choose a texture image (height map)"), wxEmptyString, wxEmptyString, wildcard, + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dialog.ShowModal() != wxID_OK) + return; + + // Converts to the 8-bit grayscale PNG the bake code understands and copies it into the user's + // own texture folder, so it stays available for later models (and survives an app update, which + // rewrites the shipped folder wholesale). + std::string error; + const std::optional entry = import_texture_to_library(into_u8(dialog.GetPath()), error); + if (!entry) { + show_error(nullptr, error); + return; + } + set_layer_texture(layer, *entry); +} + +void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &panel_min, const ImVec2 &panel_max) +{ + ModelVolume *mv = texture_volume(); + TextureDisplacementLayer *layer = nullptr; + if (mv != nullptr) + for (TextureDisplacementLayer &l : mv->texture_displacement_layers) + if (l.slot == m_picker_slot) + layer = &l; + + static const char *const popup_id = "##texture_library"; + if (m_picker_open_request) { + m_picker_open_request = false; + if (layer != nullptr) + ImGui::OpenPopup(popup_id); + } + + const float tile = std::round(m_imgui->scaled(3.2f)); + const float spacing = std::round(m_imgui->scaled(0.4f)); + const float grid_w = 4.f * tile + 3.f * spacing; + const float gap = m_imgui->scaled(0.5f); + // Beside the panel rather than over it, so the layer being retextured stays in view: to its left when + // there is room (the docked panel sits against the right edge of the canvas), otherwise to its right. + const float popup_w = grid_w + 2.f * ImGui::GetStyle().WindowPadding.x; + const bool left = panel_min.x - gap - popup_w >= 0.f; + ImGui::SetNextWindowPos(ImVec2(left ? panel_min.x - gap : panel_max.x + gap, panel_min.y + m_imgui->scaled(4.f)), + ImGuiCond_Appearing, ImVec2(left ? 1.f : 0.f, 0.f)); + if (!ImGui::BeginPopup(popup_id)) + return; + if (layer == nullptr) { + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + return; + } + + m_imgui->push_bold_font(); + m_imgui->text(_L("Choose a texture")); + m_imgui->pop_bold_font(); + + const std::vector &library = texture_library(); + ImDrawList *dl = ImGui::GetWindowDrawList(); + const ImU32 selected_col = ImGui::GetColorU32(ImGuiWrapper::COL_ORCA); + std::optional chosen; + std::optional to_remove; + bool import_clicked = false; + + // Tiles are placed on the four-column grid explicitly rather than flowed with SameLine(): a user tile + // overlays a remove button on its corner, and that button would otherwise be "the previous item" the + // next tile lines up against. + const auto tile_pos = [&](const ImVec2 &origin, int i) { + return ImVec2(origin.x + float(i % 4) * (tile + spacing), origin.y + float(i / 4) * (tile + spacing)); + }; + // Leaves the cursor below a grid of `count` tiles, with the grid's area registered as content. + const auto end_grid = [&](const ImVec2 &origin, int count) { + ImGui::SetCursorScreenPos(origin); + if (count > 0) { + const int rows = (count + 3) / 4; + ImGui::Dummy(ImVec2(grid_w, float(rows) * tile + float(rows - 1) * spacing)); + } + }; + // One group's tiles: shipped textures alphabetically, or the user's own most recently used first (the + // order texture_library() keeps), those with a remove button on hover. Returns how many it drew. + const auto grid = [&](const ImVec2 &origin, bool user) { + int n = 0; + for (const TextureLibraryEntry &entry : library) { + if (entry.is_user != user) + continue; + ImGui::SetCursorScreenPos(tile_pos(origin, n++)); + ImGui::PushID(entry.path.c_str()); + const LibraryTexture *tex = get_library_texture(entry.path); + GLTexture *thumb = tex != nullptr ? tex->thumbnail.get() : nullptr; + bool clicked = false; + if (thumb != nullptr) { + // Centre-crop a non-square image into the square tile instead of squashing it. + const float aspect = thumb->get_height() > 0 ? float(thumb->get_width()) / float(thumb->get_height()) : 1.f; + ImVec2 uv0(0.f, 0.f), uv1(1.f, 1.f); + if (aspect > 1.f) { + uv0.x = 0.5f * (1.f - 1.f / aspect); + uv1.x = 1.f - uv0.x; + } else if (aspect < 1.f) { + uv0.y = 0.5f * (1.f - aspect); + uv1.y = 1.f - uv0.y; + } + clicked = ImGui::ImageButton((ImTextureID) (intptr_t) thumb->get_id(), ImVec2(tile, tile), uv0, uv1, 0); + } else { + clicked = ImGui::Button("?", ImVec2(tile, tile)); + } + if (user) + ImGui::SetItemAllowOverlap(); // the remove button drawn over its corner takes its own clicks + const ImVec2 a = ImGui::GetItemRectMin(), b = ImGui::GetItemRectMax(); + if (entry.path == layer->path) + dl->AddRect(ImVec2(a.x - 1.f, a.y - 1.f), ImVec2(b.x + 1.f, b.y + 1.f), selected_col, 0.f, 0, 2.f); + if (ImGui::IsItemHovered()) + m_imgui->tooltip(entry.name, m_imgui->scaled(18.f)); + + if (user && ImGui::IsMouseHoveringRect(a, b)) { + const float r = std::round(tile * 0.17f); + const ImVec2 c(b.x - r - 2.f, a.y + r + 2.f); + ImGui::SetCursorScreenPos(ImVec2(c.x - r, c.y - r)); + const bool remove_clicked = ImGui::InvisibleButton("##remove", ImVec2(2.f * r, 2.f * r)); + const bool hot = ImGui::IsItemHovered(); + dl->AddCircleFilled(c, r, hot ? IM_COL32(214, 72, 64, 235) : IM_COL32(0, 0, 0, 170)); + const float k = 0.42f * r; + dl->AddLine(ImVec2(c.x - k, c.y - k), ImVec2(c.x + k, c.y + k), IM_COL32_WHITE, 1.5f); + dl->AddLine(ImVec2(c.x - k, c.y + k), ImVec2(c.x + k, c.y - k), IM_COL32_WHITE, 1.5f); + if (hot) + m_imgui->tooltip(_u8L("Remove from My textures"), m_imgui->scaled(18.f)); + if (remove_clicked) + to_remove = entry; + } + ImGui::PopID(); + if (clicked) + chosen = entry; + } + return n; + }; + + ImGui::TextDisabled("%s", _u8L("Built-in").c_str()); + { + const ImVec2 origin = ImGui::GetCursorScreenPos(); + const int count = grid(origin, false); + end_grid(origin, count); + if (count == 0) + ImGui::TextDisabled("%s", _u8L("No textures found.").c_str()); + } + ImGui::TextDisabled("%s", _u8L("My textures").c_str()); + { + const ImVec2 origin = ImGui::GetCursorScreenPos(); + const int count = grid(origin, true); + ImGui::SetCursorScreenPos(tile_pos(origin, count)); + if (ImGui::Button("+##import_texture", ImVec2(tile, tile))) + import_clicked = true; + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Import your own image as a height map. It is converted to the format the slicer " + "bakes from and saved to your personal texture folder, kept separate from the " + "textures shipped with OrcaSlicer."), + m_imgui->scaled(20.f)); + end_grid(origin, count + 1); + } + + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + grid_w); + ImGui::TextDisabled("%s", _u8L("PNG, JPG or BMP - converted to a height map on import. Hover one of yours to remove it.").c_str()); + ImGui::PopTextWrapPos(); + + if (chosen || import_clicked) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + + // Only once the loop over the library is done: importing, picking (which reorders the recently used) + // and removing all change the library the loop was iterating. + if (chosen) + set_layer_texture(*layer, *chosen); + else if (import_clicked) + import_custom_texture(*layer); + else if (to_remove) { + MessageDialog dlg(nullptr, + wxString::Format(_L("Remove \"%s\" from My textures?\n\nThe image file is deleted from your " + "texture folder. Layers already using it keep it."), + from_u8(to_remove->name)), + _L("Remove texture"), wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION); + if (dlg.ShowModal() == wxID_YES) { + std::string error; + if (remove_user_texture(to_remove->path, error)) + m_library_textures.erase(to_remove->path); // its cached thumbnail and bytes go with it + else + show_error(nullptr, error); + } + // The modal dialog took the focus, which closes the popup; the user was choosing a texture and + // still is, so bring it back. + m_picker_open_request = true; + } +} + +void GLGizmoTextureDisplacement::remove_texture_layer(int slot) +{ + ModelVolume *mv = texture_volume(); + if (!mv) + return; + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Remove texture displacement layer"), UndoRedo::SnapshotType::GizmoAction); + + auto &layers = mv->texture_displacement_layers; + layers.erase(std::remove_if(layers.begin(), layers.end(), + [slot](const TextureDisplacementLayer &l) { return l.slot == slot; }), + layers.end()); + if (slot >= 0 && slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS)) { + mv->texture_displacement_facet(slot).reset(); + m_thumbnails[size_t(slot)].reset(); + m_thumbnail_source[size_t(slot)] = nullptr; + m_layer_expanded[size_t(slot)] = false; + } + if (m_picker_slot == slot) + m_picker_slot = -1; + + if (m_active_layer_slot == slot) + update_from_model_object(false); // also rebuilds the preview + else + rebuild_preview(); // a non-active layer's contribution to the combined preview changed + + m_parent.set_as_dirty(); +} + +void GLGizmoTextureDisplacement::swap_layer_slots(int slot_a, int slot_b) +{ + ModelObject *mo = m_c->selection_info()->model_object(); + ModelVolume *mv = texture_volume(); + const int n = int(TEXTURE_DISPLACEMENT_MAX_LAYERS); + if (mo == nullptr || mv == nullptr || slot_a == slot_b || slot_a < 0 || slot_b < 0 || slot_a >= n || slot_b >= n) + return; + + // Every model part, not only the texture volume: the selectors paint the active slot on all of them. + for (ModelVolume *v : mo->volumes) { + if (!v->is_model_part()) + continue; + TriangleSelector::TriangleSplittingData data_a = v->texture_displacement_facet(slot_a).get_data(); + TriangleSelector::TriangleSplittingData data_b = v->texture_displacement_facet(slot_b).get_data(); + v->texture_displacement_facet(slot_a).set_data(std::move(data_b)); + v->texture_displacement_facet(slot_b).set_data(std::move(data_a)); + } + for (TextureDisplacementLayer &l : mv->texture_displacement_layers) { + if (l.slot == slot_a) + l.slot = slot_b; + else if (l.slot == slot_b) + l.slot = slot_a; + } + + const size_t a = size_t(slot_a), b = size_t(slot_b); + std::swap(m_thumbnails[a], m_thumbnails[b]); + std::swap(m_thumbnail_source[a], m_thumbnail_source[b]); + std::swap(m_thumbnail_smoothing[a], m_thumbnail_smoothing[b]); + std::swap(m_layer_expanded[a], m_layer_expanded[b]); + if (m_active_layer_slot == slot_a) + m_active_layer_slot = slot_b; + else if (m_active_layer_slot == slot_b) + m_active_layer_slot = slot_a; + if (m_picker_slot == slot_a) + m_picker_slot = slot_b; + else if (m_picker_slot == slot_b) + m_picker_slot = slot_a; +} + +void GLGizmoTextureDisplacement::move_texture_layer(int slot, int to_index) +{ + ModelVolume *mv = texture_volume(); + if (mv == nullptr) + return; + + std::vector slots; + for (const TextureDisplacementLayer &l : mv->texture_displacement_layers) + slots.push_back(l.slot); + std::sort(slots.begin(), slots.end()); + const auto it = std::find(slots.begin(), slots.end(), slot); + if (it == slots.end()) + return; + const int from = int(it - slots.begin()); + // An insertion index counts the moved layer's own position, which is gone once it is lifted out. + const int to = std::clamp(to_index > from ? to_index - 1 : to_index, 0, int(slots.size()) - 1); + if (to == from) + return; + + update_model_object(); // flush the active layer's strokes into its slot before slots are exchanged + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Reorder texture displacement layers"), + UndoRedo::SnapshotType::GizmoAction); + // Walked one neighbour at a time, so every layer in between shifts by one and keeps its order. + const int step = to > from ? 1 : -1; + for (int i = from; i != to; i += step) + swap_layer_slots(slots[size_t(i)], slots[size_t(i + step)]); + + // The active layer's mask now sits in a different slot, so the selectors are reloaded from there. + update_from_model_object(false); // also rebuilds the preview + if (m_adjust_texture_mode) + update_adjust_anchor(); + m_parent.set_as_dirty(); +} + +int GLGizmoTextureDisplacement::select_visible_faces(const std::array *uv_clip) +{ + ModelVolume *mv = texture_volume(); + ModelObject *mo = m_c->selection_info()->model_object(); + const int idx = texture_volume_raycaster_index(); + if (mv == nullptr || mo == nullptr || idx < 0 || idx >= int(m_triangle_selectors.size())) + return 0; + + const indexed_triangle_set &its = mv->mesh().its; + if (its.indices.empty()) + return 0; + + const Selection &selection = m_parent.get_selection(); + const Geometry::Transformation trafo(mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * + mv->get_matrix()); + const Transform3d &to_world = trafo.get_matrix(); + const Camera &camera = wxGetApp().plater()->get_camera(); + + // Normals transform by the inverse transpose, not by the matrix itself - with a non-uniform + // scale the two differ, and using the wrong one flips the facing test on the scaled axes. + const Matrix3d normal_matrix = to_world.matrix().block<3, 3>(0, 0).inverse().transpose(); + + // Pass 1 (cheap): drop back-facing triangles. Under perspective the view direction varies across + // the model, so it is taken per triangle from the eye to the centroid; under an orthographic + // camera get_position() is still a point on the view axis, so the same expression stays correct + // in direction terms for everything actually on screen. + const Vec3d eye = camera.get_position(); + const bool ortho = camera.get_type() == Camera::EType::Ortho; + const Vec3d fwd = camera.get_dir_forward(); + + std::vector centroids; // world coords, what get_unobscured_idxs() expects + std::vector front_facing; // parallel: which facet each centroid came from + centroids.reserve(its.indices.size() / 2); + front_facing.reserve(its.indices.size() / 2); + + for (size_t f = 0; f < its.indices.size(); ++f) { + const stl_triangle_vertex_indices &tri = its.indices[f]; + const Vec3d a = its.vertices[tri[0]].cast(); + const Vec3d b = its.vertices[tri[1]].cast(); + const Vec3d c = its.vertices[tri[2]].cast(); + const Vec3d n_world = normal_matrix * (b - a).cross(c - a); + if (n_world.squaredNorm() < 1e-20) + continue; // degenerate triangle: no meaningful normal, so no meaningful facing test + const Vec3d centroid_local = (a + b + c) / 3.0; + const Vec3d centroid_world = to_world * centroid_local; + const Vec3d view_dir = ortho ? fwd : Vec3d(centroid_world - eye); + if (n_world.dot(view_dir) >= 0.0) + continue; // facing away from the camera + + // Clip to the projection frame before the raycast, not after: outside the frame the texture + // samples to nothing anyway, so those facets would only be painted to no effect - and this + // is also what keeps the ray queries proportional to the framed area instead of the model. + if (uv_clip != nullptr) { + Vec2f uv; + if (!project_uv_projective(*uv_clip, centroid_local.cast(), uv)) + continue; // behind the projector + if (uv.x() < 0.f || uv.x() > 1.f || uv.y() < 0.f || uv.y() > 1.f) + continue; + } + centroids.emplace_back(centroid_world.cast()); + front_facing.push_back(unsigned(f)); + } + if (centroids.empty()) + return 0; + + // Pass 2 (the expensive one): a real ray query per surviving centroid, so geometry in front of a + // front-facing triangle correctly hides it - the far inner wall of a cup is front-facing but not + // visible. This is why the whole thing is click-driven rather than live. + std::vector unobscured; + { + wxBusyCursor wait; + unobscured = m_c->raycaster()->raycasters()[size_t(idx)]->get_unobscured_idxs( + trafo, camera, centroids, m_c->object_clipper()->get_clipping_plane()); + } + if (unobscured.empty()) + return 0; + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Select visible faces for texture displacement"), + UndoRedo::SnapshotType::GizmoAction); + + // Replaces the layer's paint rather than adding to it: the checkbox means "project onto what I + // can see", so a second capture from a new angle should not leave the previous angle painted. + // TriangleSelectorGUI, not the TriangleSelector base: request_update_render_data() is declared on + // the GUI subclass, so binding to the base here would drop it. + TriangleSelectorGUI &selector = *m_triangle_selectors[size_t(idx)]; + selector.reset(); + for (unsigned i : unobscured) + selector.set_facet(int(front_facing[i]), EnforcerBlockerType::ENFORCER); + selector.request_update_render_data(); + + update_model_object(); + m_parent.set_as_dirty(); + return int(unobscured.size()); +} + +void GLGizmoTextureDisplacement::select_whole_model() +{ + ModelObject *mo = m_c->selection_info()->model_object(); + if (!mo) + return; + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Select whole model for texture displacement"), UndoRedo::SnapshotType::GizmoAction); + + int idx = -1; + for (const ModelVolume *v : mo->volumes) { + if (!v->is_model_part()) + continue; + ++idx; + const size_t facet_count = v->mesh().its.indices.size(); + for (size_t i = 0; i < facet_count; ++i) + m_triangle_selectors[idx]->set_facet(int(i), EnforcerBlockerType::ENFORCER); + m_triangle_selectors[idx]->request_update_render_data(); + } + update_model_object(); + m_parent.set_as_dirty(); +} + +void GLGizmoTextureDisplacement::subdivide_model() +{ + ModelVolume *mv = texture_volume(); + ModelObject *mo = m_c->selection_info()->model_object(); + if (mv == nullptr || mo == nullptr || m_subdivide_count < 1) + return; // 0 passes means "no subdivision" - don't take a snapshot for a no-op + + Plater *plater = wxGetApp().plater(); + Plater::TakeSnapshot snapshot(plater, _u8L("Subdivide model for texture displacement"), UndoRedo::SnapshotType::GizmoAction); + + // Same save/replace/restore-painting dance GLGizmoSimplify uses when it re-tessellates a + // volume's mesh: supported/seam/mmu/fuzzy-skin masks get remapped onto the new triangles, + // texture-displacement doesn't (no remap support for it yet) and is dropped instead of being + // left referring to triangle indices that no longer mean the same thing. + std::optional saved_painting = mv->save_painting(); + + // max_edge_length 0 means "no triangle is ever small enough", so every non-degenerate edge is + // split on each of the m_subdivide_count passes - i.e. a plain "subdivide the whole mesh N times". + TriangleMesh new_mesh(subdivide_mesh_uniform(mv->mesh().its, 0.f, m_subdivide_count)); + mv->set_mesh(std::move(new_mesh)); + mv->set_new_unique_id(); + mv->calculate_convex_hull(); + mv->restore_painting(saved_painting); + + if (ObjectList *obj_list = wxGetApp().obj_list()) { + const ModelObjectPtrs &objs = plater->model().objects; + auto it = std::find(objs.begin(), objs.end(), mo); + if (it != objs.end()) + obj_list->update_info_items(size_t(it - objs.begin())); + } + + plater->changed_object(*mo); + update_from_model_object(false); // reload selectors/preview against the new mesh + cleared paint + m_parent.set_as_dirty(); +} + +bool GLGizmoTextureDisplacement::collect_paint_region( + const TriangleMesh &mesh, const TextureDisplacementFacetsData &facets, + std::vector ®ion, + std::array *paint) +{ + const indexed_triangle_set &its = mesh.its; + const size_t ntri = its.indices.size(); + const size_t nvert = its.vertices.size(); + + region.assign(ntri, 0); + if (paint) + for (LayerPaintMap &pm : *paint) + pm = LayerPaintMap{}; + + // Twice the area of each source triangle, for the "is this one covered edge to edge" test below. + // Only the paint carry-forward needs it, and the live subdivide preview calls this on every + // slider frame, so it is not built for the region-only path. + const auto tri_area2 = [](const Vec3f &a, const Vec3f &b, const Vec3f &c) { + return (b - a).cross(c - a).norm(); + }; + std::vector source_area2; + if (paint) { + source_area2.resize(ntri); + for (size_t i = 0; i < ntri; ++i) + source_area2[i] = tri_area2(its.vertices[size_t(its.indices[i][0])], + its.vertices[size_t(its.indices[i][1])], + its.vertices[size_t(its.indices[i][2])]); + } + + bool any_paint = false; + for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) { + const TriangleSelector::TriangleSplittingData &data = facets[size_t(slot)]; + if (!TriangleSelector::has_facets(data, EnforcerBlockerType::ENFORCER)) + continue; + + // The refine region is exactly the original triangles the brush touched. `triangles_to_split` + // lists precisely those: serialize() records an entry for every original triangle that is + // either split (i.e. partially painted, which is the patch boundary) or carries a non-default + // state (fully painted). No dilation - an earlier version marked every triangle sharing a + // *vertex* with the patch, which on a coarse model pulls in a whole fan of huge unpainted + // neighbours and then refines them to the resolution floor, since the height field the detail + // test samples is not restricted to the painted area. The conformal closure inside + // subdivide_mesh_adaptive() already grades the size change outward on its own. + for (const TriangleSelector::TriangleBitStreamMapping &m : data.triangles_to_split) + if (size_t(m.triangle_idx) < ntri) + region[m.triangle_idx] |= REFINE_PAINTED; + + if (paint) { + TriangleSelector sel(mesh); + sel.deserialize(data, false); + // get_facets_strict() now reports which source triangle each sub-triangle came from, which + // is what lets partial coverage be carried forward geometrically instead of being rounded + // away. Note a *fully* painted source can still come back as several sub-triangles (T-joint + // splits forced by a refined neighbour), so "wholly painted" is an area test, not a + // one-piece test. + std::vector src; + const indexed_triangle_set patch = sel.get_facets_strict(EnforcerBlockerType::ENFORCER, &src); + + LayerPaintMap &pm = (*paint)[slot]; + pm.full.assign(ntri, 0); + pm.part_start.assign(ntri + 1, 0); + + std::vector covered2(ntri, 0.f); + for (size_t j = 0; j < patch.indices.size() && j < src.size(); ++j) { + if (size_t(src[j]) >= ntri) + continue; + const stl_triangle_vertex_indices &t = patch.indices[j]; + covered2[size_t(src[j])] += tri_area2(patch.vertices[size_t(t[0])], patch.vertices[size_t(t[1])], + patch.vertices[size_t(t[2])]); + } + for (size_t i = 0; i < ntri; ++i) + pm.full[i] = (source_area2[i] > 0.f && covered2[i] >= 0.999f * source_area2[i]) ? 1 : 0; + + // Only partly covered sources need their pieces kept - a full one answers every query with + // "painted", and an untouched one with "not painted". + for (size_t j = 0; j < patch.indices.size() && j < src.size(); ++j) + if (size_t(src[j]) < ntri && !pm.full[size_t(src[j])]) + ++pm.part_start[size_t(src[j]) + 1]; + for (size_t i = 0; i < ntri; ++i) + pm.part_start[i + 1] += pm.part_start[i]; + pm.part.resize(size_t(pm.part_start[ntri])); + { + std::vector fill(pm.part_start.begin(), pm.part_start.begin() + ntri); + for (size_t j = 0; j < patch.indices.size() && j < src.size(); ++j) { + const size_t S = size_t(src[j]); + if (S >= ntri || pm.full[S]) + continue; + const stl_triangle_vertex_indices &t = patch.indices[j]; + pm.part[size_t(fill[S]++)] = { patch.vertices[size_t(t[0])], patch.vertices[size_t(t[1])], + patch.vertices[size_t(t[2])] }; + } + } + } + any_paint = true; + } + if (!any_paint) + return false; + + // The band straddling the paint's edge. The bake steps the surface from full displacement to zero + // across it, and nothing else in the refinement criteria can see that step: the chord-error + // sampler has no per-point paint test, so just outside the paint it keeps reporting the same + // smooth height field and reports no error at all. Left alone, the transition therefore stays at + // whatever density the input had - which is what makes the rim of an unpainted island a ring of + // big, steeply tilted triangles. + // + // Seeded from the vertices shared by a painted and an unpainted triangle (the actual edge of the + // paint) and grown outward over vertex adjacency, so the band covers both sides of the step. + if (BORDER_BAND_RINGS > 0) { + // Vertex -> incident triangles, CSR-style (counted, prefix-summed, filled). This runs on every + // frame of the subdivide preview's sliders, so it must not allocate a small vector per vertex. + std::vector vstart(nvert + 1, 0); + for (size_t i = 0; i < ntri; ++i) + for (int k = 0; k < 3; ++k) + if (size_t(its.indices[i][k]) < nvert) + ++vstart[size_t(its.indices[i][k]) + 1]; + for (size_t v = 0; v < nvert; ++v) + vstart[v + 1] += vstart[v]; + // static_cast, not size_t(...): the latter parses as a parameter declaration (see the note + // above the identical prefix sum on `part`). + std::vector vtri(static_cast(vstart[nvert]), 0); + { + std::vector fill(vstart.begin(), vstart.begin() + nvert); + for (size_t i = 0; i < ntri; ++i) + for (int k = 0; k < 3; ++k) + if (size_t(its.indices[i][k]) < nvert) + vtri[size_t(fill[size_t(its.indices[i][k])]++)] = int(i); + } + + // A vertex used by both a painted and an unpainted triangle sits exactly on the paint's edge. + std::vector ring_vertex(nvert, 0); + for (size_t v = 0; v < nvert; ++v) { + bool painted = false, unpainted = false; + for (int k = vstart[v]; k < vstart[v + 1]; ++k) + ((region[size_t(vtri[size_t(k)])] & REFINE_PAINTED) ? painted : unpainted) = true; + ring_vertex[v] = (painted && unpainted) ? 1 : 0; + } + + for (int ring = 0; ring < BORDER_BAND_RINGS; ++ring) { + std::vector next = ring_vertex; + for (size_t v = 0; v < nvert; ++v) { + if (!ring_vertex[v]) + continue; + for (int k = vstart[v]; k < vstart[v + 1]; ++k) { + const size_t t = size_t(vtri[size_t(k)]); + region[t] |= REFINE_BORDER; + // Grow through this triangle's other corners, for the following ring. + for (int c = 0; c < 3; ++c) + if (size_t(its.indices[t][c]) < nvert) + next[size_t(its.indices[t][c])] = 1; + } + } + ring_vertex.swap(next); + } + } + return true; +} + +TextureDisplacementFacetsData GLGizmoTextureDisplacement::masks_after_subdivision( + const TriangleMesh &new_mesh, const std::vector &source, + const std::array &paint) +{ + // Children inherit their parent's source triangle, and subdivision only ever adds edge midpoints, + // so every new triangle lies inside its source and on the same surface - which means the source's + // painted *pieces* can be queried directly by point containment. That is what keeps a brush outline + // smooth: rounding each source to wholly painted or not instead leaves a ragged fringe of isolated + // triangles along any curved boundary, and the refined mesh then reproduces that fringe exactly + // rather than hiding it. + TextureDisplacementFacetsData out{}; + const indexed_triangle_set &its = new_mesh.its; + for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) { + const LayerPaintMap &pm = paint[size_t(slot)]; + if (pm.empty()) + continue; + TriangleSelector sel(new_mesh); + for (size_t i = 0; i < source.size() && i < its.indices.size(); ++i) { + const size_t S = size_t(source[i]); + if (S >= pm.full.size()) + continue; + bool painted = pm.full[S] != 0; + if (!painted && pm.part_start[S] != pm.part_start[S + 1]) { + const stl_triangle_vertex_indices &t = its.indices[i]; + const Vec3f centroid = (its.vertices[size_t(t[0])] + its.vertices[size_t(t[1])] + + its.vertices[size_t(t[2])]) / 3.f; + for (int k = pm.part_start[S]; k < pm.part_start[S + 1] && !painted; ++k) + painted = point_in_triangle_coplanar(centroid, pm.part[size_t(k)]); + } + if (painted) + sel.set_facet(int(i), EnforcerBlockerType::ENFORCER); + } + out[size_t(slot)] = sel.serialize(); + } + return out; +} + +double GLGizmoTextureDisplacement::painted_area_mm2(const ModelVolume &mv) +{ + // Keyed on the paint generation, which every stroke and every layer edit raises, so a panel that + // asks for this on every frame walks the mesh only when the answer can have changed. + const std::string key = std::to_string(mv.id().id) + ":" + std::to_string(mv.mesh().facets_count()) + ":" + + std::to_string(m_preview_generation->load()); + if (key == m_painted_area_key) + return m_painted_area_mm2; + + const TriangleMesh &mesh = mv.mesh(); + std::vector region; + double area = 0.; + if (collect_paint_region(mesh, facets_data_of(mv), region, nullptr)) { + // In the frame the bake refines in, so a scaled instance is measured at the size it prints at. + const Transform3d frame = texture_displacement_bake_frame(texture_displacement_volume_to_world(mv)); + const indexed_triangle_set &its = mesh.its; + for (size_t t = 0; t < its.indices.size() && t < region.size(); ++t) { + if ((region[t] & REFINE_PAINTED) == 0) + continue; + const stl_triangle_vertex_indices &tri = its.indices[t]; + const Vec3d a = frame * its.vertices[size_t(tri[0])].cast(); + const Vec3d b = frame * its.vertices[size_t(tri[1])].cast(); + const Vec3d c = frame * its.vertices[size_t(tri[2])].cast(); + area += 0.5 * (b - a).cross(c - a).norm(); + } + } + m_painted_area_mm2 = area; + m_painted_area_key = key; + return area; +} + +size_t GLGizmoTextureDisplacement::estimated_refined_triangles(const ModelVolume &mv, float edge_mm) +{ + if (edge_mm <= 0.f) + return 0; + return size_t(4.0 * painted_area_mm2(mv) / (double(edge_mm) * double(edge_mm))); +} + +const V2Resolution &GLGizmoTextureDisplacement::v2_recommendation(const ModelVolume &mv) +{ + // Rebuilt only when something it depends on changes: the surface area scan is O(triangles) and the + // panel asks for this every frame. + std::string key = std::to_string(mv.id().id) + ":" + std::to_string(mv.mesh().facets_count()); + for (const TextureDisplacementLayer &l : mv.texture_displacement_layers) + key += Slic3r::format("|%1%:%2%:%3%:%4%", l.slot, reinterpret_cast(l.image_data.get()), l.tiling_scale, l.depth_mm); + const Transform3d trafo = texture_displacement_volume_to_world(mv); + for (int i = 0; i < 12; ++i) + key += Slic3r::format(",%1%", trafo.matrix()(i / 4, i % 4)); + if (key != m_v2_rec_key) { + m_v2_rec = recommend_v2_resolution(mv.mesh().its, mv.texture_displacement_layers, trafo); + m_v2_rec_key = std::move(key); + } + return m_v2_rec; +} + +TextureDisplacementFacetsData GLGizmoTextureDisplacement::facets_data_of(const ModelVolume &mv) +{ + TextureDisplacementFacetsData out{}; + for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) + out[size_t(i)] = mv.texture_displacement_facet(i).get_data(); + return out; +} + +bool GLGizmoTextureDisplacement::any_layer_colors(const ModelVolume &mv) +{ + for (const TextureDisplacementLayer &layer : mv.texture_displacement_layers) + if (layer.color_enabled && !layer.empty() && decode_height_texture(layer).has_color()) + return true; + return false; +} + +TextureColorSettings GLGizmoTextureDisplacement::color_settings_for(const ModelVolume &mv) +{ + TextureColorSettings out; + if (!any_layer_colors(mv)) + return out; // nothing is colouring: every colour path stays switched off + out.palette = cached_palette(); + out.palette_pure = make_palette(m_palette_filaments, /* mixing */ false); + out.mix_mode = mv.texture_displacement_options.color_mix_mode; + out.despeckle_passes = mv.texture_displacement_options.color_despeckle; + out.layer_height = color_band_mm(mv); + // The dither cell is tied to the colour-detail target: a cell much smaller than a facet cannot be + // drawn at all, and one much larger stops reading as a blend and starts reading as a check. + out.dither_cell_mm = std::max(m_subdivide_color_mm, 0.05f) * 2.f; + return out; +} + +const std::vector &GLGizmoTextureDisplacement::cached_palette() +{ + // Rebuilt only when the loaded filaments or the mixing setting actually change. The shaded preview + // rebuilds on every paint stroke and the subdivide preview on every slider frame, and filling the + // quantizer's lookup cube for a 64-entry palette is tens of milliseconds - paying that per stroke + // is the difference between painting that keeps up and painting that stutters. + const ModelVolume *mv = texture_volume(); + const bool mixing = mv != nullptr && mv->texture_displacement_options.color_mix_enabled; + std::vector filaments = filament_palette(); + if (m_palette_cache.empty() || filaments != m_palette_filaments || mixing != m_palette_mixing) { + m_palette_filaments = std::move(filaments); + m_palette_mixing = mixing; + m_palette_cache = make_palette(m_palette_filaments, mixing); + m_palette_quantizer = make_palette_quantizer(m_palette_cache); + } + return m_palette_cache; +} + +std::vector GLGizmoTextureDisplacement::filament_palette() +{ + std::vector palette = wxGetApp().plater()->get_extruders_colors(); + // mmu_segmentation_facets encodes the filament in a 6-bit prefix code and stops at Extruder16. + if (palette.size() > size_t(EnforcerBlockerType::ExtruderMax)) + palette.resize(size_t(EnforcerBlockerType::ExtruderMax)); + return palette; +} + +float GLGizmoTextureDisplacement::color_band_mm(const ModelVolume &mv) +{ + const float lh = print_layer_height(); + const float edge = (mv.texture_displacement_options.v2_refine_mm > 0.f) ? mv.texture_displacement_options.v2_refine_mm + : v2_recommendation(mv).edge_mm; + if (edge <= 0.f || lh <= 0.f) + return lh; + // A refined triangle of edge e stacks in rows about 0.87 * e apart (an equilateral triangle's + // height), and a dither needs at least two rows per period to be a dither at all. + constexpr float ROW_PER_EDGE = 0.87f; + return lh * std::max(1.f, std::ceil(2.f * ROW_PER_EDGE * edge / lh)); +} + +float GLGizmoTextureDisplacement::print_layer_height() +{ + try { + const DynamicPrintConfig &cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; + if (const ConfigOptionFloat *opt = cfg.option("layer_height"); opt != nullptr) + if (opt->value > 1e-3) + return float(opt->value); + } catch (...) { + } + return 0.2f; +} + +std::vector GLGizmoTextureDisplacement::make_palette( + const std::vector &filaments, bool mixing) +{ + std::vector out; + const int n = int(filaments.size()); + for (int i = 0; i < n; ++i) + out.push_back({ Vec3f(filaments[size_t(i)].r(), filaments[size_t(i)].g(), filaments[size_t(i)].b()), + i, i, 1, 1 }); + if (!mixing || n < 2) + return out; + + // How many intermediate steps each pair gets, chosen so the whole palette stays under + // PALETTE_MAX_ENTRIES. Fewer filaments means more room for mixes, which is also what you want: + // with two filaments the mixes are the only way to get anywhere, and with sixteen there is little + // point mixing at all. `den` is also the band/dither repeat, so a small one is a short pattern. + const int pairs = n * (n - 1) / 2; + int steps = 0; + for (int s = 5; s >= 1; --s) + if (n + pairs * s <= PALETTE_MAX_ENTRIES) { + steps = s; + break; + } + if (steps == 0) + return out; + const int den = steps + 1; + + for (int i = 0; i < n; ++i) + for (int j = i + 1; j < n; ++j) { + const Vec3f lab_i = srgb_to_lab(Vec3f(filaments[size_t(i)].r(), filaments[size_t(i)].g(), filaments[size_t(i)].b())); + const Vec3f lab_j = srgb_to_lab(Vec3f(filaments[size_t(j)].r(), filaments[size_t(j)].g(), filaments[size_t(j)].b())); + for (int k = 1; k <= steps; ++k) { + // k/den of filament i, the rest of j - averaged in Lab, which is what the eye does + // when the two are interleaved too finely to resolve. + const float t = float(k) / float(den); + out.push_back({ lab_to_srgb(lab_i * t + lab_j * (1.f - t)), i, j, k, den }); + } + } + return out; +} + +ColorResolveFn GLGizmoTextureDisplacement::make_mix_resolver(const std::vector &palette, + ColorMixMode mode, float layer_height, + float cell_mm) +{ + if (palette.empty()) + return nullptr; + auto entries = std::make_shared>(palette); + const float band = std::max(layer_height, 0.01f); + const float cell = std::max(cell_mm, 0.01f); + + return [entries, mode, band, cell](int index, const Vec3f &pos, const Vec3f &normal) -> int { + if (index < 0 || size_t(index) >= entries->size()) + return -1; + const PaletteEntry &e = (*entries)[size_t(index)]; + if (!e.is_mix()) + return e.a; + + // Which of the two filaments this point falls on. Both patterns are *ordered*, never random: + // the eye blends a regular pattern into a flat colour, and turns a random one into noise. + // Auto: bands wherever the surface is steeper than ~45 degrees - consecutive layers alternate + // there, which is how a blend prints and reads. On a flat-facing surface a layer is one band + // and the only way to interleave is a checkerboard across the surface, which at print scale + // reads as a pattern rather than a colour; there the mix falls back to its dominant filament. + const bool upright = std::abs(normal.z()) < 0.7f; + if (mode == ColorMixMode::Auto && !upright) + return e.num * 2 >= e.den ? e.a : e.b; + const bool bands = mode == ColorMixMode::ZBands || mode == ColorMixMode::Auto; + if (bands) { + // One band per print layer. floorf, not a cast, so this stays correct below z = 0. + const int slot = int(std::floor(pos.z() / band)); + const int phase = ((slot % e.den) + e.den) % e.den; + return phase < e.num ? e.a : e.b; + } + // Ordered 4x4 Bayer over the surface, indexed by position so the pattern is stable in space + // rather than in triangle order (which would move under any remesh, and read as noise). + static const int BAYER[16] = { 0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5 }; + const int gx = ((int(std::floor(pos.x() / cell)) % 4) + 4) % 4; + const int gy = ((int(std::floor(pos.y() / cell)) % 4) + 4) % 4; + // A third axis would be ideal, but the two dominant ones are enough for a surface pattern and + // keep the cell square on the faces that matter. + const float threshold = (float(BAYER[gy * 4 + gx]) + 0.5f) / 16.f; + return (float(e.num) / float(e.den)) > threshold ? e.a : e.b; + }; +} + +ColorQuantizeFn GLGizmoTextureDisplacement::make_palette_quantizer(const std::vector &palette) +{ + if (palette.empty()) + return nullptr; + + // Lab once per entry, not once per lookup. + struct Lab { float l, a, b; }; + std::vector palette_lab(palette.size()); + for (size_t i = 0; i < palette.size(); ++i) { + const Vec3f lab = srgb_to_lab(palette[i].rgb); + palette_lab[i] = { lab.x(), lab.y(), lab.z() }; + } + + constexpr int E = PALETTE_LUT_EDGE; + auto lut = std::make_shared>(size_t(E) * E * E, 0); + tbb::parallel_for(tbb::blocked_range(0, E), [&](const tbb::blocked_range &range) { + for (int r = range.begin(); r < range.end(); ++r) + for (int g = 0; g < E; ++g) + for (int b = 0; b < E; ++b) { + // Cell centre, so the quantization error is symmetric across the cell. + float l0, a0, b0; + const Vec3f lab0 = srgb_to_lab(Vec3f((r + 0.5f) / E, (g + 0.5f) / E, (b + 0.5f) / E)); + l0 = lab0.x(); a0 = lab0.y(); b0 = lab0.z(); + int best = 0, best_pure = -1; + float best_d = std::numeric_limits::max(), best_pure_d = best_d; + for (size_t i = 0; i < palette_lab.size(); ++i) { + const float d = DeltaE00(l0, a0, b0, palette_lab[i].l, palette_lab[i].a, palette_lab[i].b); + if (d < best_d) { + best_d = d; + best = int(i); + } + if (!palette[i].is_mix() && d < best_pure_d) { + best_pure_d = d; + best_pure = int(i); + } + } + // A mix is an interleave that only reads as its colour from a distance; up close + // it is stripes. Spend it only where it buys a clearly better match than the nearest + // single filament: ten Delta E is a visible step, less is not worth the stripes. + constexpr float PREFER_PURE_DE = 10.f; + if (best_pure >= 0 && palette[size_t(best)].is_mix() && best_pure_d - best_d < PREFER_PURE_DE) + best = best_pure; + (*lut)[(size_t(r) * E + size_t(g)) * E + size_t(b)] = uint8_t(best); + } + }); + + return [lut](const Vec3f &rgb) -> int { + constexpr int E = PALETTE_LUT_EDGE; + const int r = std::clamp(int(rgb.x() * E), 0, E - 1); + const int g = std::clamp(int(rgb.y() * E), 0, E - 1); + const int b = std::clamp(int(rgb.z() * E), 0, E - 1); + return int((*lut)[(size_t(r) * E + size_t(g)) * E + size_t(b)]); + }; +} + +TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh( + const indexed_triangle_set &base, const TextureDisplacementFacetsData &masks, + const std::vector &layers, const TextureDisplacementPrepareParams ¶ms, + const std::vector &palette, const DisplacementProgressFn &progress, + BakeStageRecorder *debug) +{ + TextureDisplacementPrepareResult out; + const auto report = [&progress](int pct) { return !progress || progress(pct); }; + + // Stage capture for the debug view. Timed from the end of the previous stage, so the capture - + // a copy plus an edge scan - never lands inside the measurement it reports. + auto stage_clock = std::chrono::steady_clock::now(); + const auto capture = [&](const char *name, const indexed_triangle_set &m, + const std::string &detail = {}) { + if (debug == nullptr) + return; + const double ms = + std::chrono::duration(std::chrono::steady_clock::now() - stage_clock).count(); + debug->capture(name, m.vertices, m.indices, ms, detail); + stage_clock = std::chrono::steady_clock::now(); + }; + capture("input", base, "as handed to prepare"); + + TriangleMesh mesh(base); + TextureDisplacementFacetsData current = masks; + bool changed = false; + bool had_paint = false; + for (const TriangleSelector::TriangleSplittingData &m : masks) + had_paint = had_paint || TriangleSelector::has_facets(m, EnforcerBlockerType::ENFORCER); + if (!report(1)) + return out; + + // 1. Even out the triangle density, and carry the paint onto the result. Everything downstream is + // driven by that paint, so losing it is a hard stop rather than something to bake around - and + // stopping here, before anything is committed, leaves the user's model exactly as it was. + if (params.remesh_edge_mm > 0.f) { + indexed_triangle_set remeshed; + if (plan_remesh(mesh.its, params.remesh_edge_mm, params.remesh_sharp_deg, remeshed)) { + TriangleMesh new_mesh(std::move(remeshed)); + const AABBTreeIndirect::Tree3f old_tree = + AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(mesh.its.vertices, mesh.its.indices); + TextureDisplacementFacetsData carried{}; + bool any = false; + for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) { + // Both meshes are the volume's own local frame, so there is no shift between them. + carried[size_t(i)] = + remap_texture_paint_spatial(mesh, current[size_t(i)], old_tree, new_mesh, Vec3f::Zero()); + any = any || !carried[size_t(i)].bitstream.empty(); + } + mesh = std::move(new_mesh); + current = std::move(carried); + changed = true; + capture("remesh", mesh.its, + "target " + std::to_string(params.remesh_edge_mm) + " mm" + + (had_paint && !any ? ", paint lost" : "")); + if (had_paint && !any) { + out.paint_lost = true; + return out; + } + } else if (debug != nullptr) { + capture("remesh", mesh.its, "skipped: remeshing changed nothing"); + } + } + if (!report(50)) + return {}; + + // 2. Refine where the texture bends - the painted area only, plus the graded band the conformal + // closure pulls in around it. + if (params.subdiv_target_mm > 0.f) { + std::vector region; + std::array paint; + if (collect_paint_region(mesh, current, region, &paint)) { + // Feature-adaptive: sample the combined displacement so refinement follows texture + // curvature. A null sampler (only LSCM layers, or nothing decodable) falls back to the + // length baseline alone. + HeightFieldSampler sampler; + if (params.subdiv_feature) + sampler = make_combined_displacement_sampler(mesh.its, layers, current); + // Colour boundaries need triangles of their own - the chord test cannot see them, since + // the height field is perfectly smooth across a change of filament. + ColorFieldSampler color; + if (params.subdiv_color_edge_mm > 0.f && !palette.empty()) + color = make_combined_color_sampler(mesh.its, layers, current, make_palette_quantizer(palette)); + // Note the sampler is built on the *quantizer* alone - the refinement follows perceived + // colour, never the interleaving that realises a mix (see ColorResolveFn). + // "Min edge" is a feature-mode control (it is the floor the curvature test refines down + // to); in plain adaptive mode the target edge length is the only criterion, so the floor + // must not be allowed to silently override a target the user set below it. + const float tol = params.subdiv_feature ? params.subdiv_detail_mm : 0.f; + const float floor = params.subdiv_feature ? params.subdiv_min_edge_mm : 0.f; + // Step textures (a grid, a knurl: two levels with sharp edges between them) are cut into + // walls after refining rather than refined into ramps, see 3. below. The verdict is taken + // here, on the coarse mesh, because the refinement has to know: in step mode it leaves + // the stepped triangles to the cutter, which is only right if the cutter then cuts them. + // The step width and the seam gap both span two texels of the finest layer: the bilinear + // blend across a step is a texel wide, so a seam copy a texel from the step samples a + // pure side. + float texel_mm = std::numeric_limits::max(); + if (params.cut_steps && sampler) + for (const TextureDisplacementLayer &layer : layers) { + if (layer.empty()) + continue; + const DecodedHeightTexture &tex = decode_height_texture(layer); + if (tex.width > 0 && layer.tiling_scale > 0.f) + texel_mm = std::min(texel_mm, layer.tiling_scale / float(tex.width)); + } + const bool cut_steps = texel_mm < std::numeric_limits::max() && + texture_has_steps_to_cut(mesh.its, region, sampler, 2.f * texel_mm, 2.f * texel_mm); + bool aborted = false; + std::vector source; + // The budget is "triangles the refinement may *add*", so the mesh's own count is the + // baseline - otherwise the control would be meaningless (or a dead end) on a dense model. + indexed_triangle_set refined = + subdivide_mesh_adaptive(mesh.its, region, params.subdiv_target_mm, + int(mesh.its.indices.size()) + params.subdiv_added_triangles, &source, + sampler, tol, floor, params.subdiv_border_mm, [&](int pct) { + aborted = !report(50 + pct / 2); + return !aborted; + }, + color, params.subdiv_color_edge_mm, cut_steps); + if (aborted) + return {}; + if (refined.indices.size() != mesh.its.indices.size()) { + mesh = TriangleMesh(std::move(refined)); + current = masks_after_subdivision(mesh, source, paint); + changed = true; + capture("adaptive subdivide", mesh.its, + params.subdiv_feature ? "feature adaptive" : "length only"); + } else if (debug != nullptr) { + capture("adaptive subdivide", mesh.its, "skipped: already meets the criteria"); + } + + // 3. Cut the painted area along the texture's sharp steps so they bake as walls. The + // refinement above left the stepped triangles alone, so this is where a binary + // texture's edges get their geometry. Paint is carried the same way as through the + // subdivision: every output triangle lies inside the pre-subdivision triangle it + // descends from, so the source maps compose. + if (cut_steps) { + std::vector cut_region(mesh.its.indices.size(), 0); + for (size_t i = 0; i < cut_region.size() && i < source.size(); ++i) + cut_region[i] = (region[size_t(source[i])] & REFINE_PAINTED) ? 1 : 0; + const HeightFieldSampler refined_sampler = + make_combined_displacement_sampler(mesh.its, layers, current); + std::vector cut_source; + size_t cuts = 0; + indexed_triangle_set cut = cut_mesh_at_steps(mesh.its, cut_region, refined_sampler, + 2.f * texel_mm, 2.f * texel_mm, 0.f, + &cut_source, &cuts); + if (cuts > 0) { + for (int &s : cut_source) + s = source[size_t(s)]; + mesh = TriangleMesh(std::move(cut)); + current = masks_after_subdivision(mesh, cut_source, paint); + changed = true; + capture("step cut", mesh.its, std::to_string(cuts) + " triangles cut"); + } else if (debug != nullptr) { + capture("step cut", mesh.its, "skipped: the refined mesh has no steps left to cut"); + } + } else if (debug != nullptr && params.cut_steps) { + capture("step cut", mesh.its, "skipped: not a step texture"); + } + } + } + if (!report(100) || !changed) + return out; // an empty result: nothing to commit, which is not a failure + + out.mesh = std::move(mesh.its); + out.masks = std::move(current); + return out; +} + +void GLGizmoTextureDisplacement::subdivide_model_adaptive() +{ + ModelVolume *mv = texture_volume(); + if (mv == nullptr || m_subdivide_target_mm <= 0.f) + return; + + update_model_object(); // flush any in-progress stroke into the committed masks first + + if (!mv->is_texture_displacement_painted()) { + show_error(nullptr, _u8L("Paint the area you want to subdivide first - adaptive subdivision only " + "refines where you have painted.")); + return; + } + + TextureDisplacementPrepareParams params; + params.subdiv_target_mm = m_subdivide_target_mm; + params.subdiv_detail_mm = m_subdivide_detail_mm; + params.subdiv_min_edge_mm = m_subdivide_min_edge_mm; + params.subdiv_border_mm = m_subdivide_border_mm; + params.subdiv_color_edge_mm = m_subdivide_color_mm; + params.subdiv_feature = m_subdivide_feature; + params.subdiv_added_triangles = m_subdivide_budget_k * 1000; + queue_prepare(params, _u8L("Adaptive subdivide for texture displacement"), /* then_bake */ false, + _u8L("Nothing to subdivide - the painted area already meets the target edge length and " + "detail tolerance, or the triangle budget is already used up.")); +} + +void GLGizmoTextureDisplacement::smooth_model() +{ + ModelVolume *mv = texture_volume(); + ModelObject *mo = m_c->selection_info()->model_object(); + if (mv == nullptr || mo == nullptr) + return; + const TextureDisplacementOptions &opts = mv->texture_displacement_options; + if (opts.smooth_strength <= 0.f || opts.smooth_iterations <= 0) + return; + + update_model_object(); // flush any in-progress stroke, so the painted region below is current + + // Movable = the vertices of the painted triangles, so the first ring of purely unpainted vertices + // outside them stays put and anchors the result. Restricted rather than whole-model on purpose: + // this runs on geometry that has already been baked, and relaxing the whole thing would quietly + // round off every unrelated feature on the part. With "Ignore outer ring" the patch's own rim is + // held as well - see TextureDisplacementOptions::smooth_skip_border. + const indexed_triangle_set &its = mv->mesh().its; + std::vector painted_face(its.indices.size(), 0); + bool any = false; + for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) + for (const TriangleSelector::TriangleBitStreamMapping &m : mv->texture_displacement_facet(slot).get_data().triangles_to_split) + if (size_t(m.triangle_idx) < its.indices.size()) { + painted_face[size_t(m.triangle_idx)] = 1; + any = true; + } + if (!any) { + show_error(nullptr, _u8L("Paint the area you want to smooth first - smoothing only touches the " + "painted part of the model.")); + return; + } + std::vector movable(its.vertices.size(), 0); + for (size_t i = 0; i < its.indices.size(); ++i) + if (painted_face[i]) + for (int k = 0; k < 3; ++k) + movable[size_t(its.indices[i][k])] = 1; + if (opts.smooth_skip_border) + for (size_t i = 0; i < its.indices.size(); ++i) + if (!painted_face[i]) + for (int k = 0; k < 3; ++k) + movable[size_t(its.indices[i][k])] = 0; // also used by unpainted geometry -> the rim + if (std::none_of(movable.begin(), movable.end(), [](uint8_t m) { return m != 0; })) { + // Every painted vertex is on the patch's rim, so "Ignore outer ring" leaves nothing to move. + show_error(nullptr, _u8L("Nothing to smooth - the painted area is only one triangle deep, so with " + "\"Ignore outer ring\" on there are no interior vertices to relax.")); + return; + } + + indexed_triangle_set smoothed = its; + { + wxBusyCursor wait; + smooth_mesh_vertices(smoothed, movable, opts.smooth_strength, opts.smooth_iterations); + } + + Plater *plater = wxGetApp().plater(); + Plater::TakeSnapshot snapshot(plater, _u8L("Smooth texture displacement"), UndoRedo::SnapshotType::GizmoAction); + + // No save/restore-painting dance here, unlike subdivide and remesh: smoothing only moves vertices, + // it does not touch the triangle list, so every paint channel still refers to exactly the triangles + // it did before. set_mesh() clears the extra facets, so this saves and puts back the + // texture-displacement masks verbatim - no remap needed, and none of them is lost. + std::optional saved_painting = mv->save_painting(); + std::array saved_texture; + for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) + saved_texture[size_t(i)] = mv->texture_displacement_facet(i).get_data(); + + mv->set_mesh(TriangleMesh(std::move(smoothed))); + mv->set_new_unique_id(); + mv->calculate_convex_hull(); + mv->restore_painting(saved_painting); + for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) + mv->texture_displacement_facet(i).set_data(std::move(saved_texture[size_t(i)])); + + if (ObjectList *obj_list = wxGetApp().obj_list()) { + const ModelObjectPtrs &objs = plater->model().objects; + auto it = std::find(objs.begin(), objs.end(), mo); + if (it != objs.end()) + obj_list->update_info_items(size_t(it - objs.begin())); + } + plater->changed_object(*mo); + update_from_model_object(false); + m_parent.set_as_dirty(); +} + +bool GLGizmoTextureDisplacement::plan_remesh(const indexed_triangle_set &src, float target_edge_mm, + float sharp_angle_deg, indexed_triangle_set &out) +{ + if (target_edge_mm <= 0.f) + return false; + + // Its cost and memory grow with the *square* of 1/target_edge, and it runs single-threaded on the + // UI thread, so the target has to be bounded against the part's actual surface area rather than + // taken at face value. Standard mode asks for 1 mm whatever it is handed, and the slider goes down + // to 0.1 mm: on a 150 mm part those are ~500 k and ~50 M triangles respectively - minutes to hours + // of frozen UI, which is indistinguishable from a hang. Raising the target instead still gives the + // subdivider the even density it needs, and the subdivision that follows is where the detail was + // always going to come from anyway. An equilateral triangle of edge L covers sqrt(3)/4 * L^2. + static constexpr double REMESH_MAX_TRIANGLES = 150000.0; + double area = 0.0; + for (const stl_triangle_vertex_indices &tri : src.indices) + area += 0.5 * double((src.vertices[size_t(tri[1])] - src.vertices[size_t(tri[0])]) + .cross(src.vertices[size_t(tri[2])] - src.vertices[size_t(tri[0])]) + .norm()); + if (const double budget_edge = std::sqrt(area / (0.4330127 * REMESH_MAX_TRIANGLES)); + budget_edge > double(target_edge_mm)) { + BOOST_LOG_TRIVIAL(info) << "Texture displacement: remesh target raised from " << target_edge_mm + << " mm to " << budget_edge << " mm to stay within " + << int(REMESH_MAX_TRIANGLES) << " triangles."; + target_edge_mm = float(budget_edge); + } + + // Five iterations with three relaxation passes each, rather than CGAL's three-and-one. Splitting + // and collapsing bring edge lengths near the target but leave the vertices where they fell, so it + // is the relaxation count that decides how even the result looks - and three passes in total was + // nowhere near enough. Fifteen costs proportionally more, but only on an explicit Remesh. + indexed_triangle_set remeshed = MeshBoolean::cgal::remesh_isotropic( + src, double(target_edge_mm), /* iterations */ 5, double(sharp_angle_deg), + /* relaxation steps */ 3); + // remesh_isotropic() signals failure by handing the input straight back, so compare against it + // structurally. Vertex count alone is not enough: a remesh that only redistributes triangles at + // roughly the current density legitimately lands on the same count, and treating that as failure + // meant a perfectly good result got thrown away with an error message. + if (remeshed.indices.empty() || + (remeshed.vertices.size() == src.vertices.size() && remeshed.indices.size() == src.indices.size() && + remeshed.indices == src.indices)) + return false; + + out = std::move(remeshed); + return true; +} + +void GLGizmoTextureDisplacement::remesh_model() +{ + if (texture_volume() == nullptr || m_remesh_target_edge_mm <= 0.f) + return; + + update_model_object(); // the paint rides across the remesh, so flush any in-progress stroke first + + TextureDisplacementPrepareParams params; + params.remesh_edge_mm = m_remesh_target_edge_mm; + params.remesh_sharp_deg = m_remesh_keep_sharp_edges ? m_remesh_sharp_angle_deg : 0.f; + queue_prepare(params, _u8L("Remesh model for texture displacement"), /* then_bake */ false, + _u8L("Remeshing did not change the model. It may be non-manifold (open edges or edges " + "shared by more than two triangles), or the target edge length may already be met.")); +} + +void GLGizmoTextureDisplacement::rebuild_subdivide_preview() +{ + m_subdivide_preview_glmodel.reset(); + m_subdivide_preview_tris = -1; + const ModelVolume *mv = texture_volume(); + if (mv == nullptr) + return; + + // The same subdivision Apply would commit, kept in a throwaway mesh and shown only as a + // wireframe - the model itself is not touched until Apply. + indexed_triangle_set its; + if (m_subdivide_adaptive) { + if (m_subdivide_target_mm <= 0.f) + return; + const TextureDisplacementFacetsData facets = facets_data_of(*mv); + std::vector region; + if (!collect_paint_region(mv->mesh(), facets, region, nullptr)) + return; // nothing painted yet: nothing to preview + HeightFieldSampler sampler; + if (m_subdivide_feature) + sampler = make_combined_displacement_sampler(mv->mesh().its, mv->texture_displacement_layers, facets); + // Feature mode: curvature (Detail tolerance) on top of the Max-edge baseline, down to the + // Min-edge floor. Plain adaptive: tol 0, so only the target-edge-length criterion applies. + const float tol = m_subdivide_feature ? m_subdivide_detail_mm : 0.f; + const float floor = m_subdivide_feature ? m_subdivide_min_edge_mm : 0.f; + // Same colour criterion Apply will use, so the previewed wireframe is the mesh that commits. + ColorFieldSampler color; + if (m_subdivide_color_mm > 0.f && any_layer_colors(*mv)) { + cached_palette(); // refreshes m_palette_quantizer if the filaments changed + color = make_combined_color_sampler(mv->mesh().its, mv->texture_displacement_layers, facets, + m_palette_quantizer); + } + its = subdivide_mesh_adaptive(mv->mesh().its, region, m_subdivide_target_mm, + int(mv->mesh().its.indices.size()) + m_subdivide_budget_k * 1000, + nullptr, sampler, tol, floor, m_subdivide_border_mm, nullptr, color, + m_subdivide_color_mm); + } else { + if (m_subdivide_count < 1) + return; + its = subdivide_mesh_uniform(mv->mesh().its, 0.f, m_subdivide_count); + } + if (its.indices.empty()) + return; + m_subdivide_preview_tris = int(its.indices.size()); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(its.vertices.size()); + init_data.reserve_indices(its.indices.size() * 6); + for (const Vec3f &v : its.vertices) + init_data.add_vertex(v); + for (const stl_triangle_vertex_indices &tri : its.indices) + for (int i = 0; i < 3; ++i) + init_data.add_line(unsigned(tri[i]), unsigned(tri[(i + 1) % 3])); + if (!init_data.is_empty()) + m_subdivide_preview_glmodel.init_from(std::move(init_data)); +} + +void GLGizmoTextureDisplacement::render_subdivide_preview() +{ + const ModelObject *mo = m_c->selection_info()->model_object(); + const ModelVolume *mv = texture_volume(); + if (mo == nullptr || mv == nullptr || !m_subdivide_preview_glmodel.is_initialized()) + return; + GLShaderProgram *shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + const Selection &selection = m_parent.get_selection(); + const Transform3d trafo_matrix = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix(); + const Camera &camera = wxGetApp().plater()->get_camera(); + + shader->start_using(); + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * trafo_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + glsafe(::glEnable(GL_POLYGON_OFFSET_LINE)); + glsafe(::glPolygonOffset(-1.0f, -1.0f)); + m_subdivide_preview_glmodel.set_color(ColorRGBA(0.2f, 0.9f, 1.0f, 0.7f)); // cyan, reads as "preview" + m_subdivide_preview_glmodel.render(); + glsafe(::glDisable(GL_POLYGON_OFFSET_LINE)); + shader->stop_using(); +} + +GLTexture *GLGizmoTextureDisplacement::get_layer_thumbnail(const TextureDisplacementLayer &layer) +{ + if (layer.empty() || layer.slot < 0 || size_t(layer.slot) >= TEXTURE_DISPLACEMENT_MAX_LAYERS) + return nullptr; + + const size_t slot = size_t(layer.slot); + if (m_thumbnails[slot] && m_thumbnail_source[slot] == layer.image_data.get() && + m_thumbnail_smoothing[slot] == layer.smoothing) + return m_thumbnails[slot].get(); + + // Reuses the already-decoded, already-cached grayscale pixels (see decode_height_texture()'s + // own cache in TextureDisplacement.cpp) - only the gray-to-RGBA expansion and GPU upload below are + // new work. Rebuilt when the texture *or the smoothing* changes, so the fast/shaded preview - which + // samples this GPU texture directly - reflects the current smoothing rather than the raw image. + std::unique_ptr texture = upload_height_thumbnail(decode_height_texture(layer)); + if (!texture) + return nullptr; + + m_thumbnails[slot] = std::move(texture); + m_thumbnail_source[slot] = layer.image_data.get(); + m_thumbnail_smoothing[slot] = layer.smoothing; + return m_thumbnails[slot].get(); +} + +GLTexture *GLGizmoTextureDisplacement::get_layer_color_texture(const TextureDisplacementLayer &layer) +{ + if (layer.empty() || !layer.color_enabled) + return nullptr; + if (m_color_tex && m_color_tex_source == layer.image_data.get() && m_color_tex_smoothing == layer.smoothing) + return m_color_tex.get(); + + std::unique_ptr texture = upload_color_texture(decode_height_texture(layer), HEIGHT_TEX_MAX_PX); + if (!texture) { + m_color_tex.reset(); + m_color_tex_source = nullptr; + return nullptr; + } + m_color_tex = std::move(texture); + m_color_tex_source = layer.image_data.get(); + m_color_tex_smoothing = layer.smoothing; + return m_color_tex.get(); +} + +GLTexture *GLGizmoTextureDisplacement::get_layer_height_texture(const TextureDisplacementLayer &layer) +{ + if (layer.empty()) + return nullptr; + + // One slot, not one per layer: the preview shader only ever shades the *active* layer, so a single + // full-resolution upload is enough and the VRAM cost stays at one texture rather than eight. + if (m_height_tex && m_height_tex_source == layer.image_data.get() && m_height_tex_smoothing == layer.smoothing) + return m_height_tex.get(); + + std::unique_ptr texture = upload_height_thumbnail(decode_height_texture(layer), HEIGHT_TEX_MAX_PX); + if (!texture) + return nullptr; + + m_height_tex = std::move(texture); + m_height_tex_source = layer.image_data.get(); + m_height_tex_smoothing = layer.smoothing; + return m_height_tex.get(); +} + +void GLGizmoTextureDisplacement::bake(bool own_snapshot) +{ + ModelVolume *mv = texture_volume(); + if (!mv || m_bake_in_progress) + return; + + // Make sure the currently active layer's in-progress edits are flushed into the model before + // baking, otherwise the most recent, not-yet-committed strokes would be silently skipped. + update_model_object(); + + if (!mv->is_texture_displacement_painted()) { + show_error(nullptr, _u8L("Nothing is painted, there is nothing to bake.")); + return; + } + + m_bake_in_progress = true; + queue_texture_displacement_bake(*mv, color_settings_for(*mv), [this]() { + m_bake_in_progress = false; + // Baking replaces the volume's mesh (new id, new topology) without changing the object's + // id or volume count, so GLGizmoPainterBase::data_changed()'s usual change-detection never + // notices it needs to reload - do it explicitly here, otherwise the gizmo keeps painting + // and rendering against the stale, pre-bake TriangleSelectorPatch until the object is + // deselected and reselected. + if (m_state == On && m_c->selection_info() && m_c->selection_info()->model_object()) + update_from_model_object(false); + m_parent.set_as_dirty(); + }, own_snapshot); +} + +// Standard mode's fixed recipe. These are both the values the hidden controls are pinned to and what +// bake_standard() drives its remesh and subdivision with - one set of numbers, so the preview cannot +// disagree with the bake. Chosen to be safe on an arbitrary imported part rather than optimal on any +// particular one: a 1 mm isotropic remesh gives the subdivider an even starting density whatever the +// input looked like, and the subdivision then spends up to 1.5 M triangles chasing texture curvature +// down to a 0.02 mm floor, which is finer than any FDM nozzle will resolve. The triangle budget is not +// here: it is the one control Standard mode still shows, so it belongs to the user (its default is +// m_subdivide_budget_k's initialiser). +static constexpr float STD_REMESH_EDGE_MM = 1.0f; +static constexpr float STD_REMESH_SHARP_DEG = 40.f; +static constexpr float STD_SUBDIV_MAX_EDGE_MM = 20.f; +static constexpr float STD_SUBDIV_DETAIL_MM = 0.02f; +// 0.1 mm rather than the 0.02 it was: a hard step in the texture never satisfies the chord tolerance +// however finely it is split, so the floor is what stops it, and 0.02 mm carpeted every step edge with +// triangles five times finer than a nozzle can print - most of the budget, on almost every texture. +static constexpr float STD_SUBDIV_MIN_EDGE_MM = 0.1f; +// Edge length the band straddling the paint's edge is refined to. This is the one number that decides +// how clean the rim of an unpainted island looks: the bake steps the surface from full displacement to +// zero across that band, and nothing else in the criteria can see the step (see collect_paint_region()). +static constexpr float STD_SUBDIV_BORDER_MM = 0.4f; +// Edge length a colour boundary is refined to. Finer than the border band because a colour edge is +// what the eye actually lands on - a stepped outline around a printed decal reads as a defect in a +// way a slightly coarse relief transition does not - and it costs triangles along an outline only. +static constexpr float STD_SUBDIV_COLOR_MM = 0.25f; + +bool GLGizmoTextureDisplacement::apply_standard_mode_presets(ModelVolume *mv) +{ + bool changed = false; + const auto pin = [&changed](auto &field, auto value) { + if (field != value) { + field = value; + changed = true; + } + }; + + if (mv != nullptr) { + pin(mv->texture_displacement_options.displace_border, true); + pin(mv->texture_displacement_options.smooth_enabled, false); + } + pin(m_subdivide_adaptive, true); + pin(m_subdivide_feature, true); + pin(m_subdivide_target_mm, STD_SUBDIV_MAX_EDGE_MM); + pin(m_subdivide_detail_mm, STD_SUBDIV_DETAIL_MM); + pin(m_subdivide_min_edge_mm, STD_SUBDIV_MIN_EDGE_MM); + pin(m_subdivide_border_mm, STD_SUBDIV_BORDER_MM); + pin(m_subdivide_color_mm, STD_SUBDIV_COLOR_MM); + // Deliberately *not* pinned: the triangle budget stays visible and editable in Standard mode, so + // pinning it would fight the user's own slider every frame. + pin(m_remesh_target_edge_mm, STD_REMESH_EDGE_MM); + pin(m_remesh_keep_sharp_edges, true); + pin(m_remesh_sharp_angle_deg, STD_REMESH_SHARP_DEG); + return changed; +} + +void GLGizmoTextureDisplacement::bake_standard() +{ + ModelVolume *mv = texture_volume(); + if (mv == nullptr || m_bake_in_progress || m_prepare_in_progress) + return; + + update_model_object(); // flush the active layer's in-progress strokes before anything reads them + if (!mv->is_texture_displacement_painted()) { + show_error(nullptr, _u8L("Nothing is painted, there is nothing to bake.")); + return; + } + apply_standard_mode_presets(mv); // belt and braces: never bake with values the panel is not showing + + // It refines as part of the bake, so preparing first would refine a second time at another + // target. + if (mv->texture_displacement_options.pipeline_v2) { + bake(); + return; + } + + // The whole recipe in one go. Either stage having nothing to do is normal, not a failure - a mesh + // that is already even needs no remesh, one that is already fine enough for the texture needs no + // subdivision - so no "nothing changed" message here: it goes straight on to the displacement. + TextureDisplacementPrepareParams params; + params.remesh_edge_mm = STD_REMESH_EDGE_MM; + params.remesh_sharp_deg = STD_REMESH_SHARP_DEG; + params.subdiv_target_mm = STD_SUBDIV_MAX_EDGE_MM; + params.subdiv_detail_mm = STD_SUBDIV_DETAIL_MM; + params.subdiv_min_edge_mm = STD_SUBDIV_MIN_EDGE_MM; + params.subdiv_border_mm = STD_SUBDIV_BORDER_MM; + params.subdiv_color_edge_mm = STD_SUBDIV_COLOR_MM; + params.subdiv_feature = true; + params.subdiv_added_triangles = m_subdivide_budget_k * 1000; + params.cut_steps = true; + queue_prepare(params, _u8L("Bake texture displacement"), /* then_bake */ true, {}); +} + +// --------------------------------------------------------------------------------------------- +// Bake stage debug view +// --------------------------------------------------------------------------------------------- + +void GLGizmoTextureDisplacement::run_stage_debug() +{ + ModelVolume *mv = texture_volume(); + if (mv == nullptr || m_debug_in_progress || m_bake_in_progress || m_prepare_in_progress) + return; + + update_model_object(); // flush the active layer's in-progress strokes, as a real bake would + if (!mv->is_texture_displacement_painted()) { + show_error(nullptr, _u8L("Nothing is painted, so there are no bake stages to capture.")); + return; + } + + // The default (one-run) pipeline refines as part of the bake, and Pro mode has the user prepare the + // mesh themselves with the controls above - in both cases there is no preparation to replay, and + // running one anyway would show stages the Bake button would not. + const bool run_prepare = !pro_mode() && !mv->texture_displacement_options.pipeline_v2; + + TextureDisplacementPrepareParams params; + if (run_prepare) { + apply_standard_mode_presets(mv); // capture what the panel is showing, exactly as Bake does + params.remesh_edge_mm = STD_REMESH_EDGE_MM; + params.remesh_sharp_deg = STD_REMESH_SHARP_DEG; + params.subdiv_target_mm = STD_SUBDIV_MAX_EDGE_MM; + params.subdiv_detail_mm = STD_SUBDIV_DETAIL_MM; + params.subdiv_min_edge_mm = STD_SUBDIV_MIN_EDGE_MM; + params.subdiv_border_mm = STD_SUBDIV_BORDER_MM; + params.subdiv_color_edge_mm = STD_SUBDIV_COLOR_MM; + params.subdiv_feature = true; + params.subdiv_added_triangles = m_subdivide_budget_k * 1000; + params.cut_steps = true; + } + + // Free the previous run's meshes before the next one allocates its own: every stage is a full + // copy of the mesh, so holding two runs at once doubles what is already the expensive part. + exit_debug_view(); + + m_debug_in_progress = true; + queue_texture_displacement_debug( + *mv, color_settings_for(*mv), params, run_prepare, m_debug_check_topology, + [this](std::vector stages) { + m_debug_in_progress = false; + if (!stages.empty()) { + m_debug_stages = std::move(stages); + // Land on the last stage: that is what a real bake would have committed, and the + // question is usually what it looks like before stepping back to find where it went + // wrong. + show_debug_stage(int(m_debug_stages.size()) - 1); + } + m_parent.set_as_dirty(); + }); +} + +void GLGizmoTextureDisplacement::show_debug_stage(int index) +{ + if (index < 0 || size_t(index) >= m_debug_stages.size()) + return; + const BakeStageSnapshot &stage = m_debug_stages[size_t(index)]; + if (stage.mesh.empty()) + return; // over the memory cap: its counts are still listed, there is just nothing to draw + + m_debug_stage = index; + // Drawn through the ordinary true-displacement preview, so the Fast view - a shader trick over + // the base mesh that never draws m_preview_glmodel - has to be left first. + m_use_shaded_preview = false; + m_preview_color_runs.clear(); + m_preview_glmodel.reset(); + + indexed_triangle_set its; + its.vertices = stage.mesh.vertices; + its.indices = stage.mesh.indices; + m_preview_glmodel.init_from(its); + m_preview_glmodel.set_color(GLVolume::NEUTRAL_COLOR); + // Kept so the wireframe overlay draws this stage's edges rather than the base mesh's - which is + // most of the point, since what a refinement stage did is a change in the edges, not the surface. + m_preview_its = std::move(its); + refresh_wireframe(); + m_parent.set_as_dirty(); +} + +void GLGizmoTextureDisplacement::exit_debug_view() +{ + const bool was_showing = m_debug_stage >= 0; + m_debug_stage = -1; + m_debug_stages.clear(); + m_debug_stages.shrink_to_fit(); + if (was_showing) + rebuild_preview(); // hands m_preview_glmodel back to the live preview +} + +void GLGizmoTextureDisplacement::render_debug_stage_panel(ModelVolume *mv) +{ + if (mv == nullptr) + return; + + ImGui::Separator(); + if (!ImGui::CollapsingHeader(_u8L("Bake stages (debug)").c_str())) + return; + + m_imgui->disabled_begin(m_debug_in_progress || m_bake_in_progress || m_prepare_in_progress || + !mv->is_texture_displacement_painted()); + if (m_imgui->button(m_debug_in_progress ? _L("Capturing...") : _L("Capture stages"))) + run_stage_debug(); + m_imgui->disabled_end(); + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Run the same bake the Bake button runs, keeping the mesh after every " + "stage, and step through them here. Nothing is committed to the model - " + "this is for finding which stage a bad result came from."), + m_imgui->scaled(20.f)); + + ImGui::SameLine(); + ImGui::Checkbox(_u8L("Check topology").c_str(), &m_debug_check_topology); + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Count open and non-manifold edges after each stage, which is how a stage " + "that tore the mesh is spotted. It scans every edge, so it adds noticeably " + "to the capture on a fine bake."), + m_imgui->scaled(20.f)); + + if (m_debug_stages.empty()) + return; + + // Stage list. Selecting one draws it; the wireframe overlay is what makes a refinement stage + // readable, so it is worth leaving on while stepping. + ImGui::BeginChild("##bake_stages", ImVec2(0.f, m_imgui->scaled(9.f)), true, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize); + for (size_t i = 0; i < m_debug_stages.size(); ++i) { + const BakeStageSnapshot &st = m_debug_stages[i]; + char label[256]; + std::snprintf(label, sizeof(label), "%zu. %-18s %8zu tris %8.1f ms##stage%zu", i, + st.name.c_str(), st.triangles, st.ms, i); + if (ImGui::Selectable(label, int(i) == m_debug_stage) && !st.mesh.empty()) + show_debug_stage(int(i)); + } + ImGui::EndChild(); + + // Prev / next, so stepping does not mean aiming at a list entry. + m_imgui->disabled_begin(m_debug_stage <= 0); + if (m_imgui->button(_L("< Previous"))) + show_debug_stage(m_debug_stage - 1); + m_imgui->disabled_end(); + ImGui::SameLine(); + m_imgui->disabled_begin(m_debug_stage < 0 || size_t(m_debug_stage + 1) >= m_debug_stages.size()); + if (m_imgui->button(_L("Next >"))) + show_debug_stage(m_debug_stage + 1); + m_imgui->disabled_end(); + ImGui::SameLine(); + if (m_imgui->button(_L("Close"))) + exit_debug_view(); + + if (m_debug_stage >= 0 && size_t(m_debug_stage) < m_debug_stages.size()) { + const BakeStageSnapshot &st = m_debug_stages[size_t(m_debug_stage)]; + m_imgui->text(from_u8(st.name) + ": " + std::to_string(st.triangles) + " " + + _u8L("triangles") + ", " + std::to_string(st.vertices) + " " + _u8L("vertices")); + if (!st.detail.empty()) + m_imgui->text(from_u8(st.detail)); + if (st.topology_checked) { + // Zero on both counts is the claim every stage's header makes; anything else is the bug. + const std::string topo = _u8L("Open edges") + ": " + std::to_string(st.open_edges) + " " + + _u8L("Non-manifold") + ": " + std::to_string(st.non_manifold_edges) + + " " + _u8L("Degenerate") + ": " + std::to_string(st.degenerate); + if (st.open_edges > 0 || st.non_manifold_edges > 0) + m_imgui->warning_text(from_u8(topo)); + else + m_imgui->text(topo); + } + } + + double total_ms = 0.0; + for (const BakeStageSnapshot &st : m_debug_stages) + total_ms += st.ms; + char total[128]; + std::snprintf(total, sizeof(total), "%s: %.1f ms", _u8L("Total").c_str(), total_ms); + m_imgui->text(total); + + if (m_imgui->button(_L("Write stage meshes"))) { + const std::string dir = + (boost::filesystem::temp_directory_path() / "orca-bake-stages").string(); + const size_t written = dump_bake_stages(m_debug_stages, dir); + if (written > 0) + show_info(nullptr, from_u8(_u8L("Wrote the stage meshes as OBJ files to:") + "\n" + dir)); + else + show_error(nullptr, _u8L("Could not write the stage meshes.")); + } + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Write every captured stage out as an OBJ file, so they can be opened " + "side by side in a mesh viewer."), + m_imgui->scaled(20.f)); +} + +void GLGizmoTextureDisplacement::queue_prepare(const TextureDisplacementPrepareParams ¶ms, + const std::string &snapshot_name, bool then_bake, + const std::string &unchanged_msg) +{ + ModelVolume *mv = texture_volume(); + if (mv == nullptr || m_prepare_in_progress || m_bake_in_progress) + return; + + TextureDisplacementPrepareInput input; + input.volume_id = mv->id(); + input.base_mesh = mv->mesh().its; + input.masks = facets_data_of(*mv); + input.layers = mv->texture_displacement_layers; + input.params = params; + input.snapshot_name = snapshot_name; + if (params.subdiv_color_edge_mm > 0.f) + input.color = color_settings_for(*mv); + + m_prepare_in_progress = true; + queue_texture_displacement_prepare(std::move(input), [this, then_bake, unchanged_msg]( + TextureDisplacementPrepareOutcome outcome) { + m_prepare_in_progress = false; + // The commit replaced the volume's mesh (new id, new topology) without changing the object's id + // or volume count, which is not something GLGizmoPainterBase::data_changed() can detect - so the + // reload is explicit, exactly as it is after a bake. Done for every outcome: even a run that + // committed nothing may have left the panel showing a stale triangle count. + if (m_state == On && m_c->selection_info() && m_c->selection_info()->model_object()) + update_from_model_object(false); + m_parent.set_as_dirty(); + + switch (outcome) { + case TextureDisplacementPrepareOutcome::Failed: + return; // cancelled, or the volume went away while the job ran - say nothing, do nothing + case TextureDisplacementPrepareOutcome::PaintLost: + show_error(nullptr, _u8L("The painted area could not be carried onto the remeshed model, so " + "nothing was changed. Switch to Pro mode and remesh before painting.")); + return; + case TextureDisplacementPrepareOutcome::Unchanged: + if (!unchanged_msg.empty()) + show_error(nullptr, unchanged_msg); + break; + case TextureDisplacementPrepareOutcome::Committed: + break; + } + // ... and then the displacement itself, in the background exactly as the Pro-mode button does. + // It commits into the snapshot the prepare opened - but only if the prepare opened one: a run + // that found nothing to do took none, and a bake chained onto that has to push its own or it + // would not be undoable at all. + if (then_bake) + bake(/* own_snapshot */ outcome == TextureDisplacementPrepareOutcome::Unchanged); + }); +} + +void GLGizmoTextureDisplacement::render_paint_cursor_hint() +{ + // Only in the plain paint/select modes; seam and adjust modes have their own click semantics where + // an add/remove sign would just be noise. + if (m_seam_edit_mode || m_adjust_texture_mode) + return; + const ImGuiIO &io = ImGui::GetIO(); + // The pointer must be over the 3D view, not over this panel (or any other ImGui window). + if (io.WantCaptureMouse || !ImGui::IsMousePosValid()) + return; + + // Shift erases (see handle_snapshot_action_name()), and so does a plain stroke in Erase mode. + const bool removing = io.KeyShift || m_erase_mode; + const ImU32 color = removing ? IM_COL32(235, 70, 60, 255) : IM_COL32(90, 210, 110, 255); + const char *glyph = removing ? "-" : "+"; + + ImDrawList *dl = ImGui::GetForegroundDrawList(); + const float fs = ImGui::GetFontSize() * 1.5f; + const ImVec2 at(io.MousePos.x + 15.f, io.MousePos.y - fs - 6.f); + // A translucent dark disc behind the glyph so it reads on any material colour. + dl->AddCircleFilled(ImVec2(at.x + fs * 0.28f, at.y + fs * 0.5f), fs * 0.62f, IM_COL32(0, 0, 0, 150)); + dl->AddText(ImGui::GetFont(), fs, at, color, glyph); +} + +void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float bottom_limit) +{ + ModelObject *mo = m_c->selection_info()->model_object(); + if (!mo) + return; + ModelVolume *mv = texture_volume(); + + const float approx_height = m_imgui->scaled(24.f); + y = std::min(y, bottom_limit - approx_height); + + // Docked (the default) the panel is pinned to the right edge of the 3D canvas and cannot be + // moved. Deliberately *not* next to the gizmo toolbar, which is where `x` points and where every + // other gizmo's window goes: this panel is far taller than those (layer stack plus the whole + // per-layer control set), so at the toolbar it sits right on top of the part of the model being + // painted. Pinning it to the canvas edge also parks it against the UV editor, since that pane is + // docked on the right and the canvas therefore ends exactly at the pane's left edge - so the + // panel follows the pane in and out instead of being clipped by it. + // + // Undocked it becomes an ordinary floating window: a title bar to drag it by, and no forced + // position - the position is deliberately not seeded on undock, so the window stays exactly + // where it already was and the user just gains the ability to move it from there. + ImGuiWindowFlags flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse; + if (!m_undocked) { + flags |= ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar; + // Right-aligned (pivot 1), so the width the panel auto-resized to last frame does not need to + // be known here. Width 0 skips GizmoImguiSetNextWIndowPos()'s own left-aligned fit-to-canvas + // clamp, which would push the window back off the edge it is being pinned to. + float right = float(m_parent.get_canvas_size().get_width()) - m_imgui->scaled(0.5f); + GizmoImguiSetNextWIndowPos(right, y, 0.f, 0.f, ImGuiCond_Always, 1.0f, 0.0f); + } + + ImGuiWrapper::push_toolbar_style(m_parent.get_scale()); + GizmoImguiBegin(get_name(), flags); + ensure_panel_icons(); + process_uv_commands(); // clicks from the UV editor pane, run here where the GL context is current + + // Pinned every frame while Standard is active, so what Preview shows is always what Bake will do. + if (!pro_mode() && apply_standard_mode_presets(mv)) + m_preview_params_dirty = true; + + // Layout follows the Option C design: a fixed-width panel, a header pinned at the top, a body that + // scrolls as one, and a footer with Bake pinned at the bottom. Rows are laid out against panel_w and + // against the body child's own fixed width - never against this window's width, which under + // AlwaysAutoResize is derived from its content and would feed back into it. + const bool dark = wxGetApp().dark_mode(); + const bool busy = m_bake_in_progress || m_prepare_in_progress; + const ImGuiStyle &style = ImGui::GetStyle(); + const float panel_w = m_imgui->scaled(21.5f); + const float frame_h = ImGui::GetFrameHeight(); + const float icon_md = std::round(frame_h * 1.25f); // tool and view buttons + const float icon_sm = std::round(frame_h * 1.05f); // buttons sitting in a row of text + const float gap_s = std::round(m_imgui->scaled(0.4f)); + const float label_w = m_imgui->scaled(4.8f); // label column of a label + slider row + const float card_pad = std::round(m_imgui->scaled(0.55f)); + const float wrap_w = m_imgui->scaled(20.f); + const ImVec4 orca = ImGuiWrapper::COL_ORCA; + const ImVec4 col_link = dark ? ImVec4(0.30f, 0.71f, 0.67f, 1.f) : ImVec4(0.f, 0.47f, 0.42f, 1.f); + const ImVec4 col_frame = dark ? ImVec4(0.212f, 0.212f, 0.235f, 1.f) : ImVec4(0.808f, 0.808f, 0.808f, 1.f); + const ImU32 col_card = dark ? IM_COL32(255, 255, 255, 10) : IM_COL32(0, 0, 0, 12); + const ImU32 col_line = dark ? IM_COL32(255, 255, 255, 18) : IM_COL32(0, 0, 0, 23); + const ImU32 col_sep = ImGui::GetColorU32(ImGuiCol_Separator); + + // Combo drop-downs otherwise inherit ImGui's near-black default popup background; under the light + // theme that leaves the dark item text unreadable ("the dropbox is black"). Pushed only around each + // Combo below (never around a tooltip, whose own near-black default is what makes it readable). + const ImVec4 combo_popup_bg = dark ? ImVec4(0.18f, 0.18f, 0.19f, 1.f) : ImVec4(0.93f, 0.93f, 0.93f, 1.f); + const auto scoped_combo = [&](const char *id, int *v, const char *const items[], int n) { + ImGui::PushStyleColor(ImGuiCol_PopupBg, combo_popup_bg); + const bool changed = ImGui::Combo(id, v, items, n); + ImGui::PopStyleColor(); + return changed; + }; + const auto hover_tip = [&](const auto &text) { + if (ImGui::IsItemHovered()) + m_imgui->tooltip(text, wrap_w); + }; + + // Framed icon toggle: a 1 px frame, and when on a teal frame over a teal tint with the icon in its + // original colours (IconManager's color_wite_gray variant [1]); off, the theme's grey variant [0]. + // `unavailable`, when not empty, draws it faded but still hoverable, so the tooltip can say why it + // cannot be used - a disabled ImGui item would swallow the hover and leave the user guessing. + const auto icon_toggle = [&](int uid, const std::string &iconfile, bool active, float sz, const wxString &label, + const wxString &tip_text, const wxString &unavailable = wxString()) -> bool { + const bool na = !unavailable.empty(); + const auto it = m_panel_icon_map.find(iconfile); + const float pad = std::max(1.f, std::round(sz * 0.14f)); + const int variant = active ? 1 : 0; + bool clicked = false; + ImGui::PushID(uid); + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, na ? style.Alpha * 0.32f : style.Alpha); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.f); + ImGui::PushStyleColor(ImGuiCol_Button, active ? ImVec4(orca.x, orca.y, orca.z, 0.22f) : ImVec4(0.f, 0.f, 0.f, 0.f)); + if (na) { + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.f, 0.f, 0.f, 0.f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.f, 0.f, 0.f, 0.f)); + } + if (it != m_panel_icon_map.end() && int(it->second.size()) > variant && it->second[size_t(variant)]->is_valid()) { + const IconManager::Icon &ic = *it->second[size_t(variant)]; + clicked = m_imgui->image_button((ImTextureID) (intptr_t) ic.tex_id, ImVec2(sz - 2.f * pad, sz - 2.f * pad), ic.tl, ic.br, + int(pad)); + } else { + clicked = ImGui::Button(label.ToUTF8().data(), ImVec2(0.f, sz)); // the control is never lost to a missing icon + } + ImGui::GetWindowDrawList()->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), + ImGui::GetColorU32(active ? orca : col_frame), style.FrameRounding); + ImGui::PopStyleColor(na ? 3 : 1); + ImGui::PopStyleVar(2); + ImGui::PopID(); + if (ImGui::IsItemHovered()) + m_imgui->tooltip(na ? tip_text + "\n\n" + unavailable : tip_text, wrap_w); + return clicked && !na; + }; + // Borderless icon button (non-toggle): always the grey monochrome variant. + const auto icon_button = [&](int uid, const std::string &iconfile, float sz, const wxString &label, + const wxString &tip_text) -> bool { + const auto it = m_panel_icon_map.find(iconfile); + const float pad = std::max(1.f, std::round(sz * 0.14f)); + bool clicked = false; + ImGui::PushID(uid); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.f); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.f, 0.f, 0.f, 0.f)); + if (it != m_panel_icon_map.end() && !it->second.empty() && it->second[0]->is_valid()) { + const IconManager::Icon &ic = *it->second[0]; + clicked = m_imgui->image_button((ImTextureID) (intptr_t) ic.tex_id, ImVec2(sz - 2.f * pad, sz - 2.f * pad), ic.tl, ic.br, + int(pad)); + } else { + clicked = ImGui::Button(label.ToUTF8().data(), ImVec2(0.f, sz)); + } + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + ImGui::PopID(); + if (!tip_text.empty() && ImGui::IsItemHovered()) + m_imgui->tooltip(tip_text, wrap_w); + return clicked; + }; + // A short vertical rule between groups of buttons on one row; call it right after an item. + const auto vsep = [&](float h) { + ImGui::SameLine(0.f, gap_s); + const ImVec2 p = ImGui::GetCursorScreenPos(); + ImGui::GetWindowDrawList()->AddLine(ImVec2(p.x, p.y + h * 0.2f), ImVec2(p.x, p.y + h * 0.8f), col_sep); + ImGui::Dummy(ImVec2(1.f, h)); + ImGui::SameLine(0.f, gap_s); + }; + const auto heading = [&](const wxString &text) { + m_imgui->push_bold_font(); + m_imgui->text(text); + m_imgui->pop_bold_font(); + }; + // Cuts a UTF-8 string down to `max_w` pixels, ending it in "..." when it had to be cut. + const auto ellipsize = [](std::string s, float max_w) { + if (ImGui::CalcTextSize(s.c_str()).x <= max_w) + return s; + while (!s.empty() && ImGui::CalcTextSize((s + "...").c_str()).x > max_w) { + unsigned char c; + do { // a whole code point at a time + c = static_cast(s.back()); + s.pop_back(); + } while (!s.empty() && (c & 0xC0) == 0x80); + } + return s + "..."; + }; + // Label column + a slider filling the rest of the row, value printed inside the track. Ctrl+click on + // any of these types a value in. `right_inset` keeps a layer card's inner padding. + const auto slider_label = [&](const wxString &label) { + const float x0 = ImGui::GetCursorPosX(); + ImGui::AlignTextToFramePadding(); + m_imgui->text(label); + ImGui::SameLine(); + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w)); + }; + const auto float_row = [&](const char *id, const wxString &label, float *v, float v_min, float v_max, const char *format, + bool log, float right_inset) -> bool { + slider_label(label); + ImGui::SetNextItemWidth(-std::max(right_inset, 1.f)); + return ImGui::SliderFloat(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0)); + }; + const auto int_row = [&](const char *id, const wxString &label, int *v, int v_min, int v_max, const char *format, + float right_inset) -> bool { + slider_label(label); + ImGui::SetNextItemWidth(-std::max(right_inset, 1.f)); + return ImGui::SliderInt(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp); + }; + const auto layer_name = [](const TextureDisplacementLayer &l) { + return l.name.empty() ? Slic3r::format(_u8L("Layer %1%"), l.slot + 1) : l.name; + }; + // Painted on any model part. The selectors flush into the model at the end of every stroke. + const auto slot_painted = [mo](int slot) { + for (const ModelVolume *v : mo->volumes) + if (v->is_model_part() && !v->texture_displacement_facet(slot).empty()) + return true; + return false; + }; + + // ---- Header: title, Standard / Pro, dock toggle. Pinned above the body. ---- + { + const float x0 = ImGui::GetCursorPosX(); + ImGui::AlignTextToFramePadding(); + heading(_L("Texture displacement")); + + const std::string labels[2] = { _u8L("Standard"), _u8L("Pro") }; + const float seg_pad = m_imgui->scaled(0.5f); + const float seg_w[2] = { ImGui::CalcTextSize(labels[0].c_str()).x + 2.f * seg_pad, ImGui::CalcTextSize(labels[1].c_str()).x + 2.f * seg_pad }; + ImGui::SameLine(); + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + panel_w - (seg_w[0] + seg_w[1] + gap_s + icon_sm))); + + // Standard / Pro is a mode, not an option: Standard hides every mesh-preparation control and folds + // the whole recipe into Bake, Pro shows all of it and hands the ordering to the user. + const ImVec2 seg_min = ImGui::GetCursorScreenPos(); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.f, style.ItemSpacing.y)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.f); + m_imgui->disabled_begin(busy); + for (int m = 0; m < 2; ++m) { + if (m > 0) + ImGui::SameLine(); + const bool on = m_panel_mode == m; + ImGui::PushStyleColor(ImGuiCol_Button, on ? orca : ImVec4(0.f, 0.f, 0.f, 0.f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, on ? orca : ImVec4(orca.x, orca.y, orca.z, 0.25f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, orca); + ImGui::PushStyleColor(ImGuiCol_Text, on ? ImVec4(1.f, 1.f, 1.f, 1.f) : style.Colors[ImGuiCol_TextDisabled]); + const bool clicked = ImGui::Button((labels[m] + "##panel_mode").c_str(), ImVec2(seg_w[m], frame_h)); + ImGui::PopStyleColor(4); + hover_tip(m == 0 ? _u8L("Standard - pick a texture, paint where it goes, press Bake. Everything the " + "mesh needs is done for you in that one step.") : + _u8L("Pro - the mesh preparation is yours to run: Remesh, Subdivide and Bake as " + "separate steps, with every setting on show. For when Standard's result is " + "not what you wanted and you know why.")); + if (clicked && !on) { + m_panel_mode = m; + if (!pro_mode()) { + // Leaving the subdivision preview open would strand a wireframe whose controls just + // disappeared, so close it as part of the switch. + m_subdivide_editing = false; + m_subdivide_preview_tris = -1; + m_subdivide_preview_glmodel.reset(); + if (apply_standard_mode_presets(mv)) + m_preview_params_dirty = true; + } + m_parent.set_as_dirty(); + } + } + m_imgui->disabled_end(); + ImGui::PopStyleVar(2); + ImGui::GetWindowDrawList()->AddRect(seg_min, ImVec2(ImGui::GetItemRectMax().x, seg_min.y + frame_h), + ImGui::GetColorU32(col_frame), style.FrameRounding); + + ImGui::SameLine(0.f, gap_s); + if (icon_button(806, "canvas_drag.svg", icon_sm, m_undocked ? _L("Dock panel") : _L("Undock panel"), + _L("Detach this panel so it can be dragged anywhere over the 3D view, or dock it back beside " + "the toolbar."))) + m_undocked = !m_undocked; + } + ImGui::Separator(); + + // ---- Body: everything between the header and the footer, scrolling as one. ---- + // Sized from last frame's content and footer heights (see m_panel_body_h): as tall as its content, + // but never so tall that the footer drops below the bottom of the canvas. + const float bottom = m_undocked ? ImGui::GetIO().DisplaySize.y : bottom_limit; + const float max_body_h = std::max(m_imgui->scaled(8.f), bottom - ImGui::GetCursorScreenPos().y - m_panel_footer_h - + style.WindowPadding.y - style.ItemSpacing.y); + const float body_h = m_panel_body_h > 0.f ? std::min(m_panel_body_h + 1.f, max_body_h) : max_body_h; + + // ImGui's stock scrollbar is a wide, square-cornered slab in a tinted track - against this flat panel + // it reads as a raw widget bolted onto the edge. Slim it to a rounded thumb over an invisible track. + const float scrollbar_w = m_imgui->scaled(0.5f); + const ImVec4 grab = dark ? ImVec4(1.f, 1.f, 1.f, 0.26f) : ImVec4(0.f, 0.f, 0.f, 0.26f); + ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, scrollbar_w); + ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 0.5f * scrollbar_w); + ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f)); + ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, grab); + ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(grab.x, grab.y, grab.z, 0.45f)); + ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(grab.x, grab.y, grab.z, 0.65f)); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.f, 0.f, 0.f, 0.f)); + // NoScrollWithMouse: the wheel is handled below so the scroll can be eased instead of teleporting + // five text lines per notch, which on blocks this tall lost the reader's place. + ImGui::BeginChild("##td_body", ImVec2(panel_w, body_h), false, ImGuiWindowFlags_NoScrollWithMouse); + { + ImGuiIO &io = ImGui::GetIO(); + const float scroll_now = ImGui::GetScrollY(); + const float scroll_max = ImGui::GetScrollMaxY(); + + // Anything that moved the scroll without us - dragging the grab, a keyboard/gamepad nav step, the + // content shrinking under a clamped offset - has to re-seed the target, or the easing below would + // immediately drag the view back to where it last animated to. + if (m_panel_scroll_applied < 0.f || std::abs(scroll_now - m_panel_scroll_applied) > 0.5f) + m_panel_scroll_target = scroll_now; + + if (io.MouseWheel != 0.f && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows)) + m_panel_scroll_target -= io.MouseWheel * ImGui::GetFontSize() * 4.f; + // Whole pixels: ImGui floors whatever SetScrollY() is given, so a fractional target could never be + // reached and the "still gliding" test below would stay true forever, repainting the canvas for good. + m_panel_scroll_target = std::floor(std::clamp(m_panel_scroll_target, 0.f, scroll_max)); + + const float delta = m_panel_scroll_target - scroll_now; + if (std::abs(delta) >= 1.f) { + // Exponential ease, formulated against the frame time so the glide takes the same wall time + // whether the canvas is running at 30 or 144 fps. The last sub-pixel step would be floored + // away, so land on the target outright once the remainder is that small. + const float t = 1.f - std::exp(-20.f * std::clamp(io.DeltaTime, 1.f / 240.f, 1.f / 15.f)); + const float step = (std::abs(delta * t) < 1.f) ? delta : delta * t; + const float next = std::floor(scroll_now + step); + ImGui::SetScrollY(next); + m_panel_scroll_applied = next; + m_parent.set_as_dirty(); // nothing else would redraw mid-glide once the mouse stops + } else { + m_panel_scroll_applied = scroll_now; + } + } + + // Everything that changes the layer list is deferred until the list is no longer being drawn: + // removing or reordering shifts mv->texture_displacement_layers under the pointers the loop holds, + // and switching the active layer mid-loop would draw two cards open for a frame. + int slot_to_remove = -1; + int move_slot = -1; + int move_to = -1; + int activate_slot = -1; + + TextureDisplacementLayer *active = mv != nullptr ? active_layer() : nullptr; + + // ---- Paint: which layer strokes land in, and Paint / Erase ---- + { + ImGui::AlignTextToFramePadding(); + heading(_L("Paint")); + const std::string into = active != nullptr ? Slic3r::format(_u8L("into %1%"), layer_name(*active)) : + _u8L("add a layer to paint"); + const float toggles_x = ImGui::GetWindowContentRegionMax().x - (2.f * icon_sm + gap_s); + const float text_end = toggles_x - (2.f * gap_s + 1.f); + ImGui::SameLine(); + const std::string shown = ellipsize(into, std::max(0.f, text_end - ImGui::GetCursorPosX())); + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), text_end - ImGui::CalcTextSize(shown.c_str()).x)); + ImGui::TextDisabled("%s", shown.c_str()); + vsep(icon_sm); + ImGui::SetCursorPosX(toggles_x); + if (icon_toggle(820, "texture_displacement_add.svg", !m_erase_mode, icon_sm, _L("Paint"), + _L("Paint - add the active layer where you click or drag. The right button erases, and so does " + "holding Shift."))) + m_erase_mode = false; + ImGui::SameLine(0.f, gap_s); + if (icon_toggle(821, "texture_displacement_add_negative.svg", m_erase_mode, icon_sm, _L("Erase"), + _L("Erase - remove the active layer where you click or drag. The right button paints."))) + m_erase_mode = true; + } + + // ---- Tools: brush / face / connected area, then the whole-model actions, then the active tool's own + // control filling the rest of the row ---- + // "Face" and "Connected area" reuse the exact same selection machinery every other paint gizmo has + // (single-facet click, and angle-limited flood fill respectively). + { + const bool is_brush_mode = m_tool_type == ToolType::BRUSH && m_cursor_type != TriangleSelector::CursorType::POINTER; + const bool is_face_mode = m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER; + const bool is_area_mode = m_tool_type == ToolType::SMART_FILL; + const float row_y = ImGui::GetCursorPosY(); + if (icon_toggle(801, "texture_displacement_brush.svg", is_brush_mode, icon_md, _L("Brush"), + _L("Brush - paint over the surface by dragging"))) { + m_tool_type = ToolType::BRUSH; + if (m_cursor_type == TriangleSelector::CursorType::POINTER) + m_cursor_type = TriangleSelector::CursorType::CIRCLE; + } + ImGui::SameLine(0.f, gap_s); + if (icon_toggle(802, "texture_displacement_face.svg", is_face_mode, icon_md, _L("Face"), _L("Face - click individual triangles"))) { + m_tool_type = ToolType::BRUSH; + m_cursor_type = TriangleSelector::CursorType::POINTER; + } + ImGui::SameLine(0.f, gap_s); + if (icon_toggle(803, "texture_displacement_connected_area.svg", is_area_mode, icon_md, _L("Connected area"), + _L("Connected area - flood-fill the region reachable without crossing an edge sharper than the " + "angle threshold"))) { + m_tool_type = ToolType::SMART_FILL; + m_cursor_type = TriangleSelector::CursorType::POINTER; + } + + // Whole model: paint every face with the active layer, or clear its paint from all of them. + const wxString whole_na = busy ? _L("Wait for the bake to finish.") : + active == nullptr ? _L("Add a layer first.") : + wxString(); + const wxString erase_na = !whole_na.empty() ? whole_na : + !slot_painted(m_active_layer_slot) ? _L("The active layer has no paint yet.") : + wxString(); + ImGui::SameLine(0.f, gap_s); + if (icon_toggle(806, "texture_displacement_select_all.svg", false, icon_md, _L("Select whole model"), + _L("Select whole model - paint every face of the model with the active layer"), whole_na)) + select_whole_model(); + ImGui::SameLine(0.f, gap_s); + if (icon_toggle(807, "texture_displacement_erase_all.svg", false, icon_md, _L("Erase whole model"), + _L("Erase whole model - clear the active layer's paint from every face"), erase_na)) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Reset texture displacement selection"), + UndoRedo::SnapshotType::GizmoAction); + int idx = -1; + for (ModelVolume *v : mo->volumes) + if (v->is_model_part()) { + ++idx; + m_triangle_selectors[idx]->reset(); + m_triangle_selectors[idx]->request_update_render_data(); + } + update_model_object(); + m_parent.set_as_dirty(); + } + + // The active tool's control fills the rest of the row, each part centred on the (taller) tool icons. + const float row_end = ImGui::GetWindowContentRegionMax().x; + const auto centre_on_row = [&](float h) { ImGui::SetCursorPosY(row_y + std::round((icon_md - h) * 0.5f)); }; + vsep(icon_md); + if (is_brush_mode) { + ImGui::SetNextItemWidth(std::max(1.f, row_end - ImGui::GetCursorPosX() - (3.f * gap_s + 1.f + 2.f * icon_sm))); + centre_on_row(frame_h); + ImGui::SliderFloat("##cursor_radius", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f mm", + ImGuiSliderFlags_AlwaysClamp); + hover_tip(m_desc.at("cursor_size")); + vsep(icon_sm); + const bool is_circle = m_cursor_type == TriangleSelector::CursorType::CIRCLE; + centre_on_row(icon_sm); + if (icon_toggle(804, "circle_paint.svg", is_circle, icon_sm, m_desc.at("circle"), + _L("Circle - paints everything under the brush as seen from the camera"))) + m_cursor_type = TriangleSelector::CursorType::CIRCLE; + ImGui::SameLine(0.f, gap_s); + centre_on_row(icon_sm); + if (icon_toggle(805, "menu_obj_sphere.svg", !is_circle, icon_sm, m_desc.at("sphere"), + _L("Sphere - paints only within a ball around the point under the cursor"))) + m_cursor_type = TriangleSelector::CursorType::SPHERE; + } else if (is_area_mode) { + ImGui::SetNextItemWidth(std::max(1.f, row_end - ImGui::GetCursorPosX())); + centre_on_row(frame_h); + ImGui::SliderFloat("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°", + ImGuiSliderFlags_AlwaysClamp); + hover_tip(_u8L("Angle threshold - the fill stops at edges sharper than this")); + } else { + centre_on_row(frame_h); + ImGui::AlignTextToFramePadding(); + ImGui::TextDisabled("%s", ellipsize(_u8L("Click a triangle to paint it"), + std::max(0.f, row_end - ImGui::GetCursorPosX())).c_str()); + } + } + + // ---- View: Normal / Fast / Checker / Distortion as one group, Wireframe on its own ---- + // The underlying state stays m_use_shaded_preview + m_uv_check_mode. + { + const int cur_mode = m_use_shaded_preview ? 1 : + m_uv_check_mode == UVCheckMode::Checker ? 2 : + m_uv_check_mode == UVCheckMode::Distortion ? 3 : 0; + int new_mode = cur_mode; + bool wf_toggle = false; + const wxString distortion_na = active == nullptr ? _L("Add a layer first.") : + active->projection_method != TextureProjectionMethod::LSCM ? + _L("Needs the active layer mapped with Unwrap (LSCM).") : + wxString(); + // Distortion over a layer that stopped being an unwrap shows nothing at all, so fall back to Normal. + if (cur_mode == 3 && !distortion_na.empty()) + new_mode = 0; + + const float x0 = ImGui::GetCursorPosX(); + ImGui::AlignTextToFramePadding(); + ImGui::TextDisabled("%s", _u8L("View").c_str()); + ImGui::SameLine(); + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + m_imgui->scaled(2.6f))); + if (icon_toggle(701, "texture_displacement_real_preview.svg", cur_mode == 0, icon_md, _L("Normal"), + _L("Normal - the real geometry, exactly what Bake will produce. Slower to update"))) + new_mode = 0; + ImGui::SameLine(0.f, gap_s); + if (icon_toggle(702, "texture_displacement_fast_preview.svg", cur_mode == 1, icon_md, _L("Fast"), + _L("Fast - the relief is only shaded on, not built, and only for the layer you are " + "editing. Updates instantly while you paint"))) + new_mode = 1; + ImGui::SameLine(0.f, gap_s); + if (icon_toggle(703, "texture_displacement_checker.svg", cur_mode == 2, icon_md, _L("Checker"), + _L("Checker - a test grid instead of the texture. Where the squares stay square the " + "texture is undistorted; where they stretch, it will too"))) + new_mode = 2; + ImGui::SameLine(0.f, gap_s); + if (icon_toggle(704, "texture_displacement_distortion.svg", cur_mode == 3, icon_md, _L("Distortion"), + _L("Distortion - blue-to-red stretch heatmap over the unwrap"), distortion_na)) + new_mode = 3; + vsep(icon_md); + if (icon_toggle(705, "texture_displacement_wireframe.svg", m_wireframe_overlay, icon_md, _L("Wireframe"), + _L("Wireframe - overlay the mesh edges; independent of the view above"))) + wf_toggle = true; + + const std::string auto_label = _u8L("Auto"); + const float auto_w = frame_h + style.ItemInnerSpacing.x + ImGui::CalcTextSize(auto_label.c_str()).x; + ImGui::SameLine(); + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), ImGui::GetWindowContentRegionMax().x - auto_w)); + if (ImGui::Checkbox((auto_label + "##auto_update").c_str(), &m_auto_update) && m_auto_update) + rebuild_preview(); // catch up anything that changed while it was off + hover_tip(_u8L("Rebuilds the preview as soon as anything changes. Turn it off on a heavy model if painting " + "or dragging a slider starts to stutter - the preview then waits until you let go.")); + + if (new_mode != cur_mode) + apply_view_mode(new_mode); + if (wf_toggle) { + m_wireframe_overlay = !m_wireframe_overlay; + refresh_wireframe(); + m_parent.set_as_dirty(); + } + } + + ImGui::Separator(); + // ---- Texture layers: the active layer as an open card, every other one as a single row ---- + { + ImGui::AlignTextToFramePadding(); + heading(_L("Texture layers")); + if (mv != nullptr) { + const std::string count = std::to_string(mv->texture_displacement_layers.size()) + " / " + + std::to_string(TEXTURE_DISPLACEMENT_MAX_LAYERS); + ImGui::SameLine(); + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), ImGui::GetWindowContentRegionMax().x - ImGui::CalcTextSize(count.c_str()).x)); + ImGui::TextDisabled("%s", count.c_str()); + } + } + + std::vector ordered; + if (mv != nullptr) { + for (TextureDisplacementLayer &l : mv->texture_displacement_layers) + ordered.push_back(&l); + std::sort(ordered.begin(), ordered.end(), [](const auto *a, const auto *b) { return a->slot < b->slot; }); + } + + ImDrawList *dl = ImGui::GetWindowDrawList(); + const float content_rx = ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x; + const float rounding = style.FrameRounding + 1.f; + + // Opens the texture library for a layer, making it the active one too (after the loop). + const auto open_picker = [&](int slot) { + m_picker_slot = slot; + m_picker_open_request = true; + }; + // The six-dot drag icon, drawn into `list` over [x, x + w] x [y, y + h]. + const auto draw_drag_icon = [&](ImDrawList *list, float x, float y, float w, float h, ImU32 tint) { + const auto it = m_panel_icon_map.find("texture_displacement_drag.svg"); + if (it != m_panel_icon_map.end() && !it->second.empty() && it->second[0]->is_valid()) { + // The icon's dots sit in the middle of its square, so it is drawn at the row's height centred on the + // narrow hit area; only its transparent margins overhang the neighbouring widgets. + const IconManager::Icon &ic = *it->second[0]; + const float cx = x + 0.5f * w; + list->AddImage((ImTextureID) (intptr_t) ic.tex_id, ImVec2(cx - 0.5f * h, y), ImVec2(cx + 0.5f * h, y + h), ic.tl, ic.br, tint); + } else { + const float r = std::max(1.f, std::round(m_imgui->scaled(0.08f))); + for (int row = 0; row < 3; ++row) + for (int col = 0; col < 2; ++col) + list->AddCircleFilled(ImVec2(x + w * (0.3f + 0.4f * col), y + h * 0.5f + float(row - 1) * 4.f * r), r, tint); + } + }; + const auto drag_tint = [dark](int alpha) { return dark ? IM_COL32(255, 255, 255, alpha) : IM_COL32(90, 90, 90, alpha); }; + + const float grip_w = std::round(m_imgui->scaled(0.7f)); + // Six-dot handle that drags a layer onto another one to reorder the stack. + const auto grip = [&](const TextureDisplacementLayer &l, float h) { + const ImVec2 p = ImGui::GetCursorScreenPos(); + ImGui::InvisibleButton("##grip", ImVec2(grip_w, h)); + const bool hot = ImGui::IsItemHovered() || ImGui::IsItemActive(); + draw_drag_icon(dl, p.x, p.y, grip_w, h, drag_tint(hot ? 255 : 150)); + hover_tip(_u8L("Drag onto another layer to reorder. The first layer sits on the bare surface; each one after " + "it adds its relief on top of the ones before.")); + // ImGui's own preview is a tooltip holding whatever is submitted here. The panel draws a copy of the whole + // layer row under the cursor instead (after the layer list), so that one is switched off. + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceNoPreviewTooltip)) { + const int slot = l.slot; + ImGui::SetDragDropPayload("TD_LAYER", &slot, sizeof(slot)); + ImGui::EndDragDropSource(); + } + }; + + // The layer being dragged, while a reorder drag is in flight. Its card stays in place as a faded ghost. + int dragged_slot = -1; + if (const ImGuiPayload *payload = ImGui::GetDragDropPayload(); payload != nullptr && payload->IsDataType("TD_LAYER")) + dragged_slot = *static_cast(payload->Data); + int dragged_index = -1; + for (size_t i = 0; i < ordered.size(); ++i) + if (ordered[i]->slot == dragged_slot) + dragged_index = int(i); + const ImU32 ghost_col = ImGui::GetColorU32(ImGuiCol_WindowBg, 0.65f); + + // A layer card or row [mn, mx] as a drop target. Dropped anywhere on another layer, the dragged one takes its + // place and the layers in between shift over by one - so it lands above the target when moving up and below it + // when moving down. A teal line on that side shows where; ImGui's default highlight rectangle says only + // "something is here", so it is replaced by the line. + // + // Deciding by which half of the target the cursor is over instead made dropping onto a neighbour's near half a + // silent no-op ("after the layer I am already after"), and that is exactly where a drag to the top ends. + const auto drop_target = [&](size_t index, int slot, const ImVec2 &mn, const ImVec2 &mx) { + if (!ImGui::BeginDragDropTarget()) + return; + if (const ImGuiPayload *payload = ImGui::AcceptDragDropPayload("TD_LAYER", ImGuiDragDropFlags_AcceptBeforeDelivery | + ImGuiDragDropFlags_AcceptNoDrawDefaultRect)) { + const int from = *static_cast(payload->Data); + const bool after = dragged_index >= 0 && dragged_index < int(index); + if (from != slot) { + const float y = after ? mx.y + 2.f : mn.y - 2.f; + dl->AddLine(ImVec2(mn.x, y), ImVec2(mx.x, y), ImGui::GetColorU32(orca), 2.f); + } + if (payload->IsDelivery()) { + move_slot = from; + move_to = int(index) + (after ? 1 : 0); + } + } + ImGui::EndDragDropTarget(); + }; + + for (size_t li = 0; li < ordered.size(); ++li) { + TextureDisplacementLayer &layer = *ordered[li]; + const bool is_active = layer.slot == m_active_layer_slot; + const bool painted = slot_painted(layer.slot); + ImGui::PushID(layer.slot); + + if (is_active) { + // The card's height is not known until it is laid out, so its content goes into a foreground + // channel and the backing rectangle into a background one, merged at the end - the standard + // ImGui "rect behind a group" trick. + const ImVec2 card_min = ImGui::GetCursorScreenPos(); + dl->ChannelsSplit(2); + dl->ChannelsSetCurrent(1); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + card_pad); + ImGui::Indent(card_pad); + ImGui::BeginGroup(); + + // Header: grip, thumbnail and name (both open the library), paint state, order, remove. + { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(gap_s, style.ItemSpacing.y)); + grip(layer, frame_h); + ImGui::SameLine(); + if (GLTexture *thumb = get_layer_thumbnail(layer)) { + if (ImGui::ImageButton((ImTextureID) (intptr_t) thumb->get_id(), ImVec2(frame_h, frame_h), ImVec2(0.f, 0.f), + ImVec2(1.f, 1.f), 0)) + open_picker(layer.slot); + } else if (ImGui::Button("?##thumb", ImVec2(frame_h, frame_h))) { + open_picker(layer.slot); + } + hover_tip(_u8L("Pick a different image for this layer. The paint, depth and tiling stay as they are.")); + { + // The teal outline on the thumbnail is what marks this card as the layer being painted. + const ImVec2 a = ImGui::GetItemRectMin(), b = ImGui::GetItemRectMax(); + dl->AddRect(ImVec2(a.x - 2.f, a.y - 2.f), ImVec2(b.x + 2.f, b.y + 2.f), ImGui::GetColorU32(orca), style.FrameRounding); + } + ImGui::SameLine(); + + const std::string not_painted = _u8L("not painted"); + const float right_w = (painted ? 0.f : ImGui::CalcTextSize(not_painted.c_str()).x + gap_s) + 3.f * frame_h + 2.f * gap_s; + const float name_w = std::max(frame_h, ImGui::GetWindowContentRegionMax().x - card_pad - ImGui::GetCursorPosX() - gap_s - right_w); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(orca.x, orca.y, orca.z, 0.2f)); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(orca.x, orca.y, orca.z, 0.35f)); + ImGui::AlignTextToFramePadding(); + const std::string shown = ellipsize(layer_name(layer), name_w - 2.f * style.FramePadding.x); + if (ImGui::Selectable((shown + "##name").c_str(), false, 0, ImVec2(name_w, frame_h))) + open_picker(layer.slot); + ImGui::PopStyleColor(2); + hover_tip(_u8L("Pick a different image for this layer. The paint, depth and tiling stay as they are.")); + if (!painted) { + ImGui::SameLine(); + ImGui::TextDisabled("%s", not_painted.c_str()); + } + ImGui::SameLine(); + m_imgui->disabled_begin(busy || li == 0); + if (icon_button(611, "texture_displacement_move_up.svg", frame_h, _L("Move up"), _L("Move up - applied earlier"))) { + move_slot = layer.slot; + move_to = int(li) - 1; + } + m_imgui->disabled_end(); + ImGui::SameLine(); + m_imgui->disabled_begin(busy || li + 1 >= ordered.size()); + if (icon_button(612, "texture_displacement_move_down.svg", frame_h, _L("Move down"), _L("Move down - applied later"))) { + move_slot = layer.slot; + move_to = int(li) + 2; + } + m_imgui->disabled_end(); + ImGui::SameLine(); + m_imgui->disabled_begin(busy); + if (icon_button(600 + layer.slot, "texture_displacement_cross.svg", frame_h, m_desc.at("remove_layer"), + _L("Remove this layer"))) + slot_to_remove = layer.slot; + m_imgui->disabled_end(); + ImGui::PopStyleVar(); + } + + // The three controls nearly every layer needs; everything else waits behind "More settings". + // Depth and tile size are logarithmic, so both ends of their wide ranges stay usable. + m_preview_params_dirty |= float_row("##depth", _L("Depth"), &layer.depth_mm, 0.01f, 10.f, "%.3f mm", true, card_pad); + hover_tip(_u8L("Height of the relief: how far white in the texture lifts the surface, in " + "millimetres. Black does not move it at all, unless Midlevel says otherwise.")); + m_preview_params_dirty |= float_row("##tile_size", _L("Tile size"), &layer.tiling_scale, 0.2f, 200.f, "%.2f mm", true, card_pad); + hover_tip(_u8L("How wide one copy of the texture is on the model. Smaller repeats the pattern " + "more often and makes its detail finer; with Tile off, this is the size of the " + "single copy.")); + m_preview_params_dirty |= float_row("##rotation", _L("Rotation"), &layer.rotation_deg, 0.f, 360.f, "%.0f°", false, card_pad); + hover_tip(_u8L("Turns the texture on the surface, in degrees - for lining a pattern up with an " + "edge of the model.")); + + if (m_layer_expanded[size_t(layer.slot)]) { + { + // A dashed rule between the basic and the advanced settings. + const ImVec2 p = ImGui::GetCursorScreenPos(); + const float x1 = content_rx - card_pad; + const float dash = std::round(m_imgui->scaled(0.25f)); + for (float xx = p.x; xx < x1; xx += 2.f * dash) + dl->AddLine(ImVec2(xx, p.y), ImVec2(std::min(xx + dash, x1), p.y), col_line); + ImGui::Dummy(ImVec2(1.f, 1.f)); + } + + // Midlevel: the height that means "don't move". At 0 the surface only ever bulges outwards; + // at 0.5 mid-grey is neutral and darker texels cut inwards. + m_preview_params_dirty |= float_row("##midlevel", _L("Midlevel"), &layer.midlevel, 0.f, 10.f, "%.2f", false, card_pad); + hover_tip(_u8L("Which grey stays where the surface already is. At 0 the texture only pushes " + "outwards; at 0.5 mid-grey stays put, so darker greys cut in and lighter ones " + "still push out - one image both embosses and engraves.\n\n" + "What cuts in has to fit: inside a sharp corner or through a thin wall, a deep " + "cut can pass through the other side.")); + if (layer.midlevel > 0.f && layer.depth_mm > 1.f) { + ImGui::PushTextWrapPos(content_rx - card_pad); + m_imgui->warning_text(_L("Deep inward displacement may self-intersect.")); + ImGui::PopTextWrapPos(); + } + + m_preview_params_dirty |= float_row("##smoothing", _L("Smoothing"), &layer.smoothing, 0.f, 1.f, "%.2f", false, card_pad); + hover_tip(_u8L("Blurs the image before it is used, which rounds off hard steps and removes " + "speckle from a noisy photo. Raise it if the relief looks harsh or grainy; it " + "costs fine detail.")); + + // Edge fade: the relief flattens toward the boundary of the painted area. + m_preview_params_dirty |= ImGui::Checkbox((_u8L("Edge fade") + "##edge_smoothing").c_str(), &layer.edge_smoothing); + hover_tip(_u8L("Flattens the relief as it approaches the edge of the painted area, so it " + "blends into the bare surface instead of stopping at a step.")); + ImGui::SameLine(); + m_imgui->disabled_begin(!layer.edge_smoothing); + ImGui::SetNextItemWidth(-card_pad); + m_preview_params_dirty |= ImGui::SliderFloat("##edge_amount", &layer.edge_smoothing_amount, 0.02f, 1.f, "%.2f", + ImGuiSliderFlags_AlwaysClamp); + m_imgui->disabled_end(); + hover_tip(_u8L("How far in the fade reaches, as a share of the painted area. Small values " + "soften a narrow band at the edge; 1 flattens almost all of it.")); + + // Invert and Colours share a row. + { + const float x0 = ImGui::GetCursorPosX(); + m_preview_params_dirty |= ImGui::Checkbox(_u8L("Invert").c_str(), &layer.invert); + hover_tip(_u8L("Turns the relief inside out: what stood out is cut in, and the other way " + "round. The same as using a negative of the image.")); + ImGui::SameLine(); + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + 0.5f * (ImGui::GetWindowContentRegionMax().x - card_pad - x0))); + + // Only offered for a texture that actually has colour - the shipped library is grayscale, + // and a checkbox that silently does nothing on nine textures out of ten is worse than no + // checkbox. Disabled rather than hidden so it is clear the feature exists and what it wants. + const bool has_color = decode_height_texture(layer).has_color(); + bool color_enabled = layer.color_enabled && has_color; + m_imgui->disabled_begin(!has_color); + if (ImGui::Checkbox(_u8L("Colours").c_str(), &color_enabled)) { + layer.color_enabled = color_enabled; + m_preview_params_dirty = true; + } + m_imgui->disabled_end(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + m_imgui->tooltip(has_color ? + _u8L("Prints the painted area in the texture's colours as well as its " + "relief. Each colour is matched to the nearest of your loaded " + "filaments; anything you did not paint keeps the object's own.") : + _u8L("This texture is a grayscale height map, so it has no colours to " + "apply. Import a colour image to use this."), + wrap_w); + + // The rest of colour belongs to the whole stack, not to this layer, so it only appears once - + // under whichever layer turned colour on. + if (color_enabled) { + TextureDisplacementOptions &opts = mv->texture_displacement_options; + if (ImGui::Checkbox(_u8L("Mix filaments").c_str(), &opts.color_mix_enabled)) + m_preview_params_dirty = true; + hover_tip(_u8L("Interleaves two filaments to fake the colours in between, so a handful " + "of filaments can cover a photo or a gradient. An image of flat colours " + "prints the same either way. Off uses one filament per area.")); + if (opts.color_mix_enabled) { + slider_label(_L("Mix by")); + const std::string mix_z = _u8L("Layers"); + const std::string mix_xy = _u8L("Surface"); + const std::string mix_auto = _u8L("Automatic"); + const char *mix_items[] = { mix_z.c_str(), mix_xy.c_str(), mix_auto.c_str() }; + int mix_mode = int(opts.color_mix_mode); + ImGui::SetNextItemWidth(-card_pad); + if (scoped_combo("##color_mix_mode", &mix_mode, mix_items, IM_ARRAYSIZE(mix_items))) { + opts.color_mix_mode = ColorMixMode(mix_mode); + m_preview_params_dirty = true; + } + hover_tip(_u8L("Layers: the two filaments alternate between print layers, which " + "blends smoothly on upright surfaces but disappears on flat-facing " + "ones, where a whole layer is a single band.\n" + "Surface: a fine checkerboard across the surface, which works at " + "any angle but can read as texture rather than as a blend.\n" + "Automatic: layers on upright faces; flat-facing faces take the nearer " + "single filament, since a checkerboard there shows as a pattern.")); + ImGui::TextDisabled("%s", Slic3r::format(_u8L("%1% printable colours from %2% filaments"), + int(cached_palette().size()), int(m_palette_filaments.size())).c_str()); + } + if (int_row("##color_despeckle", _L("Denoise"), &opts.color_despeckle, 0, 6, "%d", card_pad)) + m_preview_params_dirty = true; + hover_tip(_u8L("Cleans up single stray triangles of the wrong colour, which detail finer " + "than the mesh leaves behind. Raise it if the result looks speckled, " + "lower it if small features are being swallowed.")); + } + } + + // Mapping, as five icons in TextureProjectionMethod's own order. + { + const float x0 = ImGui::GetCursorPosX(); + ImGui::AlignTextToFramePadding(); + ImGui::TextDisabled("%s", _u8L("Mapping").c_str()); + ImGui::SameLine(); + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w)); + struct MappingIcon + { + const char *file; + wxString label; + wxString tip; + }; + const MappingIcon mappings[] = { + { "menu_obj_cube.svg", _L("Triplanar (blended)"), + _L("Triplanar - projects the texture from all three axes at once and blends between them, so a " + "patch wrapping around a sharp edge has no seam.") }, + { "menu_obj_cylinder.svg", _L("Cylindrical"), + _L("Cylindrical - wraps the texture around the painted area's own centre, for round shapes.") }, + { "menu_obj_sphere.svg", _L("Spherical"), + _L("Spherical - wraps the texture around the painted area's own centre in both directions.") }, + { "texture_displacement_map_unwrap.svg", _L("Unwrap (LSCM)"), + _L("Unwrap - flattens the painted area and maps the texture onto it with as little stretching as " + "possible. The area is cut into pieces at its sharp edges first (see Seam angle), so each " + "piece can lie flat on its own.") }, + { "texture_displacement_map_view.svg", _L("From view"), + _L("From view - projects straight onto the painted area from where you are looking, like a slide " + "projector.") }, + }; + for (int mi = 0; mi < int(IM_ARRAYSIZE(mappings)); ++mi) { + if (mi > 0) + ImGui::SameLine(0.f, gap_s); + const auto method = static_cast(mi); + if (icon_toggle(830 + mi, mappings[mi].file, layer.projection_method == method, icon_sm, mappings[mi].label, + mappings[mi].tip) && + layer.projection_method != method) { + // Capture the current view the moment "From view" is chosen, so it does something + // sensible immediately rather than projecting from a stale/default direction. + if (method == TextureProjectionMethod::ViewProjected) + capture_view_projection(layer); + layer.projection_method = method; + m_preview_params_dirty = true; + // Unwrap and its tools live in the UV editor pane, so picking it opens the pane + // (via the preview rebuild this triggers). + if (method == TextureProjectionMethod::LSCM) + m_show_uv_editor = true; + } + } + } + + if (layer.projection_method == TextureProjectionMethod::LSCM) { + // Unwrap and everything that edits it live in the UV editor pane, next to the islands they change. + // Picking this mapping opens the pane; this brings it back after it has been closed. + if (m_imgui->button(_u8L("Open UV editor"))) { + m_show_uv_editor = true; + update_uv_editor(); + } + hover_tip(_u8L("Opens the UV editor, where you can see how the texture is laid out over the " + "painted area, cut seams and move the pieces around.")); + ImGui::SameLine(); + ImGui::AlignTextToFramePadding(); + if (m_uv_editor_unwrap.empty()) + ImGui::TextDisabled("%s", _u8L("Not unwrapped yet").c_str()); + else + ImGui::TextDisabled("%s", Slic3r::format(_u8L("%1% islands"), m_uv_editor_unwrap.chart_count).c_str()); + } + + if (layer.projection_method == TextureProjectionMethod::ViewProjected) { + // Re-capture the projector from wherever the camera is now: orbit the model, press this, and + // the texture is re-laid from the new angle. + if (m_imgui->button(_u8L("Capture current view"))) { + capture_view_projection(layer); + // Selecting the visible faces *after* capturing means the projector axes are already the + // ones the selection was made against - the two describe the same viewpoint. + if (m_project_only_visible && select_visible_faces() == 0) + show_error(nullptr, _u8L("Nothing is visible from this angle - turn the model to face the " + "part you want to project onto.")); + m_preview_params_dirty = true; + update_projector(); + } + hover_tip(_u8L("Projects the texture onto the painted area from where you are looking now, like " + "a slide projector. Faces turned away from you stretch, so line the view up with " + "the surface you care about first.")); + + if (ImGui::Checkbox(_u8L("Project only on visible").c_str(), &m_project_only_visible)) { + if (m_project_only_visible && select_visible_faces() == 0) + show_error(nullptr, _u8L("Nothing is visible from this angle - turn the model to face the " + "part you want to project onto.")); + m_preview_params_dirty = true; + update_projector(); + } + hover_tip(_u8L("Paints only the faces you can actually see right now - facing you and not hidden " + "behind anything - and projects onto those. Replaces what the layer had painted.")); + + bool projector_open = m_projector_frame != nullptr && m_projector_frame->IsShown(); + if (ImGui::Checkbox(_u8L("Projection frame").c_str(), &projector_open)) + show_projector(projector_open); + hover_tip(_u8L("Opens a window you drag over the model. Whatever you can see through it is what " + "gets the texture, and its border becomes the edge of the projection.")); + + if (projector_open) { + if (int_row("##projector_opacity", _L("Opacity"), &m_projector_opacity, 20, 255, "%d", card_pad)) + m_projector_frame->set_opacity(m_projector_opacity); + hover_tip(_u8L("How solid the frame window looks while you place it. Lower to see the " + "model through it; it does not affect the result.")); + + if (m_imgui->button(_u8L("Apply projection frame"))) { + const int painted_count = apply_projection_frame(); + if (painted_count == 0) + show_error(nullptr, _u8L("Nothing of the model is inside the frame - move it over the " + "part you want to project onto.")); + else if (painted_count < 0) + show_error(nullptr, _u8L("The frame could not be applied. Make sure it overlaps the 3D view.")); + } + hover_tip(_u8L("Projects the texture through the frame from where you are looking now and " + "paints the faces inside it. Replaces what the layer had painted; the result " + "sticks to the model, so you can orbit afterwards.")); + } + + if (layer.view_project_projective) { + ImGui::AlignTextToFramePadding(); + ImGui::TextDisabled("%s", _u8L("Placed by projection frame.").c_str()); + ImGui::SameLine(); + if (m_imgui->button(_u8L("Clear"))) { + // Back to the plain axis projection, where tiling/rotation/offset mean something again - + // the matrix path deliberately ignores them. + layer.view_project_projective = false; + layer.tile_enabled = true; + m_preview_params_dirty = true; + } + } + } + + // Tile, and how it repeats. + { + const float x0 = ImGui::GetCursorPosX(); + m_preview_params_dirty |= ImGui::Checkbox((_u8L("Tile") + "##tile_enabled").c_str(), &layer.tile_enabled); + hover_tip(_u8L("Repeats the texture across the painted area. Off places one copy, like a decal, " + "at the size set by Tile size.")); + ImGui::SameLine(); + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w)); + const wxString tile_na = layer.tile_enabled ? wxString() : _L("Turn Tile on to choose how the texture repeats."); + if (icon_toggle(840, "texture_displacement_tile_repeat.svg", layer.tile_method == static_cast(0), + icon_sm, _L("Repeat"), _L("Repeat"), tile_na)) { + layer.tile_method = static_cast(0); + m_preview_params_dirty = true; + } + ImGui::SameLine(0.f, gap_s); + if (icon_toggle(841, "menu_mirror_x.svg", layer.tile_method == static_cast(1), icon_sm, + _L("Mirrored repeat"), _L("Mirrored repeat - every other tile is flipped, so edges meet seamlessly"), + tile_na)) { + layer.tile_method = static_cast(1); + m_preview_params_dirty = true; + } + } + + // Blend, and placement on the model. + { + const float x0 = ImGui::GetCursorPosX(); + ImGui::AlignTextToFramePadding(); + ImGui::TextDisabled("%s", _u8L("Blend").c_str()); + ImGui::SameLine(); + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w)); + const std::string blend_add = _u8L("Add"); + const std::string blend_subtract = _u8L("Subtract"); + const std::string blend_multiply = _u8L("Multiply"); + const std::string blend_divide = _u8L("Divide"); + const char *blend_items[] = { blend_add.c_str(), blend_subtract.c_str(), blend_multiply.c_str(), blend_divide.c_str() }; + // The first layer has nothing before it to combine with - build_texture_displacement() makes it + // add regardless - so its combo shows Add and cannot be changed. + const bool base_layer = li == 0; + int blend_mode = base_layer ? 0 : static_cast(layer.blend_mode); + ImGui::SetNextItemWidth(-(card_pad + gap_s + icon_sm)); + m_imgui->disabled_begin(base_layer); + if (scoped_combo("##blend_mode", &blend_mode, blend_items, IM_ARRAYSIZE(blend_items)) && !base_layer) { + layer.blend_mode = static_cast(blend_mode); + m_preview_params_dirty = true; + } + m_imgui->disabled_end(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + m_imgui->tooltip(base_layer ? + _u8L("The base layer has nothing beneath it to combine with, so it always adds.") : + _u8L("What this layer does where it overlaps the ones below. Add and " + "Subtract pile relief on or carve it away. Multiply and Divide scale " + "what is underneath, which turns this layer into a mask over it - " + "there, Depth acts as a strength, and 1 mm leaves white areas " + "untouched."), + wrap_w); + + ImGui::SameLine(0.f, gap_s); + if (icon_toggle(842, "texture_displacement_adjust.svg", m_adjust_texture_mode, icon_sm, _L("Adjust placement"), + _L("Adjust placement - drag the handle on the model to move the texture"))) { + if (!m_adjust_texture_mode) { + update_model_object(); // flush any pending strokes before anchoring + if (update_adjust_anchor()) + m_adjust_texture_mode = true; + else + show_error(nullptr, _u8L("Paint something with this layer first.")); + } else { + m_adjust_texture_mode = false; + m_adjust_drag_handle = AdjustHandle::None; + } + } + } + } + + // "More settings" / "Fewer settings", drawn as a link. + { + const bool expanded = m_layer_expanded[size_t(layer.slot)]; + const std::string text = expanded ? _u8L("Fewer settings") : _u8L("More settings"); + const float arrow = std::round(ImGui::GetFontSize() * 0.55f); + const ImVec2 ts = ImGui::CalcTextSize(text.c_str()); + const ImVec2 p = ImGui::GetCursorScreenPos(); + if (ImGui::InvisibleButton("##more", ImVec2(arrow + gap_s + ts.x, ts.y))) { + m_layer_expanded[size_t(layer.slot)] = !expanded; + // Placement is one of the settings being hidden, so it stops with them. + if (expanded && m_adjust_texture_mode) { + m_adjust_texture_mode = false; + m_adjust_drag_handle = AdjustHandle::None; + } + } + const ImU32 c = ImGui::GetColorU32(col_link); + const float cx = p.x + 0.5f * arrow, cy = p.y + 0.5f * ts.y, hs = 0.3f * arrow; + if (expanded) + dl->AddTriangleFilled(ImVec2(cx - hs, cy + 0.5f * hs), ImVec2(cx + hs, cy + 0.5f * hs), ImVec2(cx, cy - 0.5f * hs), c); + else + dl->AddTriangleFilled(ImVec2(cx - hs, cy - 0.5f * hs), ImVec2(cx + hs, cy - 0.5f * hs), ImVec2(cx, cy + 0.5f * hs), c); + const float tx = p.x + arrow + gap_s; + dl->AddText(ImVec2(tx, p.y), c, text.c_str()); + if (ImGui::IsItemHovered()) + dl->AddLine(ImVec2(tx, p.y + ts.y), ImVec2(tx + ts.x, p.y + ts.y), c); + } + + ImGui::EndGroup(); + ImGui::Unindent(card_pad); + const float card_max_y = ImGui::GetItemRectMax().y + card_pad; + drop_target(li, layer.slot, card_min, ImVec2(content_rx, card_max_y)); + if (layer.slot == dragged_slot) + dl->AddRectFilled(card_min, ImVec2(content_rx, card_max_y), ghost_col, rounding); + dl->ChannelsSetCurrent(0); + dl->AddRectFilled(card_min, ImVec2(content_rx, card_max_y), col_card, rounding); + dl->ChannelsMerge(); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + card_pad); + } else { + // A closed layer: one outlined row - grip, chevron, thumbnail, name, depth, remove. A click + // anywhere that is not one of its buttons opens it. + const float inner = std::round(m_imgui->scaled(0.15f)); + const float row_h = frame_h + 2.f * inner; + const ImVec2 p0 = ImGui::GetCursorScreenPos(); + const float row_w = content_rx - p0.x; + if (ImGui::InvisibleButton("##row", ImVec2(row_w, row_h))) + activate_slot = layer.slot; + const bool row_hovered = ImGui::IsItemHovered(); + ImGui::SetItemAllowOverlap(); // the row's own buttons, submitted after it, still take their clicks + drop_target(li, layer.slot, p0, ImVec2(p0.x + row_w, p0.y + row_h)); + if (row_hovered) + dl->AddRectFilled(p0, ImVec2(p0.x + row_w, p0.y + row_h), col_card, rounding); + dl->AddRect(p0, ImVec2(p0.x + row_w, p0.y + row_h), col_line, rounding); + + ImGui::SetCursorScreenPos(ImVec2(p0.x + inner, p0.y + inner)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(gap_s, style.ItemSpacing.y)); + grip(layer, frame_h); + ImGui::SameLine(); + { + const ImVec2 c = ImGui::GetCursorScreenPos(); + const float aw = std::round(ImGui::GetFontSize() * 0.5f), hs = 0.3f * aw; + const float cx = c.x + 0.5f * aw, cy = c.y + 0.5f * frame_h; + dl->AddTriangleFilled(ImVec2(cx - 0.5f * hs, cy - hs), ImVec2(cx - 0.5f * hs, cy + hs), ImVec2(cx + 0.5f * hs, cy), + ImGui::GetColorU32(ImGuiCol_TextDisabled)); + ImGui::Dummy(ImVec2(aw, frame_h)); + } + ImGui::SameLine(); + const float th = std::round(frame_h * 0.9f); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 0.5f * (frame_h - th)); + if (GLTexture *thumb = get_layer_thumbnail(layer)) { + if (ImGui::ImageButton((ImTextureID) (intptr_t) thumb->get_id(), ImVec2(th, th), ImVec2(0.f, 0.f), ImVec2(1.f, 1.f), 0)) + open_picker(layer.slot); + } else if (ImGui::Button("?##thumb", ImVec2(th, th))) { + open_picker(layer.slot); + } + hover_tip(_u8L("Pick a different image for this layer. The paint, depth and tiling stay as they are.")); + ImGui::SameLine(); + + char depth_text[32]; + std::snprintf(depth_text, sizeof(depth_text), "%.3f mm", layer.depth_mm); + const std::string summary = painted ? std::string(depth_text) : _u8L("not painted"); + const float sum_w = ImGui::CalcTextSize(summary.c_str()).x; + const float right_edge = ImGui::GetWindowContentRegionMax().x - inner; + const float name_w = std::max(0.f, right_edge - ImGui::GetCursorPosX() - 2.f * gap_s - sum_w - frame_h); + ImGui::SetCursorPosY(p0.y - ImGui::GetWindowPos().y + ImGui::GetScrollY() + inner); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(ellipsize(layer_name(layer), name_w).c_str()); + ImGui::SameLine(); + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), right_edge - frame_h - gap_s - sum_w)); + ImGui::TextDisabled("%s", summary.c_str()); + ImGui::SameLine(); + ImGui::SetCursorPosX(right_edge - frame_h); + m_imgui->disabled_begin(busy); + if (icon_button(600 + layer.slot, "texture_displacement_cross.svg", frame_h, m_desc.at("remove_layer"), _L("Remove this layer"))) + slot_to_remove = layer.slot; + m_imgui->disabled_end(); + ImGui::PopStyleVar(); + if (layer.slot == dragged_slot) + dl->AddRectFilled(p0, ImVec2(p0.x + row_w, p0.y + row_h), ghost_col, rounding); + ImGui::SetCursorScreenPos(ImVec2(p0.x, p0.y + row_h + 0.5f * style.ItemSpacing.y)); + } + + ImGui::PopID(); + } + + // The dragged layer follows the cursor as a copy of its row, held by its grip. On the foreground draw list, so + // it stays visible over the rest of the panel and anywhere the drag wanders. + if (dragged_slot >= 0) { + const auto it = std::find_if(ordered.begin(), ordered.end(), [dragged_slot](const auto *l) { return l->slot == dragged_slot; }); + if (it != ordered.end()) { + const TextureDisplacementLayer &l = **it; + ImDrawList *fg = ImGui::GetForegroundDrawList(); + const float inner = std::round(m_imgui->scaled(0.15f)); + const float row_h = frame_h + 2.f * inner; + const float list_x = ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMin().x; + const ImVec2 mouse = ImGui::GetIO().MousePos; + const ImVec2 mn(mouse.x - inner - 0.5f * grip_w, mouse.y - 0.5f * row_h); + const ImVec2 mx(mn.x + (content_rx - list_x), mn.y + row_h); + fg->AddRectFilled(ImVec2(mn.x + 2.f, mn.y + 4.f), ImVec2(mx.x + 2.f, mx.y + 4.f), IM_COL32(0, 0, 0, 70), rounding); + fg->AddRectFilled(mn, mx, dark ? IM_COL32(0x3a, 0x3a, 0x40, 245) : IM_COL32(255, 255, 255, 245), rounding); + fg->AddRect(mn, mx, ImGui::GetColorU32(orca), rounding, 0, 1.5f); + + float x = mn.x + inner; + draw_drag_icon(fg, x, mn.y + inner, grip_w, frame_h, drag_tint(255)); + x += grip_w + gap_s; + const float th = std::round(frame_h * 0.9f); + if (GLTexture *thumb = get_layer_thumbnail(l)) { + const float ty = mn.y + 0.5f * (row_h - th); + fg->AddImage((ImTextureID) (intptr_t) thumb->get_id(), ImVec2(x, ty), ImVec2(x + th, ty + th)); + } + x += th + gap_s; + + char depth_text[32]; + std::snprintf(depth_text, sizeof(depth_text), "%.3f mm", l.depth_mm); + const std::string summary = slot_painted(l.slot) ? std::string(depth_text) : _u8L("not painted"); + const float sum_w = ImGui::CalcTextSize(summary.c_str()).x; + const float text_y = mn.y + 0.5f * (row_h - ImGui::GetFontSize()); + fg->AddText(ImVec2(mx.x - inner - sum_w, text_y), ImGui::GetColorU32(ImGuiCol_TextDisabled), summary.c_str()); + const std::string name = ellipsize(layer_name(l), std::max(0.f, mx.x - inner - sum_w - gap_s - x)); + fg->AddText(ImVec2(x, text_y), ImGui::GetColorU32(ImGuiCol_Text), name.c_str()); + } + } + + if (mv != nullptr) { + const bool full = mv->texture_displacement_layers.size() >= TEXTURE_DISPLACEMENT_MAX_LAYERS; + const std::string label = full ? Slic3r::format(_u8L("All %1% layers used"), TEXTURE_DISPLACEMENT_MAX_LAYERS) : + "+ " + _u8L("Add layer"); + m_imgui->disabled_begin(busy || full); + if (ImGui::Button((label + "##add_layer").c_str(), ImVec2(ImGui::GetContentRegionAvail().x, 0.f))) + add_texture_layer(); + m_imgui->disabled_end(); + } + + // ---- Mesh preparation: Pro only. Standard pins all of it and runs it from Bake. ---- + if (pro_mode()) { + ImGui::Separator(); + heading(_L("Subdivision")); + + if (ImGui::Checkbox(_u8L("Only painted area (adaptive)").c_str(), &m_subdivide_adaptive)) { + if (m_subdivide_editing) + rebuild_subdivide_preview(); // switch the wireframe between the uniform and adaptive result + m_parent.set_as_dirty(); + } + hover_tip(_u8L("Adds triangles only where you painted, instead of everywhere. On a large part with a small " + "decal that is the difference between thousands of triangles and millions, and your paint " + "survives the refinement.")); + + if (m_subdivide_adaptive) { + if (m_subdivide_target_mm <= 0.f && mv != nullptr) { + // Seed the target at about half the mesh's mean edge length, so the default already adds a + // useful amount of detail rather than landing on "no change". + const indexed_triangle_set &its = mv->mesh().its; + double sum = 0.0; + size_t cnt = 0; + for (const stl_triangle_vertex_indices &tri : its.indices) + for (int i = 0; i < 3; ++i) { + sum += (its.vertices[tri[i]] - its.vertices[tri[(i + 1) % 3]]).norm(); + ++cnt; + } + m_subdivide_target_mm = cnt > 0 ? std::clamp(float(sum / double(cnt)) * 0.5f, 0.001f, 20.f) : 1.f; + } + // Live preview: rebuild the wireframe as the slider moves, not only on release. The rebuild is + // bounded by the painted region, so it stays responsive. + const auto preview_live = [this]() { + if (m_subdivide_editing) + rebuild_subdivide_preview(); + m_parent.set_as_dirty(); + }; + + if (ImGui::Checkbox(_u8L("Follow texture detail").c_str(), &m_subdivide_feature)) + preview_live(); + hover_tip(_u8L("Spends the triangles where the texture actually bends - packed along ridges and edges, " + "sparse over flat ground - instead of spreading them evenly. The same detail for fewer " + "triangles on most textures.")); + + // The edge-length target is a baseline in both modes. In feature mode it is what guarantees the + // curvature test can actually see the texture: left too coarse, a big triangle over a fine + // pattern can sample four points that all happen to land at similar heights, report no error, + // and stall before refinement ever starts. + if (float_row("##subdiv_target", m_subdivide_feature ? _L("Max edge") : _L("Target edge"), &m_subdivide_target_mm, + 0.001f, 20.f, "%.3f mm", true, 0.f)) + preview_live(); + hover_tip(m_subdivide_feature ? + _u8L("No triangle in the painted area stays larger than this, even where the texture " + "is flat. Keep it near the size of the smallest feature you want to come out.") : + _u8L("Triangles in the painted area are split until none is larger than this. Smaller " + "means finer detail and, fast, a lot more triangles.")); + + if (m_subdivide_feature) { + if (float_row("##subdiv_detail", _L("Detail"), &m_subdivide_detail_mm, 0.001f, 1.f, "%.3f mm", true, 0.f)) + preview_live(); + hover_tip(_u8L("How far the mesh may sit from the shape the texture describes, in millimetres. " + "Smaller follows fine detail and costs triangles; larger only chases the big " + "features.")); + if (float_row("##subdiv_min", _L("Min edge"), &m_subdivide_min_edge_mm, 0.001f, 20.f, "%.3f mm", true, 0.f)) + preview_live(); + hover_tip(_u8L("A floor on triangle size, so a sharp step in the texture cannot be chased " + "forever. Lower it for finer relief, raise it if the count runs away at hard " + "edges.")); + } + + // Applies in both adaptive sub-modes: it is about the step the bake puts at the paint's edge, which + // exists whether or not "Follow texture detail" is on. 0 turns the band off. + if (float_row("##subdiv_border", _L("Edge detail"), &m_subdivide_border_mm, 0.f, 5.f, "%.3f mm", false, 0.f)) + preview_live(); + hover_tip(_u8L("Triangle size along the outline of the painted area, where the relief drops back to the " + "bare surface. Lower it if that rim looks jagged; it only costs triangles along the " + "outline. 0 turns it off.")); + + // Only worth showing when a layer is actually colouring: with no colour there is no boundary for it + // to refine and the control would do nothing whatever it is set to. + if (mv != nullptr && any_layer_colors(*mv)) { + if (float_row("##subdiv_color", _L("Colour detail"), &m_subdivide_color_mm, 0.f, 5.f, "%.3f mm", false, 0.f)) + preview_live(); + hover_tip(_u8L("Triangle size where two colours meet. Each triangle prints in one filament, so a " + "colour edge can only be as sharp as the triangles along it - and nothing else " + "refines there, since the surface is flat across a change of colour. 0 turns it off.")); + } + + ImGui::TextDisabled("%s", _u8L("Triangle budget: set with Triangles, below.").c_str()); + if (m_subdivide_editing && m_subdivide_preview_tris > 0) + m_imgui->text(Slic3r::format(_u8L("Preview: %1% triangles"), m_subdivide_preview_tris)); + } else { + if (int_row("##subdiv_steps", _L("Steps"), &m_subdivide_count, 0, 5, "%d", 0.f)) { + if (m_subdivide_editing) + rebuild_subdivide_preview(); // a count of 0 clears the preview, it doesn't compute one + m_parent.set_as_dirty(); + } + hover_tip(_u8L("Splits every triangle of the model into four, this many times over. Each step " + "quadruples the count - 3 turns 100 k triangles into 6.4 M - so prefer refining " + "only the painted area unless you need the whole model finer.")); + } + + { + // Subdivide is a no-op when there is nothing to commit: 0 uniform passes, or an adaptive target that + // is not set. + const bool subdivide_ready = m_subdivide_adaptive ? (m_subdivide_target_mm > 0.f) : (m_subdivide_count >= 1); + const float half = std::floor((ImGui::GetContentRegionAvail().x - style.ItemSpacing.x) * 0.5f); + m_imgui->disabled_begin(busy); + if (ImGui::Button((m_subdivide_editing ? _u8L("Hide preview") : _u8L("Preview subdivision")).c_str(), ImVec2(half, 0.f))) { + if (m_subdivide_editing) { + m_subdivide_editing = false; + m_subdivide_preview_tris = -1; + m_subdivide_preview_glmodel.reset(); + } else { + m_subdivide_editing = true; + rebuild_subdivide_preview(); + } + m_parent.set_as_dirty(); + } + m_imgui->disabled_end(); + hover_tip(_u8L("Shows what the refinement would produce as a wireframe, without touching the model.")); + ImGui::SameLine(); + m_imgui->disabled_begin(!subdivide_ready || busy); + if (ImGui::Button(_u8L("Subdivide").c_str(), ImVec2(half, 0.f))) { + if (m_subdivide_adaptive) { + subdivide_model_adaptive(); // refines only the painted area, carrying the paint forward + } else { + subdivide_model(); // commits m_subdivide_count passes (takes its own snapshot) + // Back to 0 rather than staying at the count just applied: the mesh is now up to 4^N times + // denser, so re-previewing the same N passes on top of it is both pointless and by far the + // slowest thing this panel does. + m_subdivide_count = 0; + } + if (m_subdivide_editing) + rebuild_subdivide_preview(); + m_parent.set_as_dirty(); + } + m_imgui->disabled_end(); + hover_tip(m_subdivide_adaptive ? + _u8L("Refines the painted area to the target edge length and carries your paint onto " + "the finer mesh. The rest of the model is left as it is.") : + _u8L("Replaces the model's geometry with the subdivided mesh and clears any not-yet-baked " + "paint on it (already-baked relief is unaffected).")); + } + + // Remesh: even out uneven triangle sizes (CGAL isotropic remeshing). + ImGui::Separator(); + heading(_L("Remeshing")); + if (m_remesh_target_edge_mm <= 0.f && mv != nullptr) { + // Seed the target with the model's current mean edge length, so the default is a sensible "make + // everything about the size it already averages". + const indexed_triangle_set &its = mv->mesh().its; + double sum = 0.0; + size_t cnt = 0; + for (const stl_triangle_vertex_indices &tri : its.indices) + for (int i = 0; i < 3; ++i) { + sum += (its.vertices[tri[i]] - its.vertices[tri[(i + 1) % 3]]).norm(); + ++cnt; + } + m_remesh_target_edge_mm = cnt > 0 ? std::clamp(float(sum / double(cnt)), 0.1f, 20.f) : 1.f; + } + float_row("##remesh_edge", _L("Target edge"), &m_remesh_target_edge_mm, 0.1f, 20.f, "%.2f mm", true, 0.f); + hover_tip(_u8L("Triangle size the whole model is rebuilt with, in millimetres. Displacement works best on an " + "even mesh; this is what makes one out of an uneven import.")); + + ImGui::Checkbox((_u8L("Keep sharp edges") + "##remesh_sharp").c_str(), &m_remesh_keep_sharp_edges); + hover_tip(_u8L("Holds hard edges and open borders in place while the rest is remeshed. Without it " + "the remesher slides vertices along the surface and rounds every crisp edge off - " + "a cube comes back with wobbly edges.")); + ImGui::SameLine(); + m_imgui->disabled_begin(!m_remesh_keep_sharp_edges); + ImGui::SetNextItemWidth(-1.f); + ImGui::SliderFloat("##remesh_sharp_angle", &m_remesh_sharp_angle_deg, 5.f, 90.f, "%.0f°", ImGuiSliderFlags_AlwaysClamp); + m_imgui->disabled_end(); + hover_tip(_u8L("How sharp a fold has to be, in degrees, before the remesher treats it as an edge worth " + "keeping. Lower protects more of the model's shape; higher rebuilds more of it evenly.")); + + m_imgui->disabled_begin(mv == nullptr || busy); + if (ImGui::Button(_u8L("Remesh").c_str())) + remesh_model(); + m_imgui->disabled_end(); + hover_tip(_u8L("Rebuilds the whole model with triangles close to this edge length - splitting the big " + "ones and merging the small ones - so displacement has an even density to work with. " + "Replaces the geometry; your paint is carried onto the new triangles spatially, so it " + "survives (already-baked relief is kept too).")); + + // Settings for the whole stack rather than one layer. Standard mode pins them instead of showing them. + if (mv != nullptr) { + TextureDisplacementOptions &opts = mv->texture_displacement_options; + ImGui::Separator(); + heading(_L("Result")); + + m_preview_params_dirty |= ImGui::Checkbox(_u8L("Displace up to the border").c_str(), &opts.displace_border); + hover_tip(_u8L("Lets the relief run right to the edge of the painted area. Turn it off to hold that " + "outer ring flat, which keeps the displacement strictly inside your paint but flattens " + "the pattern at the border.")); + + m_preview_params_dirty |= ImGui::Checkbox(_u8L("Smooth result").c_str(), &opts.smooth_enabled); + hover_tip(_u8L("Smooths the geometry after the texture has been applied, to take the hard steps out of a " + "low-resolution image. Only what the displacement moved is touched. The Smoothing slider " + "on a layer is a different thing: it blurs the image before it is used.")); + if (opts.smooth_enabled) { + float percent = opts.smooth_strength * 100.f; + if (float_row("##dispsmooth", _L("Strength"), &percent, 1.f, 100.f, "%.0f %%", false, 0.f)) { + opts.smooth_strength = std::clamp(percent / 100.f, 0.01f, 1.f); + m_preview_params_dirty = true; + } + hover_tip(_u8L("How far each pass pulls a vertex towards its neighbours. High values round the " + "relief off quickly; low values need more passes but keep more of the detail.")); + if (int_row("##dispsmoothit", _L("Passes"), &opts.smooth_iterations, 1, 10, "%d", 0.f)) + m_preview_params_dirty = true; + hover_tip(_u8L("How many smoothing passes to run. More passes spread the smoothing further across " + "the surface; Strength decides how much each one moves.")); + + m_preview_params_dirty |= ImGui::Checkbox(_u8L("Ignore outer ring").c_str(), &opts.smooth_skip_border); + hover_tip(_u8L("Keeps the outer ring of the painted area out of the smoothing. Its neighbours " + "outside the paint never move, so smoothing it drags the relief down and leaves the " + "pattern half-melted at the border.")); + + // The settings above ride along with Preview/Bake. This button is for geometry that has *already* + // been baked, where there is no displacement left to fold the smoothing into. + if (m_imgui->button(_u8L("Smooth baked mesh now"))) + smooth_model(); + hover_tip(_u8L("Applies the smoothing above to the model itself, right now, within the painted area. " + "For relief that is already baked in - anything not yet baked is smoothed by Bake.")); + } + } + } + + // Shown in both modes. The pipeline switch, the two v2 experiments and the stage debugger are + // developer controls: kept, but off the panel unless this is flipped. + static constexpr bool SHOW_PIPELINE_DEV_CONTROLS = false; + if (mv != nullptr) { + TextureDisplacementOptions &opts = mv->texture_displacement_options; + ImGui::Separator(); + if (SHOW_PIPELINE_DEV_CONTROLS) { + // The classic path is the opt-in: the one-run pipeline is the default, and its resolution + // control lives in the footer next to Bake (see below). + bool classic = !opts.pipeline_v2; + if (ImGui::Checkbox(_u8L("Experimental: classic bake pipeline").c_str(), &classic)) { + opts.pipeline_v2 = !classic; + m_preview_params_dirty = true; + } + hover_tip(_u8L("Bake by moving the vertices the mesh already has, after preparing it (remesh, " + "adaptive subdivision, and a cut along sharp steps in the texture). Keeps the " + "topology, which is what colours need. The default pipeline instead refines, " + "cleans up sliver triangles, displaces and simplifies in one run; nothing needs " + "preparing first, but it does not produce colours yet.")); + } + if (opts.pipeline_v2) { + // -1 is "auto": the row shows the recommendation, greyed; editing it makes it a fixed value. + const bool auto_budget = opts.v2_max_triangles_k < 0; + int shown_k = auto_budget ? v2_recommendation(*mv).budget_k : opts.v2_max_triangles_k; + m_imgui->disabled_begin(auto_budget); + if (int_row("##v2budget", _L("Budget"), &shown_k, 0, 4000, auto_budget ? "%d k (auto)" : "%d k", 0.f)) { + opts.v2_max_triangles_k = shown_k; + m_preview_params_dirty = true; + } + m_imgui->disabled_end(); + hover_tip(_u8L("How many thousand triangles this bake may spend on the area you painted. " + "Relief already baked into the rest of the model is kept on top of it, so a " + "second bake somewhere else gets the same budget as the first. Raise it if the " + "warning below Resolution says the detail will not fit; 0 keeps every triangle " + "the refinement produced, however many that is.")); + } + if (SHOW_PIPELINE_DEV_CONTROLS && opts.pipeline_v2) { + // Keeping the relief above the plate is not a checkbox: it is unconditional, in both pipelines + // (see build_texture_displacement()). + m_preview_params_dirty |= ImGui::Checkbox(_u8L("Align mesh to texture edges").c_str(), &opts.v2_relocate); + hover_tip(_u8L("Slide vertices sideways onto the edges in the texture before displacing them. " + "Displacement can only move vertices up and down, so without this a sharp step " + "in the image lands wherever the triangles happen to be and comes out as a " + "staircase. Moving the vertices onto the step first gives a straight wall at the " + "same triangle count.")); + m_preview_params_dirty |= ImGui::Checkbox(_u8L("Clean up slivers").c_str(), &opts.v2_regularize); + hover_tip(_u8L("Collapse the thin triangles refinement inherits from the model's own " + "tessellation, before displacement samples them. A sliver's three corners " + "land on three unrelated parts of the texture, which is what makes the " + "relief look jagged.")); + } + // Below both pipelines' own settings, because it replays whichever of them is selected. + if (SHOW_PIPELINE_DEV_CONTROLS) + render_debug_stage_panel(mv); + } + + // Content height, for next frame's body size. The cursor position is scroll-compensated, so this is the + // height of everything above, whether or not it is scrolled. + m_panel_body_h = ImGui::GetCursorPosY() - style.ItemSpacing.y; + ImGui::EndChild(); + ImGui::PopStyleColor(5); + ImGui::PopStyleVar(2); + + // The layer-list changes deferred from the loop above. + if (slot_to_remove >= 0) + remove_texture_layer(slot_to_remove); + else if (move_slot >= 0) + move_texture_layer(move_slot, move_to); + else if (activate_slot >= 0) + set_active_layer(activate_slot); + // Retexturing a layer makes it the one being painted, too. + if (m_picker_open_request && m_picker_slot >= 0) + set_active_layer(m_picker_slot); + + // In this window's scope, where the popup's open request and the popup itself share one ID stack. + { + const ImVec2 win_min = ImGui::GetWindowPos(); + const ImVec2 win_sz = ImGui::GetWindowSize(); + render_texture_library_popup(win_min, ImVec2(win_min.x + win_sz.x, win_min.y + win_sz.y)); + } + + // ImGui sliders report "changed" continuously on every frame while being dragged, not just once on + // release - rebuilding the preview (a real CPU mesh recompute) on every one of those frames is what made + // dragging these sliders feel slow. Only rebuild once the mouse button that's driving the drag is + // released, i.e. once per edit instead of dozens of times per drag. + // The feature-adaptive subdivision follows the *displaced* surface (it samples depth * texture), so depth / + // tile-size / rotation / invert all change where it puts triangles. The heavy displacement preview below + // only rebuilds on release, which made the subdivide wireframe look like it ignored those edits - so + // rebuild it live here (during the drag), the same cadence its own sliders use. Cheap: it is bounded by + // the painted region. + if (m_preview_params_dirty && m_subdivide_editing && m_subdivide_adaptive && m_subdivide_feature) + rebuild_subdivide_preview(); + if (m_preview_params_dirty && (m_auto_update || !ImGui::IsMouseDown(ImGuiMouseButton_Left))) { + rebuild_preview(); + // Same edits (tile size, rotation, offset, a new texture) are what the projector window draws, so it + // refreshes on the same one-per-edit cadence. No-op while it is closed. + update_projector(); + m_preview_params_dirty = false; + } + + // ---- Footer: pinned below the body, so Bake never scrolls away. ---- + const float footer_top = ImGui::GetCursorScreenPos().y; + ImGui::Separator(); + { + const float x0 = ImGui::GetCursorPosX(); + const int base_k = mv != nullptr ? int((mv->mesh().facets_count() + 500) / 1000) : 0; + const bool v2 = mv != nullptr && mv->texture_displacement_options.pipeline_v2; + + // The triangle budget is the one subdivision control Standard mode keeps: its right value depends on + // the part rather than on the recipe (a big model, or a fine texture, simply needs more of them), and + // raising or lowering it is safe without understanding anything else. Pro's Subdivide uses it too. + ImGui::AlignTextToFramePadding(); + if (v2) { + // The one control the default pipeline needs: the edge length its refinement targets, which + // decides how much of the texture the mesh can carry. About eight texels per edge is where + // fine detail (mortar joints, knurl ridges) stops being lost between vertices. + TextureDisplacementOptions &opts = mv->texture_displacement_options; + const V2Resolution &rec = v2_recommendation(*mv); + m_imgui->text(_L("Resolution")); + ImGui::SameLine(); + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w)); + // Auto: the edge (and the budget) follow the texture and the model. Off: the slider holds + // a fixed value, seeded with the recommendation so it starts from something sensible. + bool auto_res = opts.v2_refine_mm <= 0.f; + m_imgui->disabled_begin(busy); + if (ImGui::Checkbox("##v2auto", &auto_res)) { + if (auto_res) { + opts.v2_refine_mm = 0.f; + opts.v2_max_triangles_k = -1; + } else { + opts.v2_refine_mm = rec.edge_mm > 0.f ? rec.edge_mm : 0.3f; + opts.v2_max_triangles_k = rec.budget_k > 0 ? rec.budget_k : 750; + } + m_preview_params_dirty = true; + m_parent.set_as_dirty(); + } + m_imgui->disabled_end(); + hover_tip(_u8L("On: the resolution follows the size of the model and the budget is left at its " + "default. Untick to set both yourself - worth doing for a texture much finer or " + "much coarser than the part it sits on.")); + ImGui::SameLine(); + ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX()); + float shown = auto_res ? rec.edge_mm : opts.v2_refine_mm; + m_imgui->disabled_begin(busy || auto_res); + if (ImGui::SliderFloat("##v2edge", &shown, 0.02f, 2.f, auto_res ? "%.2f mm (auto)" : "%.2f mm", + ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic)) { + opts.v2_refine_mm = shown; + m_preview_params_dirty = true; + m_parent.set_as_dirty(); + } + m_imgui->disabled_end(); + if (auto_res && rec.edge_mm > 0.f) + hover_tip(Slic3r::format(_u8L("Chosen from the size of the model: %1$.2f mm triangles, " + "budget %2% k. Untick Auto to set them yourself."), + rec.edge_mm, rec.budget_k)); + else + hover_tip(_u8L("How fine the mesh is made under the paint, in millimetres. It has to be " + "smaller than the detail you want out of the texture - a 0.5 mm groove needs " + "triangles well under 0.5 mm. Smaller costs triangles fast: halving it needs " + "four times as many, and once they no longer fit the budget the bake simplifies " + "back down and the detail goes with it.")); + + // What this resolution costs over what is painted, against what the budget allows. Refining + // past the budget is not an error - the bake simplifies back down to it - but the result + // then carries less of the texture than the resolution asks for, and the only sign of that + // used to be a mesh that came out coarser than expected. Shown before the bake, so the + // answer is to change a number rather than to wait out a bake and redo it. + const float edge_now = auto_res ? rec.edge_mm : opts.v2_refine_mm; + const int budget_k = opts.v2_max_triangles_k < 0 ? rec.budget_k : opts.v2_max_triangles_k; + const size_t budget = size_t(std::max(0, budget_k)) * 1000; + const size_t needed = estimated_refined_triangles(*mv, edge_now); + // Only when it is clearly over: the estimate runs about 3% high where it matters and up to + // a third high at coarse resolutions, where the mesh's own triangles are already near the + // target, and a warning about a bake that would have fitted is worse than none. + if (budget > 0 && needed > budget * 5 / 4) { + // Thousands under a million: a 119 k budget shown as "0.1 M" says nothing. + const auto count = [](size_t n) { + return n >= 1000000 ? Slic3r::format("%1$.1f M", double(n) / 1000000.) : + Slic3r::format("%1% k", (n + 500) / 1000); + }; + // Wrapped to the panel, like the note under the buttons: unwrapped text runs past the + // panel's edge and takes the window's width with it. + ImGui::PushTextWrapPos(x0 + panel_w); + m_imgui->warning_text(Slic3r::format(_u8L("Needs about %1% triangles, budget %2% - the bake " + "will simplify back down and lose detail."), + count(needed), count(budget))); + ImGui::PopTextWrapPos(); + } + } else { + m_imgui->text(_L("Triangles")); + ImGui::SameLine(); + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w)); + ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX()); + m_imgui->disabled_begin(busy); + if (ImGui::SliderInt("##subdivbudget", &m_subdivide_budget_k, 10, 2000, "+%d k", + ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic)) { + if (m_subdivide_editing) + rebuild_subdivide_preview(); + m_parent.set_as_dirty(); + } + m_imgui->disabled_end(); + hover_tip(_u8L("How many thousand triangles the refinement may add to the model. The triangle " + "that fits worst is always split first, so even a run that spends the lot has " + "spent it where it shows most. Raise it if the relief still looks coarse.")); + } + + const float button_h = std::round(frame_h * 1.25f); + const float third = std::floor((panel_w - style.ItemSpacing.x) / 3.f); + if (busy) { + if (ImGui::Button((_u8L("Stop") + "##stop").c_str(), ImVec2(third, button_h))) + wxGetApp().plater()->get_ui_job_worker().cancel_all(); + hover_tip(_u8L("Stops the bake. Whatever it had already finished stays on the model, and can be undone.")); + } else { + if (ImGui::Button((_u8L("Close") + "##close").c_str(), ImVec2(third, button_h))) + m_parent.reset_all_gizmos(); + hover_tip(_u8L("Closes the tool without baking. Your paint, layers and settings stay with the model.")); + } + + ImGui::SameLine(); + const bool can_bake = !busy && mv != nullptr && mv->is_texture_displacement_painted(); + const std::string bake_label = m_prepare_in_progress ? _u8L("Preparing...") : + m_bake_in_progress ? _u8L("Baking...") : + into_u8(m_desc.at("bake")); + ImGui::PushStyleColor(ImGuiCol_Button, orca); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGuiWrapper::COL_ORCA_HOVER); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, orca); + ImGui::PushStyleColor(ImGuiCol_Border, orca); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f)); + m_imgui->push_bold_font(); + m_imgui->disabled_begin(!can_bake); + if (ImGui::Button((bake_label + "##bake").c_str(), ImVec2(x0 + panel_w - ImGui::GetCursorPosX(), button_h))) { + // Standard mode's Bake is the whole pipeline (remesh -> refine -> displace); Pro's is only the + // displacement, because there the user has already prepared the mesh with the controls above. + if (pro_mode()) + bake(); + else + bake_standard(); + } + m_imgui->disabled_end(); + m_imgui->pop_bold_font(); + ImGui::PopStyleColor(5); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + m_imgui->tooltip(mv != nullptr && !mv->is_texture_displacement_painted() ? + (m_seam_edit_mode ? _u8L("Nothing is painted yet. The UV editor's seam tool is on, so " + "strokes on the model mark seams instead of painting - turn " + "it off in the pane to paint.") : + _u8L("Nothing is painted yet.")) : + pro_mode() ? + _u8L("Turn the painted height maps into real geometry, by moving the vertices that are " + "already there. Use Subdivide first if the mesh is too coarse to show the detail.") : + _u8L("Turn the painted height maps into real geometry. The mesh is remeshed to an even " + "density and refined where the texture bends first, so the detail has vertices to " + "land on - all in one step."), + wrap_w); + + // What Bake will produce, and which layers it will skip. + if (mv != nullptr) { + const std::string base_count = base_k > 0 ? Slic3r::format(_u8L("%1% k"), base_k) : + std::to_string(mv->mesh().facets_count()); + std::string note = v2 ? Slic3r::format(_u8L("Model: %1% triangles - the bake refines to the resolution above, up to its budget"), base_count) : + pro_mode() ? Slic3r::format(_u8L("Bake moves existing vertices - the model stays at %1% triangles"), base_count) : + Slic3r::format(_u8L("Bake result up to ~%1% k triangles"), base_k + m_subdivide_budget_k); + if (mv->is_texture_displacement_painted()) { + std::vector skipped; + for (const TextureDisplacementLayer &l : mv->texture_displacement_layers) + if (!slot_painted(l.slot)) + skipped.push_back(layer_name(l)); + if (skipped.size() > 2) + note += ", " + Slic3r::format(_u8L("%1% layers not painted, skipped"), skipped.size()); + else if (!skipped.empty()) + note += ", " + Slic3r::format(_u8L("%1% not painted, skipped"), + skipped.size() == 2 ? skipped[0] + ", " + skipped[1] : skipped[0]); + } + ImGui::PushTextWrapPos(x0 + panel_w); + ImGui::TextDisabled("%s", note.c_str()); + ImGui::PopTextWrapPos(); + } + } + m_panel_footer_h = ImGui::GetCursorScreenPos().y - footer_top; + + GizmoImguiEnd(); + ImGuiWrapper::pop_toolbar_style(); + + // Drawn last, over everything, via the foreground draw list: the +/- add-remove sign next to the 3D + // cursor. Still inside the gizmo's ImGui frame here, which is what render_paint_cursor_hint() needs. + render_paint_cursor_hint(); +} + +} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp new file mode 100644 index 0000000000..705408b153 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp @@ -0,0 +1,946 @@ +#ifndef slic3r_GLGizmoTextureDisplacement_hpp_ +#define slic3r_GLGizmoTextureDisplacement_hpp_ + +#include "GLGizmoPainterBase.hpp" +#include "libslic3r/TextureBake/TextureBakeDebug.hpp" +#include "libslic3r/TextureDisplacement.hpp" +#include "slic3r/GUI/GLModel.hpp" +#include "slic3r/GUI/GLTexture.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/IconManager.hpp" +#include "slic3r/GUI/TextureLibrary.hpp" + +#include +#include +#include +#include +#include + +namespace Slic3r::GUI { + +class TextureProjectorFrame; + +// Paint-style gizmo that assigns one or more texture-displacement "layers" (see +// libslic3r/TextureDisplacement.hpp) to painted areas of a model, and can bake the result into +// real mesh geometry. See the project plan for the overall architecture; in short: +// - each layer owns its own independent paint mask (ModelVolume::texture_displacement_facets), +// reusing the same TriangleSelector/FacetsAnnotation machinery as every other paint gizmo - +// only one layer is "active" (paintable) at a time, selected in the panel below; +// - "Bake" runs build_texture_displacement() in a background job and commits the result exactly +// like the Emboss/SVG "project on surface" gizmo does. +class GLGizmoTextureDisplacement : public GLGizmoPainterBase +{ +public: + GLGizmoTextureDisplacement(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); + + // The whole of mesh preparation - remesh, carry the paint across, refine where the texture bends - + // as one pure function over plain data: no ModelVolume, no Model, no GUI, no undo. That is what lets + // TextureDisplacementPrepareJob run it on the job worker instead of on the UI thread, where CGAL's + // remesher and a several-hundred-thousand-triangle refinement together freeze the window for tens + // of seconds with nothing to look at and no way to cancel. + // + // `progress` is called with 0..100 and aborts the run when it returns false; an aborted run reports + // an empty result. An empty result is also how "nothing needed doing" is reported - see + // TextureDisplacementPrepareResult. + static TextureDisplacementPrepareResult prepare_mesh(const indexed_triangle_set &base, + const TextureDisplacementFacetsData &masks, + const std::vector &layers, + const TextureDisplacementPrepareParams ¶ms, + const std::vector &palette, + const DisplacementProgressFn &progress, + // Optional step capture: receives the mesh + // after the remesh and after the refinement, + // so a debug run can show the whole recipe. + BakeStageRecorder *debug = nullptr); + // The volume's eight texture-displacement masks, gathered into the array every pure function here + // (and every job input) takes. + static TextureDisplacementFacetsData facets_data_of(const ModelVolume &mv); + + using PaletteEntry = PrintableColor; + + // The printable palette: the loaded filaments (clamped to the sixteen mmu_segmentation_facets can + // address), plus - when `mixing` - every pair of them at evenly spaced ratios. + // + // Mixes are averaged in **CIELAB**, not RGB and not subtractively: two filaments interleaved too + // finely to resolve are averaged by the eye, which is what a perceptual space models. Yellow and + // blue banded together read as a desaturated grey-green, and that is what the preview must promise + // - blending them subtractively would show a green the printer cannot produce this way. + // + // How many ratios depends on how many filaments there are, so the palette stays bounded: the + // quantizer's lookup cube costs one DeltaE00 per cell per entry to fill, and with sixteen + // filaments there are already plenty of colours without mixing any of them. + static std::vector make_palette(const std::vector &filaments, bool mixing); + + // Maps an image colour to the closest entry of `palette`, perceptually (CIEDE2000 over CIELAB - a + // plain RGB distance picks visibly wrong filaments, most obviously between a saturated colour and + // a grey of similar brightness). + // + // Precomputed into a lookup cube rather than matched per call: the subdivision's colour criterion + // samples up to seven points per triangle and re-samples both children of every split, so a live + // match would dominate the refinement. The returned closure owns the cube, so it is safe to hand + // to a worker thread and outlives the palette it was built from. + static ColorQuantizeFn make_palette_quantizer(const std::vector &palette); + + // Turns a palette index plus a position into the filament to print there, interleaving the two + // filaments of a mixed entry per `mode`. `layer_height` sizes the Z bands; `cell_mm` the dither + // cells. See ColorResolveFn for why this is separate from the quantizer. + static ColorResolveFn make_mix_resolver(const std::vector &palette, ColorMixMode mode, + float layer_height, float cell_mm); + + // Everything the jobs need to colour with, for the current volume: palette, mix mode, layer + // height, despeckle. Empty when no layer is actually colouring. + TextureColorSettings color_settings_for(const ModelVolume &mv); + + // The printable palette for the current filaments and mixing setting, rebuilt only when either + // actually changes - see the definition for why that caching is not optional. + const std::vector &cached_palette(); + std::vector m_palette_cache; + std::vector m_palette_filaments; + bool m_palette_mixing = false; + ColorQuantizeFn m_palette_quantizer; + + // The loaded filaments, clamped to the sixteen mmu_segmentation_facets can address. + static std::vector filament_palette(); + // The print's layer height, which sizes ColorMixMode::ZBands. Falls back to 0.2 mm if it cannot be + // read - a wrong band size is a cosmetic error, not a reason to refuse to colour anything. + static float print_layer_height(); + // The Z band height, in mm. One print layer is the ideal, but the interleave is realised per + // *facet*: a band thinner than the mesh can resolve does not dither, it beats against the triangle + // grid and comes out as broad horizontal stripes - and since MMU segmentation reads facet colour, + // it does so in the print too, not only on screen. The refinement edge is chosen from the model's + // diagonal and knows nothing about the layer height, so the band is rounded up to a whole number of + // layers at least two facet rows tall: still exact on the printer, and representable by the mesh + // that has to carry it. Used by both the bake settings and the preview shader, so the two agree. + float color_band_mm(const ModelVolume &mv); + + // The Normal preview's triangles, grouped by the filament they will print in. Colour is per facet + // and there are at most sixteen filaments, so the mesh is uploaded once with its index buffer + // sorted by colour and drawn as one GLModel::render(range) per group - which needs no per-vertex + // colour attribute, and so no change to GLModel's vertex layouts. + // + // The *index buffer* is what gets reordered, never m_preview_its: the paint overlay and the + // wireframe index into that by the volume's own triangle numbering (the bake is + // topology-preserving), and permuting it would silently misplace both. + struct PreviewColorRun + { + std::pair range; // into the GLModel's index buffer, in elements + ColorRGBA color; + }; + std::vector m_preview_color_runs; + // True if any of the volume's layers would actually colour something: colour turned on, and a + // texture that has colour to give. What decides whether a palette is captured into a job at all, + // and so whether the colour criterion and the mmu write ever run. + static bool any_layer_colors(const ModelVolume &mv); + + void render_painter_gizmo() override; + + // Intercepts mouse input while "Adjust Texture" mode is on (dragging the on-canvas offset/ + // rotation handles instead of painting); otherwise forwards to the normal painting handling. + bool on_mouse(const wxMouseEvent &mouse_event) override; + +protected: + void on_render_input_window(float x, float y, float bottom_limit) override; + std::string on_get_name() const override; + + wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override; + + std::string get_gizmo_entering_text() const override { return _u8L("Entering Texture displacement painting"); } + std::string get_gizmo_leaving_text() const override { return _u8L("Leaving Texture displacement painting"); } + std::string get_action_snapshot_name() const override { return _u8L("Texture displacement editing"); } + + // The panel's Paint / Erase toggle swaps what the two buttons do, so the right button always does the + // opposite of the left. Shift still erases in both modes (GLGizmoPainterBase::gizmo_event()). + EnforcerBlockerType get_left_button_state_type() const override { return m_erase_mode ? EnforcerBlockerType::NONE : EnforcerBlockerType::ENFORCER; } + EnforcerBlockerType get_right_button_state_type() const override { return m_erase_mode ? EnforcerBlockerType::ENFORCER : EnforcerBlockerType::NONE; } + +private: + bool on_init() override; + void update_model_object() override; + void update_from_model_object(bool first_update) override; + void on_opening() override {} + void on_shutdown() override; + PainterGizmoType get_painter_type() const override; + + // Phase 1 restricts the texture layer list to the first model-part volume of the current + // object (the common single-volume case); multi-part objects only get texture layers on + // their first part until a later phase. Returns nullptr if there is no model part. + ModelVolume* texture_volume(); + const ModelVolume* texture_volume() const; + + void add_texture_layer(); + void remove_texture_layer(int slot); + // Moves the layer in `slot` to position `to_index` of the slot-ordered stack (an insertion index, + // 0..layer count), which is the order blend modes apply in. Layers keep their data; what moves is + // the slot each one occupies, so their paint masks move with them on every model part. + void move_texture_layer(int slot, int to_index); + // Exchanges everything two slots hold - layer definition, paint masks, panel caches. No snapshot and + // no selector reload; move_texture_layer() does both once around a run of these. + void swap_layer_slots(int slot_a, int slot_b); + void set_active_layer(int slot); // flushes the previous layer's edits, then reloads selectors + // `own_snapshot` false when the caller has already taken an undo step that is meant to cover the + // displacement too - see bake_standard(). + void bake(bool own_snapshot = true); + + // Standard (0) vs Pro (1), driven by the two-position slider in the panel header. + // + // Pro is the panel as it has always been: every geometry-preparation control is visible and the + // user drives Remesh, Subdivide and Bake themselves, in whatever order they like. Standard hides + // all of that, pins it to one fixed recipe, and folds it into the Bake button - paint, press Bake, + // done - so the common case does not require knowing that a height map can only move vertices that + // already exist. Nothing about Pro changed when Standard was added. + int m_panel_mode = 0; + bool pro_mode() const { return m_panel_mode != 0; } + // Pins every control Standard mode hides to its preset value. Idempotent, called each frame while + // Standard is active so what Preview shows is always what Bake will do. Returns true if it actually + // changed something, so the caller can invalidate the preview. + bool apply_standard_mode_presets(ModelVolume *mv); + // Standard mode's Bake: remesh to an even density, refine where the texture bends, then displace. + // The order matters and is the whole reason this is one button - a height map can only move + // existing vertices, so the mesh has to be prepared first, and remeshing after painting would drop + // the paint if it were not carried across (see prepare_mesh()). + void bake_standard(); + + // Queues one prepare_mesh() run on the job worker and commits its result when it lands. Every + // mesh-preparation button goes through here - Pro's Remesh, Pro's adaptive Subdivide and Standard's + // Bake differ only in which stages `params` enables and in what happens afterwards: + // - `snapshot_name` is the single undo step the commit opens. With `then_bake` it is also the step + // the displacement job that follows commits into, rather than pushing its own - an undo landing + // between the two would leave a mesh carrying every added triangle and no relief on it, and + // baking again from there would prepare it a second time. + // - `unchanged_msg`, when not empty, is shown if the run had nothing to do. Standard's Bake passes + // nothing: a mesh that already meets the criteria is not an error there, it just goes straight + // on to the displacement. + void queue_prepare(const TextureDisplacementPrepareParams ¶ms, const std::string &snapshot_name, + bool then_bake, const std::string &unchanged_msg); + // Set from queue_prepare() until its job's result has been committed. Distinct from + // m_bake_in_progress because Standard's Bake sets both in turn, and because every button that would + // read or replace the mesh has to stay disabled for the whole of it. + bool m_prepare_in_progress = false; + + // How one layer's paint sits on the pre-subdivision mesh, precise enough to carry across the + // refinement without rounding each source triangle to wholly painted or not. + // + // Rounding is what made the outline of a painted region come out ragged: a source triangle near a + // smooth brush boundary is wholly painted essentially at random, so "painted iff the source was + // full" turns a clean curve into a noisy fringe of isolated painted and unpainted triangles - and + // once the border band refines the mesh there, that fringe is reproduced faithfully instead of + // being blurred away by coarse geometry. + struct LayerPaintMap + { + std::vector full; // per source triangle: covered edge to edge + std::vector part_start; // CSR offsets into `part`, size (source tris + 1) + std::vector> part; // painted pieces of partly covered source triangles + bool empty() const { return full.empty(); } + }; + // Rebuilds every layer's mask on a subdivided mesh from `source` (new triangle -> the input triangle + // it descends from) and the pre-subdivision coverage in `paint`. + static TextureDisplacementFacetsData masks_after_subdivision( + const TriangleMesh &new_mesh, const std::vector &source, + const std::array &paint); + // False means the remesh failed or changed nothing (CGAL signals failure by handing the input back), + // and `out` must not be used. `target_edge_mm` is a request rather than a promise: it is clamped + // against the part's surface area first, because CGAL's cost grows with the square of 1/target. + static bool plan_remesh(const indexed_triangle_set &src, float target_edge_mm, float sharp_angle_deg, + indexed_triangle_set &out); + + // Marks every facet of every model-part volume as painted for the currently active layer - + // "whole model" as an alternative to brushing/clicking every triangle by hand. + void select_whole_model(); + + // The mesh raycasters are built one per model-part volume, in that order; this is the texture + // volume's slot among them, or -1 if it has none (no selection, or the lists disagree). + int texture_volume_raycaster_index() const; + + // Paints exactly the facets currently visible from the camera onto the active layer, replacing + // whatever that layer had painted. "Visible" is two tests: the facet faces the camera, and its + // centroid is not hidden behind other geometry (a real raycast, so a concave part's far inner + // wall is correctly excluded). When `uv_clip` is given (the projection frame's matrix), facets + // whose centroid falls outside the frame's uv unit square are skipped first - which both clips + // the selection to the frame and spares the raycast for everything outside it. Costs one ray + // query per surviving facet, so it is a one-shot action, never a per-frame one. Returns the + // number of facets selected. + int select_visible_faces(const std::array *uv_clip = nullptr); + + // When set, "Capture current view" also re-selects the visible faces, so the viewpoint the + // projector was captured from and the area it projects onto stay the same. Independent of the + // projection frame below: this takes every visible facet, the frame clips to its rectangle. + // Off by default, because turning it on replaces whatever the layer had painted. + bool m_project_only_visible = false; + + // The projection-frame overlay for a ViewProjected layer: a semi-transparent window dragged over + // the 3D view whose border becomes the projection's edge. Created lazily and owned here; hidden + // rather than destroyed when closed, so reopening keeps it where the user left it. + TextureProjectorFrame *m_projector_frame = nullptr; + int m_projector_opacity = 140; + // What the overlay's texture was last built from, so repeated updates don't rebuild the bitmap + // from unchanged pixels. Same shape as the m_thumbnail_source/m_thumbnail_smoothing pair above. + const void *m_projector_tex_source = nullptr; + float m_projector_tex_smoothing = -1.f; + void show_projector(bool show); + // Pushes the active layer's texture into the overlay. Cheap, and a no-op while it is hidden. + void update_projector(); + // Reads the overlay's rectangle and commits it as the layer's projection: builds the exact + // projective local->uv matrix from the camera and that rectangle, turns tiling off so the border + // is a hard edge, and repaints the layer with the visible facets inside the frame. Returns the + // number of facets selected, or -1 if the frame could not be used at all. + int apply_projection_frame(); + + // Uniformly subdivides the volume's mesh (see libslic3r::subdivide_mesh_uniform()) so a + // low-poly input model has enough vertices to actually show texture-displacement detail. + // A real, committed geometry change (like Bake), so it needs its own snapshot; unlike Bake it + // has no target region, so any not-yet-baked paint on the volume is dropped rather than + // remapped (texture-displacement paint has no remap-across-topology-change support yet). + void subdivide_model(); + + // The layer height map's width / height, for apply_uv_transform()'s non-square handling. 1 when + // there is no usable texture. + static float layer_texture_aspect(const TextureDisplacementLayer &layer); + + // Returns a cached GPU thumbnail of layer's texture (decoding + uploading it the first time it + // is requested, or whenever its image_data changes), or nullptr if it has no usable texture. + // Panel-sized: box-filtered down to THUMBNAIL_MAX_PX, which is right for a list row and wrong for + // anything the shader samples - see get_layer_height_texture(). + GLTexture *get_layer_thumbnail(const TextureDisplacementLayer &layer); + + // The same texture at full resolution, for the fast-preview shader. One slot, shared by whichever + // layer is active, because that is the only one the preview shader ever shades. + GLTexture *get_layer_height_texture(const TextureDisplacementLayer &layer); + // The layer's colour texture for the fast preview's per-fragment quantization. Null when the + // layer is not colouring or its texture is grayscale. + GLTexture *get_layer_color_texture(const TextureDisplacementLayer &layer); + + // A texture from the picker's library (see slic3r/GUI/TextureLibrary.hpp), read and uploaded + // once and then kept for the gizmo's lifetime. The decoded bytes are held alongside the GPU + // thumbnail so that picking the texture can hand the layer this very same image_data buffer - + // which both avoids re-reading the file and lets decode_height_texture()'s own cache (keyed by + // exactly this pointer) hit immediately on the first bake/preview. + struct LibraryTexture + { + std::shared_ptr> image_data; + std::unique_ptr thumbnail; + }; + const LibraryTexture *get_library_texture(const std::string &path); + + // The texture library: a popup grid of thumbnails beside the panel, shipped textures and the user's + // own under separate headings, with a tile that imports an image from disk. Opened for + // m_picker_slot by a click on a layer's thumbnail or name. `panel_min`/`panel_max` are the panel + // window's screen rectangle, which the popup is placed against. + void render_texture_library_popup(const ImVec2 &panel_min, const ImVec2 &panel_max); + int m_picker_slot = -1; + bool m_picker_open_request = false; + void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry); + void import_custom_texture(TextureDisplacementLayer &layer); + + // Paint / Erase, from the panel. See get_left_button_state_type(). + bool m_erase_mode = false; + // Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation. + // Panel state only, not saved with the project. + std::array m_layer_expanded{}; + + // "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active + // layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane + // (a "pan" - drag anywhere on it for free 2D movement), plus two arrows along the patch's + // own U/V axes that constrain the drag to just that one axis for precise nudging. Anchored to + // the centroid/average-normal of the active layer's current paint patch (see + // libslic3r::compute_layer_paint_anchor()), so nothing is drawn if it has nothing painted yet. + // + // NOTE: the drag direction/sign below is this session's best-effort reasoning about which way + // the texture should appear to move as the handle is dragged - it could not be visually + // confirmed while writing it (no way to render/see pixels in this environment), so it may + // need a one-line sign flip once actually tested. + bool update_adjust_anchor(); // recomputes m_adjust_anchor_pos/normal; false if nothing painted + bool on_mouse_adjust_texture(const wxMouseEvent &mouse_event); + void render_adjust_texture_gizmo(); + // Draws a small '+'/'-' next to the mouse over the 3D view while painting/selecting, so it is + // obvious whether the next stroke adds paint (default) or erases it (Shift). Uses ImGui's + // foreground draw list, so it must be called from inside the gizmo's ImGui frame. + void render_paint_cursor_hint(); + // Mesh-local tangent-plane basis at m_adjust_anchor_normal, matching project_planar()'s + // dominant-axis convention so dragging on-canvas maps consistently onto offset. + void adjust_tangent_basis(Vec3f &u_axis, Vec3f &v_axis) const; + + // The plane a drag is measured against: the paint patch's anchor, lifted clear of the surface. + // Deliberately *fixed* - independent of the layer's offset - so that moving the handle cannot + // move the plane the handle's own motion is derived from, which would be a feedback loop. + Vec3f adjust_plane_point() const; + + // Where the handle is actually drawn, in mesh-local coordinates. This is NOT just the patch's + // centroid: the handle *represents the texture's placement*, so it has to travel as `offset` + // changes. Pinning it to the centroid is why dragging it looked broken - the texture slid but + // the handle stayed put. Undoing apply_uv_transform()'s scale and rotation turns the layer's + // offset back into a displacement in mm within the patch's tangent plane, which is what gets + // added to the anchor here. That is exactly consistent with the drag arithmetic in + // on_mouse_adjust_texture(): the handle then tracks the cursor 1:1, and sits back on the anchor + // precisely when offset is zero. + Vec3f adjust_handle_center(const TextureDisplacementLayer &layer) const; + + // The layer painted by the active slot, or nullptr if that slot has no layer yet. + TextureDisplacementLayer *active_layer(); + const TextureDisplacementLayer *active_layer() const; + + // Recomputes m_preview_glmodel from the volume's current (unbaked) paint state, using the same + // build_texture_displacement() algorithm as Bake. Called whenever the paint mask changes + // (stroke end, layer switch, undo/redo reload, post-bake refresh) rather than every frame - + // this is real mesh work (PNG sampling, vertex welding), not something to redo per paint stroke + // drag sample or idle repaint. With several painted layers this can be slow, so the actual + // computation runs in a background TextureDisplacementPreviewJob; this function only queues + // it and returns immediately, and m_preview_glmodel is updated later when it completes. + void rebuild_preview(); + void render_preview_mesh(); + + // Alternate, GPU-only preview: perturbs shading normals from the active layer's height texture + // instead of actually moving vertices, using the resources/shaders/*/texture_displacement_shaded.* + // shader. Faster than the true-displacement preview (no CPU meshing at all - just a per-vertex + // paint-weight buffer built at the same cadence as rebuild_preview()) but only shows the *active* + // layer, and the relief it shows is a shading illusion, not real geometry - "Bake" always produces + // the true, exact result either way. + void rebuild_shaded_preview_mesh(); + void render_shaded_preview_mesh(); + + // Feeds the active layer's painted patch + LSCM unwrap (if it's using that projection method) + // into Plater's docked UV-editor pane and shows it, or hides the pane if the active layer + // isn't using LSCM (or nothing is painted). Called whenever something that could change what + // the pane should show happens: paint changes, layer switch, projection method change, bake, + // and on shutdown (to hide it). + void update_uv_editor(); + + // Applies one island edit reported by the UV editor's drag/rotate gestures to the active layer. + // Deltas are incremental (see UVEditorCanvas::IslandEditFn); `finished` ends the gesture, which + // is when - and only when - the 3D preview is rebuilt, since doing that per mouse-move would + // queue a mesh recompute for every pixel of a drag. + void on_island_edited(int island, const Vec2f &offset_delta, float rotation_delta, float scale_factor, bool finished); + // Applies a committed vertex/edge edit from the UV editor's Vertex/Edge modes: each entry is an + // unwrapped-vertex index and its new raw-unwrap coordinate. Maps the unwrapped index to a mesh + // vertex and stores a per-vertex UV override on the layer (see lscm_uv_overrides), then rebuilds the + // preview so the baked geometry follows. + void on_uv_vertex_edited(const std::vector> &edits); + // UV-editor sub-element select mode, mirrored into the canvas: 0 = Island, 1 = Vertex, 2 = Edge. + int m_uv_select_mode = 0; + // One affine per island (columns: x basis, y basis, translation), mapping the unwrap's raw mm + // coordinates to texture UVs - the same type as UVEditorCanvas::IslandTransform, spelled out + // here so this header needn't drag in wxGLCanvas/glad. Cheap to recompute (it is per *island*, + // not per vertex), which is what lets an island drag update the pane without re-uploading a + // single vertex. + std::vector> uv_editor_island_transforms(const TextureDisplacementLayer &layer); + // Handles a toolbar command forwarded from the UV pane that needs the layer data the canvas + // doesn't hold (average island scale, cut island). Takes the command as an int (a cast of + // UVEditorCanvas::Command) so this header needn't pull in glad/wxGLCanvas via the canvas header. + void on_uv_command(int cmd, float value); + // Pane commands, queued by on_uv_command() and run from the panel render: pane clicks arrive in wx event + // handlers, outside the 3D canvas's GL frame, and several of these rebuild GPU meshes. + std::vector> m_uv_command_queue; + void process_uv_commands(); + void run_uv_command(int cmd, float value); + // Sends the UV editor pane what its controls show (see UVEditorCanvas::PaneState). + void push_uv_pane_state(); + // The panel's view modes: 0 Normal, 1 Fast, 2 Checker, 3 Distortion. Shared by the View row and the pane's + // background buttons, so both switch views the same way. + void apply_view_mode(int mode); + // The pane header's thumbnail of the active layer's texture, rebuilt only when its image changes. + static constexpr int UV_THUMB_PX = 64; + const void *m_uv_thumb_source = nullptr; + std::vector m_uv_thumb_rgb; + // Splits one unwrap chart in two by marking the mesh edges that straddle the plane through its + // 3D centroid, perpendicular to its longest axis, as seams (#17). The re-unwrap then separates it. + void cut_island(TextureDisplacementLayer &layer, int chart); + + // Captures the current camera's right/up axes into the layer's projector (#6), transformed into + // the volume's local space so the projection is stable as the object is later moved/rotated. + void capture_view_projection(TextureDisplacementLayer &layer); + + // Manual seam marking (#9): a mode where clicking the model toggles the nearest mesh edge in the + // active layer's lscm_seam_edges, so the unwrap can be cut exactly where the user wants - the + // Blender "mark seam" workflow. Painting is suppressed while it is on. + bool m_seam_edit_mode = false; + GLModel m_seam_glmodel; // the current seam edges, highlighted on the mesh + bool on_mouse_seam(const wxMouseEvent &mouse_event); + void toggle_seam_at(const Vec2d &mouse_pos); + void rebuild_seam_overlay(); + void render_seam_overlay(); + // The mesh edge nearest the mouse, in the volume's own vertex indices, or {-1,-1} if the ray misses. + // Factored out of toggle_seam_at() so the same pick can drive a live hover highlight (below) that + // shows which edge a click would toggle - the "I don't know how it works" feedback the user hit. + std::pair seam_edge_at(const Vec2d &mouse_pos) const; + std::pair m_seam_hover_edge{ -1, -1 }; + // The vertex a click would pick in shortest-path mode, so the target is visible on hover the same + // way the edge is in normal mode. -1 when nothing is under the cursor (or not in path mode). + int m_seam_hover_vertex = -1; + GLModel m_seam_hover_glmodel; + void rebuild_seam_hover_overlay(); + + // Shortest-path seam marking, for dense meshes where clicking every single triangle edge is + // tedious: in this sub-mode a click picks the nearest vertex, and the next click marks every edge + // on the shortest surface path between the two as a seam - so a whole seam line is drawn with two + // clicks. The end vertex becomes the next start, so a multi-segment seam chains click by click. + bool m_seam_path_mode = false; + int m_seam_path_anchor = -1; // mesh vertex the path starts from, or -1 + GLModel m_seam_anchor_glmodel; // the anchor's incident edges, highlighted + int seam_vertex_at(const Vec2d &mouse_pos) const; // nearest mesh vertex under the cursor + void mark_seam_path(int v_from, int v_to); // seam every edge on the shortest path + void rebuild_seam_anchor_overlay(); + // Set while an island gesture is in flight, so the undo snapshot is taken once at the start of + // the drag (capturing the state *before* it) rather than on every motion event. + bool m_island_drag_active = false; + + // Which of the up to TEXTURE_DISPLACEMENT_MAX_LAYERS paint masks the brush currently writes + // into. Always a valid slot index (0 by default) so the base class's per-volume selector + // machinery always has something to work with, even before any texture has been added - + // painting into a slot with no texture assigned is harmless, it just has no visible/bake + // effect until a texture is added to that slot. + int m_active_layer_slot = 0; + // Set when m_triangle_selectors could not be loaded from the stored paint masks, because a mask + // was recorded against a different topology: TriangleSelector::deserialize() rejects that and + // returns without a word, leaving the selector empty even though the mask is not. While this is + // set, an *empty* selector says nothing about the paint, so update_model_object() must not flush + // one back - serializing it over the mask destroys the user's paint for good, and the bake then + // reports "nothing is painted" about the data the flush had just deleted. + // + // Deliberately not a blanket refusal to flush: once the user paints, the selector holds real + // content again and writing it back is exactly right - it replaces the unusable mask with one + // recorded against the current mesh. So only the empty-over-non-empty case is held back. + bool m_selectors_stale = false; + bool m_bake_in_progress = false; + + // When set, the true-displacement geometry is rebuilt on every parameter change (live), instead of + // only once the slider being dragged is released. On by default so painting/added textures show + // straight away without needing to nudge a slider first. + bool m_auto_update = true; + + // Subdivision is now count-based (split the whole mesh 1..5 times) rather than a target edge + // length, and is previewed as a wireframe before it is committed: nothing is written to the model + // until "Apply". While previewing, the would-be subdivided mesh is drawn as a wireframe overlay so + // the added density is visible; "Done" ends the preview without touching the model. The normal + // "Show mesh wireframe" toggle is left alone, so a wireframe the user already had on stays on. + // 0 is a real value meaning "no subdivision": it previews nothing and Apply is a no-op. Apply + // snaps the slider back to it, because each pass quadruples the triangle count - leaving the + // count where it was would immediately re-preview N more passes on top of the mesh that was just + // committed, i.e. the most expensive thing the panel can do, on every Apply. + int m_subdivide_count = 1; + bool m_subdivide_editing = false; + int m_subdivide_preview_tris = -1; // triangle count of the previewed result, shown in the panel + GLModel m_subdivide_preview_glmodel; + void rebuild_subdivide_preview(); + void render_subdivide_preview(); + + // Adaptive subdivision: refine only the painted area, down to a target edge length, via + // conformal longest-edge bisection (subdivide_mesh_adaptive()). Unlike the count-based uniform + // path it does not touch the unpainted rest of the model, and - because it is driven by the paint + // - it can carry that paint forward across the topology change (children of a painted triangle + // are painted), so the region survives the subdivision instead of being dropped. + bool m_subdivide_adaptive = false; + float m_subdivide_target_mm = 0.f; // 0 = not yet seeded; filled from the mesh on first show + // Feature-adaptive sub-mode: put the triangles where the *displaced surface* bends (texture + // curvature) rather than spreading them evenly. `detail_mm` is the chord-error tolerance ("Detail" + // slider: how far the true surface may sit off the flat triangle before it is split); the target + // above stays in play as a coarse baseline ("Max edge"), and `min_edge_mm` is the hard floor + // ("Min edge"). See subdivide_mesh_adaptive(). + bool m_subdivide_feature = false; + float m_subdivide_detail_mm = 0.05f; + float m_subdivide_min_edge_mm = 0.1f; + // Edge length the band straddling the paint's boundary is refined to (0 = leave it alone). Applies + // in both adaptive sub-modes, because it is not a texture-detail criterion: the bake steps the + // surface from full displacement to zero across that boundary whatever the texture is doing, and + // the chord-error test cannot see that step at all - its sampler has no per-point paint test, so + // just outside the paint it goes on reporting the same smooth height field. Without this the + // transition keeps the input's density and the rim of an unpainted island comes out as a ring of + // large, steeply tilted triangles. See collect_paint_region() and subdivide_mesh_adaptive(). + float m_subdivide_border_mm = 0.4f; + // Edge length triangles straddling a *colour* boundary are refined to (0 = ignore colour). Its own + // control rather than a share of "Detail (mm)" because the two measure different things: Detail is + // a chord error in mm of surface deviation, this is a triangle size in mm along a step the chord + // test cannot see at all - the height field is perfectly smooth across a change of filament, so + // without this a colour boundary lands on whatever triangles the relief happened to need, which on + // a flat surface is none. Only ever costs anything where a boundary actually runs. + float m_subdivide_color_mm = 0.3f; + // How many thousand triangles refinement may *add* (the mesh's own count is added on before it is + // passed as subdivide_mesh_adaptive()'s absolute cap, so the control still means something on a + // dense model). Refinement is worst-error-first, so hitting the budget still yields the best mesh + // that many triangles can buy - and it is what keeps a fine "Detail" over a noisy texture from + // turning into an out-of-memory, or an unrenderable preview wireframe. + // + // The default used to be 1500 (i.e. +1.5 M triangles), which is what made Standard mode's Bake + // take minutes: every stage after the subdivision - the displacement itself, the convex hull, the + // GLModel upload, and the re-slice changed_object() triggers - then runs on a mesh two orders of + // magnitude denser than the input. 750k is still far finer than any FDM nozzle resolves at the + // 0.02 mm detail tolerance Standard uses, and the slider goes to 2000 for anyone who wants more. + int m_subdivide_budget_k = 750; + void subdivide_model_adaptive(); + // Fills `region` (per current-mesh triangle, a REFINE_* bitmask) from the union of every layer's + // painted area plus the band straddling its edge. If `paint` is non-null, also fills the per-layer + // coverage map the subdivision carries forward - the expensive half, skipped by the live preview, + // which only needs the region. Returns false when nothing is painted at all. + static bool collect_paint_region(const TriangleMesh &mesh, const TextureDisplacementFacetsData &facets, + std::vector ®ion, + std::array *paint); + + // Runs the volume's TextureDisplacementOptions smoothing over the *already committed* geometry, + // restricted to the painted area. The same settings are folded into Preview/Bake automatically; + // this is the escape hatch for relief that has already been baked in, where there is no + // displacement pass left to attach them to. Topology-preserving, so unlike subdivide and remesh it + // keeps every paint channel - including texture displacement - exactly as it was. + void smooth_model(); + + // Isotropic remeshing (CGAL) to even out wildly varying triangle sizes so displacement has a + // consistent density to work with. Target edge length in mm; 0 means "not yet initialised", filled + // with the mesh's mean edge length the first time the control is shown. Like subdivide, it replaces + // the geometry, but unlike subdivide it keeps every paint channel: prepare_mesh() carries the + // texture-displacement masks across spatially, which is also what lets Standard mode remesh *after* + // the user has painted. + float m_remesh_target_edge_mm = 0.f; + // Dihedral angle above which an edge counts as a hard feature and is held fixed by the remesher. + // Off by default would round every sharp edge off, so this is on; 0 disables the protection. + float m_remesh_sharp_angle_deg = 40.f; + bool m_remesh_keep_sharp_edges = true; + void remesh_model(); + + // ---- Bake stage debug view ---- + // + // A bake is a chain of stages that each rewrite the whole mesh, so when the result looks wrong the + // only useful question is which stage made it wrong. "Capture stages" runs the same recipe Bake + // runs, keeps every intermediate mesh, and draws the selected one through the ordinary + // true-displacement preview - so this needs no rendering code of its own. + // + // The run commits nothing: TextureDisplacementDebugJob never touches the Model. + std::vector m_debug_stages; + // Which stage is on screen. -1 means the debug view is off and the live preview owns + // m_preview_glmodel again; rebuild_preview() checks this before replacing it. + int m_debug_stage = -1; + bool m_debug_in_progress = false; + // The open / non-manifold counts come from a sort over every half-edge, which on a multi-million + // triangle stage costs more than the stage did. On by default because a stage that tore the mesh + // is exactly what this exists to find. + bool m_debug_check_topology = true; + + // World-millimetre area of everything painted on `mv`, cached on the mesh and the paint generation: + // it is what the refinement has to cover, so it is what says whether a budget can pay for a + // resolution. 0 when nothing is painted. + double painted_area_mm2(const ModelVolume &mv); + double m_painted_area_mm2 = 0.; + std::string m_painted_area_key; + // Triangles the refinement needs to reach `edge_mm` over that area. Bisection converges to four of + // them to a square of the target edge - measured within 3% on a test model over a 5x range of + // resolutions - but the real count depends on the shape of the triangles it starts from, so this + // is only ever used to say a budget is clearly too small, never as a promise of what will come out. + size_t estimated_refined_triangles(const ModelVolume &mv, float edge_mm); + + // The default pipeline's automatic resolution/budget for the volume, cached on what it depends on. + const V2Resolution &v2_recommendation(const ModelVolume &mv); + V2Resolution m_v2_rec; + std::string m_v2_rec_key; + + // Queues the capture run. Uses Standard mode's recipe (remesh, refine, displace) unless Pro mode + // has already prepared the mesh or the experimental pipeline is on, either of which has nothing + // to prepare. + void run_stage_debug(); + // Puts stage `index` on screen. A stage over the memory cap has counts but no geometry and is + // skipped. + void show_debug_stage(int index); + // Drops the captured stages and hands m_preview_glmodel back to the live preview. + void exit_debug_view(); + // The panel: capture button, stage list, and what each stage produced. + void render_debug_stage_panel(ModelVolume *mv); + + // Live, pre-bake preview of the true displaced geometry (built by the same algorithm Bake + // uses). Empty/uninitialized whenever nothing is painted yet, in which case the gizmo falls + // back to the standard paint-mask overlay like every other painting gizmo. + GLModel m_preview_glmodel; + // Set while a layer parameter slider has changed since the last rebuild_preview() call but the + // mouse button driving the drag hasn't been released yet - see on_render_input_window(). + bool m_preview_params_dirty = false; + + // See rebuild_shaded_preview_mesh()/render_shaded_preview_mesh(). Off by default: the Normal + // (true-displacement) view shows the real geometry Bake will produce, across every layer, so it is + // the honest first impression. The cheap, instant-updating Fast view is one click away in the View + // row for painting on a heavy model. + bool m_use_shaded_preview = false; + // Set from the UV editor's per-move island edits instead of rebuilding the (potentially large) shaded + // mesh synchronously inside that mouse handler - doing the rebuild there stalled both the UV pane + // and the 3D view. The rebuild is instead coalesced to once per 3D frame (render_painter_gizmo). + bool m_shaded_preview_dirty = false; + GLModel m_shaded_preview_glmodel; + + // Translucent tint over the active layer's painted triangles, drawn on top of whichever preview + // is showing. The base painter's own opaque paint highlight (render_triangles()) cannot be used + // in either preview mode - it is coincident with the surface and simply covers it - so the only + // paint feedback the gizmo had was the relief itself, which meant erasing showed nothing at all + // until the stroke ended and the whole preview rebuilt. This is that feedback: cheap (the painted + // patch only), translucent (the preview stays visible through it) and rebuilt live during a + // stroke. + GLModel m_paint_overlay_glmodel; + // The islands selected in the UV editor, tinted on the model so the pane's selection can be seen + // in place. Rebuilt whenever the pane's selection differs from the one it was built for. + GLModel m_island_overlay_glmodel; + std::vector m_island_overlay_selection; + void rebuild_island_overlay(const std::vector &selection); + void render_island_overlay(); + // Set on every paint event, cleared when the overlay is rebuilt in render_painter_gizmo(). Kept + // separate from m_shaded_preview_dirty so a stroke refreshes only the small painted patch per frame, + bool m_paint_overlay_dirty = false; + void rebuild_paint_overlay(); + void render_paint_overlay(GLModel &overlay); + // Every *other* layer's paint, muted, so all layers stay visible while one is edited. Rebuilt only when that + // paint, the active layer or the preview it is lifted onto changes (m_other_paint_key). + GLModel m_other_paint_glmodel; + std::string m_other_paint_key; + void rebuild_other_paint_overlay(); + // Whether render_shaded_preview_mesh() would actually draw something. Checked before the real volume + // is hidden: with no layer, no texture or no shader the shaded path draws nothing, and hiding the + // volume for it left the model invisible. + bool shaded_preview_ready() const; + // Whether the current displacement mesh carries a precomputed per-vertex uv (LSCM) that the shader + // should sample at directly, rather than projecting in-shader. Set by rebuild_shaded_preview_mesh(). + bool m_shaded_preview_uses_vertex_uv = false; + // The projection frame handed to the preview shader, captured when the mesh is built. Cylindrical and + // Spherical are reconstructed in the fragment shader (there is no per-vertex uv for them) and wrap + // around the whole patch, which no fragment can work out for itself. See layer_projection_frame(). + int m_shaded_projection_mode = 0; + Vec3f m_shaded_patch_center = Vec3f::Zero(); + Vec3f m_shaded_patch_axis = Vec3f::UnitZ(); + // The palette the fast preview's per-triangle filament indices were built against, captured when + // the mesh was. Empty when the active layer is not colouring, which is what tells the shader to + // fall back to the model's own colour. Held rather than re-read at draw time so the indices baked + // into the mesh can never be resolved against a different set of filaments than they were computed + // from - loading a filament mid-session would otherwise recolour a stale preview at random. + std::vector m_shaded_preview_palette; + + // GPU island drag: while an island is dragged in the UV editor, the displacement mesh is baked once (with + // the dragged island's vertices flagged, v_normal.y = 1) and then moved purely through the shader's + // island_delta uniform - one uniform update per mouse move, no rebuild - so it tracks the cursor + // as smoothly as Adjust placement. m_shaded_active_chart is the dragged island (or -1); + // m_shaded_active_face flags the dragged islands' *triangles*, indexed by painted-patch face; + // m_shaded_baked_active_xf is that island's placement baked into the current mesh, against which the + // live delta is measured; m_shaded_island_delta is the resulting final-uv-space affine handed to the + // shader (identity except mid-drag). + // + // Per triangle rather than per vertex deliberately: a seam vertex belongs to every chart touching + // it, so flagging the dragged chart's base vertices also flagged the corners its neighbours use. + // island_active is an interpolated varying, so those neighbouring triangles then had island_delta + // applied too - dragging one island moved every adjacent island's texture while the editor, which + // is per chart, correctly moved only the one. A triangle belongs to exactly one chart. + int m_shaded_active_chart = -1; + std::vector m_shaded_active_face; + Eigen::Matrix m_shaded_baked_active_xf = Eigen::Matrix::Identity(); + Eigen::Matrix m_shaded_island_delta = Eigen::Matrix::Identity(); + // Flags `charts`' triangles in m_shaded_active_face, sized to `patch_face_count` (the painted patch + // the displacement mesh is being built from). Cleared if the unwrap carries no face map. + void compute_shaded_active_faces(const std::vector &charts, size_t patch_face_count); + + // The set of islands the current UV-editor drag moves together: the pane's multi-selection unioned + // with each selected island's join group (see build_island_move_set()). Populated at drag start and + // cleared when it finishes. A move applies the same offset to every island in it; rotate/scale act + // only on the primary. Empty when no move drag is in flight. + std::vector m_island_move_set; + // All islands that must move with `primary`: the pane's multi-selection plus, for each of those, the + // charts sharing its join group in `layer`. Always contains `primary`. + std::vector build_island_move_set(const TextureDisplacementLayer &layer, int primary) const; + // The join-group id of chart `c`: its explicit entry in `groups`, or `c` itself (its own singleton) + // when unset. Two charts move together iff this matches. + static int island_group_of(const std::vector &groups, int c); + // Merges chart `b`'s join group into chart `a`'s (materialising `groups` to `chart_count` first). + static void join_island_groups(std::vector &groups, int a, int b, int chart_count); + // Final per-vertex texture uv for the projections the shader can't reconstruct itself - LSCM (an + // unwrap) and ViewProjected (a projector plane the shader doesn't know). One entry per patch/base + // vertex, already through apply_uv_transform(). Empty for Triplanar/Cylindrical/Spherical, which + // the shader projects on its own. Shared by the shaded preview and the UV-check overlay. + std::vector compute_layer_vertex_uvs(const indexed_triangle_set &patch, + const TextureDisplacementLayer &layer) const; + // The same, but three UVs per patch triangle (corner 0..2 of triangle i at 3i..3i+2). This is what + // the flat, unshared-vertex preview meshes actually want: under LSCM a seam vertex has a different + // UV in each island it borders, so collapsing to one per vertex handed a triangle at an unjoined + // seam its neighbour's placement - one visibly skewed triangle per face. Every other projection is + // single-valued per point, so there a corner's UV is just its vertex's. + std::vector compute_layer_corner_uvs(const indexed_triangle_set &patch, + const TextureDisplacementLayer &layer) const; + // The in-shader projection for `layer` (0 Triplanar, 1 Cylindrical, 2 Spherical) plus, for the two + // wrapping ones, the patch centroid and cylinder axis they wrap around - in the texture frame the + // shaders project in. Taken from the bake's own texture_displacement_patch_frame(), so a preview + // can never wrap around a different centre, or pick a different axis, than the bake will. + int layer_projection_frame(const indexed_triangle_set &local_patch, const TextureDisplacementLayer &layer, + Vec3f ¢er, Vec3f &axis) const; + // `patch` with its vertices moved into world millimetres - the space the bake maps the texture in + // (see build_texture_displacement()). Returned by value because the caller usually still needs the + // original: the patch doubles as render geometry, which is drawn through the volume's own matrix. + indexed_triangle_set patch_in_world(const indexed_triangle_set &patch) const; + + // UV-check overlay drawn over the painted patch to sanity-check the unwrap (#13/#14). Built by + // rebuild_uvcheck_mesh(), drawn by render_uvcheck_mesh() with the "texture_displacement_uvcheck" + // shader. Checker works for any projection; Distortion needs the per-vertex LSCM uv. + enum class UVCheckMode { None, Checker, Distortion }; + UVCheckMode m_uv_check_mode = UVCheckMode::None; + GLModel m_uvcheck_glmodel; + bool m_uvcheck_uses_vertex_uv = false; + // As m_shaded_projection_mode and friends, for the Checker overlay. + int m_uvcheck_projection_mode = 0; + Vec3f m_uvcheck_patch_center = Vec3f::Zero(); + Vec3f m_uvcheck_patch_axis = Vec3f::UnitZ(); + void rebuild_uvcheck_mesh(); + void render_uvcheck_mesh(); + + // The UV editor pane is opened only on the user's explicit request (this toggle in the panel), + // never automatically just because a patch exists - auto-popping it whenever there was "a + // selection to process" is exactly what the user asked to stop. update_uv_editor() keeps the pane + // hidden unless this is set. Reset on gizmo shutdown so reopening the gizmo doesn't reopen the pane. + bool m_show_uv_editor = false; + // The unwrap is expensive, so it is recomputed only when the user explicitly asks for it (the + // "Unwrap" button), not on every paint stroke or slider nudge. This is set by that button and + // consumed by the next update_uv_editor() call, which is the only path that re-solves the unwrap; + // every other call merely refreshes the cheap per-island affine transforms over the existing one. + bool m_uv_unwrap_pending = false; + // Set alongside m_uv_unwrap_pending only by the Unwrap button, so the connected-net auto-layout runs + // on a genuine re-unwrap but not on a refresh re-solve (a vertex-edit commit or undo), which must + // leave island placements untouched. + bool m_uv_apply_connected_net = false; + // Signature of the per-vertex UV overrides last reflected in the pane. When it changes without the + // user pressing Unwrap - a vertex/edge edit committing, or an undo/redo reverting one - the pane + // is re-solved so its geometry follows, even though a plain edit otherwise never re-solves (#Feat2). + size_t m_uv_overrides_sig = 0; + // What the UV pane's background currently holds, so update_uv_editor() only re-uploads it when the + // choice actually changes (the height texture is large; re-sending it every stroke would be waste). + enum class UVBackground { None, Height, Checker }; + UVBackground m_uv_editor_bg = UVBackground::None; + float m_uv_editor_bg_smoothing = -1.f; // smoothing the height backdrop was uploaded at + const void *m_uv_editor_bg_image = nullptr; // the layer image it was uploaded from + // Per-chart distortion heatmap colour for the UV pane (#7/#14), computed once when the unwrap is + // re-solved (relative stretch doesn't change when islands are merely moved), fed to the canvas only + // while the Distortion check mode is on. Empty otherwise. + std::vector m_uv_editor_distortion_colors; + void compute_uv_editor_distortion_colors(const indexed_triangle_set &patch); + + // Plain triangle-edge overlay on the mesh (#8), toggled independently of the check modes. + bool m_wireframe_overlay = false; + GLModel m_wireframe_overlay_glmodel; + size_t m_wireframe_overlay_vcount = 0; // topology signature, so it rebuilds only on a real change + void rebuild_wireframe_overlay(); // from the base mesh (shaded/paint mode) + void build_wireframe_from_its(const indexed_triangle_set &its); // from an explicit mesh, no early-out + void refresh_wireframe(); // pick base vs displaced source for the current view + void render_wireframe_overlay(); + // The displaced preview geometry the last preview job produced, kept so the wireframe overlay can be + // drawn on the raised surface actually shown in the true-displacement view (#: "wireframe in real mode"). + indexed_triangle_set m_preview_its; + // Raised on every rebuild_preview() call; a background TextureDisplacementPreviewJob's result + // is only applied if this hasn't moved on since the job was queued (see rebuild_preview()), + // so a burst of edits can't have an earlier, now-stale job clobber a later one's result. + // + // Shared with the worker thread (hence the atomic) so a running job can notice mid-computation + // that it has been superseded and abort, instead of running to completion for a result that will + // only be discarded on arrival. + std::shared_ptr> m_preview_generation = std::make_shared>(0); + // At most one preview job is ever queued. The UI job worker is a single FIFO queue shared with + // Bake (and with arrange/orient/send), and rebuild_preview() is called on every stroke end, every + // slider release and - with "Auto update" on - every frame of a slider drag. Queuing one full + // displacement per call built a backlog that took minutes to drain: the preview appeared frozen, + // and a Bake pressed afterwards sat behind the whole queue. So a request made while a job is in + // flight is recorded here and issued once that job settles, collapsing any number of edits into a + // single follow-up run. + bool m_preview_job_running = false; + bool m_preview_job_pending = false; + void queue_preview_job(); + + // Per-slot GPU thumbnail cache for the layer list panel, keyed by the image_data pointer that + // was current the last time each thumbnail was built (see get_layer_thumbnail()). + std::array, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnails; + std::array m_thumbnail_source{}; + // The smoothing each cached thumbnail was built at, so a smoothing change re-uploads it. + std::array m_thumbnail_smoothing{}; + + // Full-resolution height texture for the preview shader, keyed the same way (see + // get_layer_height_texture()). A smoothing change re-uploads it, so the fast preview shows the + // blur the bake will apply. + std::unique_ptr m_height_tex; + const void *m_height_tex_source = nullptr; + float m_height_tex_smoothing = -1.f; + + // The same, for the layer's *colour*, which the fast preview quantizes per fragment so it shows + // the image at texel resolution rather than at the mesh's. Null for a grayscale texture. + std::unique_ptr m_color_tex; + const void *m_color_tex_source = nullptr; + float m_color_tex_smoothing = -1.f; + + // Library textures the picker has shown at least once, keyed by file path (see LibraryTexture). + std::map m_library_textures; + + // Everything the *unwrap* depends on. update_uv_editor() runs from rebuild_preview(), i.e. on + // every stroke end and every slider release - but depth/tiling/rotation/offset/blend change + // none of this, so re-extracting the patch and re-solving on those edits would be pure waste. + // Held as the real values rather than a hash: TriangleSplittingData has an exact operator==, so + // there is no reason to accept a hash's (however unlikely) chance of showing a stale unwrap. + struct UVEditorState + { + int slot = -1; + const void *image_data = nullptr; + float seam_angle = -1.f; + float padding = -2.f; + TriangleSelector::TriangleSplittingData facets; + // Manual/auto seam edges also change the unwrap, so a change here must force a re-solve just + // like the facets do (marking a seam leaves the paint mask untouched). + std::vector> seam_edges; + + bool operator==(const UVEditorState &other) const + { + return slot == other.slot && image_data == other.image_data && seam_angle == other.seam_angle && + padding == other.padding && facets == other.facets && seam_edges == other.seam_edges; + } + }; + UVEditorState m_uv_editor_state; + // Bounds of the UVs last handed to the pane, purely so the panel can show where the unwrap + // actually landed - it is packed in mm and then divided by the tile size, so it is easy for it + // to end up far outside the texture's first tile without any of that being visible. + Vec2f m_uv_editor_bbox_min = Vec2f::Zero(); + Vec2f m_uv_editor_bbox_max = Vec2f::Zero(); + // The unwrap m_uv_editor_state produced, kept so that changing tiling/rotation/offset only costs + // re-running apply_uv_transform() over it, not another extraction and solve. + PatchUnwrap m_uv_editor_unwrap; + + // When set, the panel is a free-floating window the user can drag anywhere (with a title bar to + // grab), instead of being pinned to the right of the gizmo toolbar. Persisted across gizmo + // open/close within a session, so the choice sticks while working. + bool m_undocked = false; + + // Smooth scrolling for the panel body (everything between the header and the pinned Bake footer). + // ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard + // jolt rather than a scroll. The wheel is intercepted (ImGuiWindowFlags_NoScrollWithMouse) and + // moves a *target* offset instead; the real scroll is eased toward it over the following frames. + float m_panel_scroll_target = 0.f; + float m_panel_scroll_applied = -1.f; // what the easing wrote last frame; <0 until the first one + // Last frame's body content height and footer height. The body is a child window that has to be + // given its height before its content is laid out, so it is sized from the previous frame: as tall + // as its content, capped so the footer still fits above the bottom of the canvas. + float m_panel_body_h = 0.f; + float m_panel_footer_h = 0.f; + + // See the "Adjust Texture" block of private methods above. + bool m_adjust_texture_mode = false; + bool m_adjust_anchor_valid = false; + Vec3f m_adjust_anchor_pos = Vec3f::Zero(); // mesh-local + Vec3f m_adjust_anchor_normal = Vec3f::UnitZ(); // mesh-local + + // Pan: free drag anywhere on the flat panel, moves offset along both axes. AxisU/AxisV: drag + // the corresponding arrow, moves offset along only that one axis. + enum class AdjustHandle { None, Pan, AxisU, AxisV }; + AdjustHandle m_adjust_drag_handle = AdjustHandle::None; + Vec2f m_adjust_drag_start_offset = Vec2f::Zero(); + // Anchor-relative planar position (see project_planar()) of the point under the mouse at the + // moment the current drag started; every subsequent frame's delta is measured against this, + // rather than accumulated frame-to-frame, to avoid drift. + Vec2f m_adjust_drag_start_planar = Vec2f::Zero(); + + // Lazily-built unit quad (the pan panel) and unit line-with-arrowhead (reused, rotated, for + // both the U and V axis arrows), transformed into place at render time. + GLModel m_adjust_panel_glmodel; + GLModel m_adjust_arrow_glmodel; + + std::map m_desc; + + // Icons for the panel's icon buttons (tools, views, mapping, tiling, layer actions). Loaded through IconManager with + // the same colour/monochrome variants the main toolbar uses, so an inactive button shows the icon in + // the theme's normal (grey) foreground colour and an active one shows it in its original colours - + // matching the toolbar's selected/unselected look. Uploaded once on first panel render. + IconManager m_panel_icons; + std::map m_panel_icon_map; // file name -> [normal, colour, disabled] + bool m_panel_icons_tried = false; + void ensure_panel_icons(); +}; + +} // namespace Slic3r::GUI + +#endif // slic3r_GLGizmoTextureDisplacement_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index dc51518acc..40c272fcb7 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -23,6 +23,7 @@ //#include "slic3r/GUI/Gizmos/GLGizmoHollow.hpp" #include "slic3r/GUI/Gizmos/GLGizmoSeam.hpp" #include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp" #include "slic3r/GUI/Gizmos/GLGizmoSimplify.hpp" #include "slic3r/GUI/Gizmos/GLGizmoEmboss.hpp" #include "slic3r/GUI/Gizmos/GLGizmoSVG.hpp" @@ -171,6 +172,13 @@ void GLGizmosManager::switch_gizmos_icon_filename() case(EType::FuzzySkin): gizmo->set_icon_filename(m_is_dark ? "toolbar_fuzzy_skin_paint_dark.svg" : "toolbar_fuzzy_skin_paint.svg"); break; + case(EType::TextureDisplacement): + // One shared icon in both themes (no dedicated dark variant yet) - but it must still be + // *this* gizmo's icon. Handing it the fuzzy-skin one here quietly replaced the icon set at + // construction, so the toolbar ended up showing two identical fuzzy-skin buttons after any + // light/dark switch. + gizmo->set_icon_filename("toolbar_texture_displacement.svg"); + break; case(EType::MeshBoolean): gizmo->set_icon_filename(m_is_dark ? "toolbar_meshboolean_dark.svg" : "toolbar_meshboolean.svg"); break; @@ -228,6 +236,8 @@ bool GLGizmosManager::init() m_gizmos.emplace_back(new GLGizmoSeam(m_parent, m_is_dark ? "toolbar_seam_dark.svg" : "toolbar_seam.svg", EType::Seam)); m_gizmos.emplace_back(new GLGizmoFuzzySkin(m_parent, m_is_dark ? "toolbar_fuzzy_skin_paint_dark.svg" : "toolbar_fuzzy_skin_paint.svg", EType::FuzzySkin)); m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, m_is_dark ? "mmu_segmentation_dark.svg" : "mmu_segmentation.svg", EType::MmSegmentation)); + // One shared icon (no dedicated dark variant yet); it recolours acceptably in both themes. + m_gizmos.emplace_back(new GLGizmoTextureDisplacement(m_parent, "toolbar_texture_displacement.svg", EType::TextureDisplacement)); m_gizmos.emplace_back(new GLGizmoEmboss(m_parent, m_is_dark ? "toolbar_text_dark.svg" : "toolbar_text.svg", EType::Emboss)); m_gizmos.emplace_back(new GLGizmoSVG(m_parent)); m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, m_is_dark ? "toolbar_measure_dark.svg" : "toolbar_measure.svg", EType::Measure)); @@ -550,6 +560,8 @@ bool GLGizmosManager::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_p return dynamic_cast(m_gizmos[Cut].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == FuzzySkin) return dynamic_cast(m_gizmos[FuzzySkin].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); + else if (m_current == TextureDisplacement) + return dynamic_cast(m_gizmos[TextureDisplacement].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == MeshBoolean) return dynamic_cast(m_gizmos[MeshBoolean].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == BrimEars) @@ -563,6 +575,7 @@ bool GLGizmosManager::is_paint_gizmo() return m_current == EType::FdmSupports || m_current == EType::MmSegmentation || m_current == EType::FuzzySkin || + m_current == EType::TextureDisplacement || m_current == EType::Seam; } @@ -1531,6 +1544,8 @@ std::string get_name_from_gizmo_etype(GLGizmosManager::EType type) return "Color Painting"; case GLGizmosManager::EType::FuzzySkin: return "Fuzzy Skin Painting"; + case GLGizmosManager::EType::TextureDisplacement: + return "Texture Displacement"; default: return ""; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 2a4afac16d..2903b38777 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -84,6 +84,7 @@ public: Seam, FuzzySkin, MmSegmentation, + TextureDisplacement, Emboss, Svg, Measure, diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index 3ccbc6745a..fe5ddc9fa0 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -2631,7 +2631,11 @@ void ImGuiWrapper::push_toolbar_style(const float scale) ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(238 / 255.0f, 238 / 255.0f, 238 / 255.0f, 1.00f)); // 10 ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(238 / 255.0f, 238 / 255.0f, 238 / 255.0f, 0.00f)); // 11 ImGui::PushStyleColor(ImGuiCol_TextSelectedBg, COL_GREEN_LIGHT); // 12 - ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(1.00f, 1.00f, 1.00f, 1.00f));//13 + // The checkbox/radio frame behind this is drawn fully transparent (see FrameBg above, + // alpha 0), showing the light window background through it - a white check mark there is + // invisible. Dark mode doesn't have this problem (its window background is dark), so only + // this branch needs a check mark color with real contrast against a light background. + ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.f, 156 / 255.f, 136 / 255.f, 1.00f));//13 ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, ImVec4(0.42f, 0.42f, 0.42f, 1.00f)); ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(0.93f, 0.93f, 0.93f, 1.00f)); ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(0.93f, 0.93f, 0.93f, 1.00f)); diff --git a/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp b/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp new file mode 100644 index 0000000000..25a6eeb301 --- /dev/null +++ b/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp @@ -0,0 +1,212 @@ +#include "TextureDisplacementBakeJob.hpp" + +#include + +#include "libslic3r/Model.hpp" +#include "libslic3r/format.hpp" +#include "libslic3r/TriangleSelector.hpp" + +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/NotificationManager.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/Utils/UndoRedo.hpp" + +namespace Slic3r::GUI { + +TextureDisplacementBakeJob::TextureDisplacementBakeJob(TextureDisplacementBakeInput &&input, std::function on_finished) + : m_input(std::move(input)), m_on_finished(std::move(on_finished)) +{ +} + +void TextureDisplacementBakeJob::process(Ctl &ctl) +{ + const std::string status = _u8L("Baking texture displacement"); + ctl.update_status(1, status); + + // Only ever touches m_input (captured by value before this job was queued) and local state - + // never the live Model - so this is safe to run concurrently with the UI thread. + // + // The progress hook matters for more than cosmetics: the framework's progress notification only + // grows a close button once it reaches 100%, so a job that reports 0 and nothing else leaves an + // uncloseable notification pinned on screen. It also carries the Cancel button's effect into the + // bake, which on a subdivided mesh can run for several seconds. + // Colour, when any layer asks for it, is computed in the same pass as the displacement: both need + // the same per-layer projection and UV work, and doing it twice would double the expensive part. + TextureColorRequest color_request; + TextureColorRequest *color = nullptr; + if (!m_input.color.empty()) { + color_request.quantize = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette); + if (!m_input.color.palette_pure.empty()) + color_request.quantize_pure = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette_pure); + color_request.resolve = GLGizmoTextureDisplacement::make_mix_resolver( + m_input.color.palette, m_input.color.mix_mode, m_input.color.layer_height, + m_input.color.dither_cell_mm); + color_request.despeckle_passes = m_input.color.despeckle_passes; + color_request.out_triangle = &m_triangle_color; + if (color_request.quantize) + color = &color_request; + } + + int last_reported = 1; + m_result = TriangleMesh(build_texture_displacement( + m_input.base_mesh, m_input.layers, m_input.facets_data, m_input.options, + [&ctl, &status, &last_reported](int percent) { + if (ctl.was_canceled()) + return false; + // The notification repaints (and wakes the idle loop) on every call, so only push a + // message when the displayed integer percentage actually moves. + if (percent > last_reported) { + last_reported = percent; + ctl.update_status(percent, status); + } + return true; + }, + color, m_input.volume_to_world, nullptr, &m_stats)); + + // Always finish at 100: this is what closes the notification. Reported even on cancel, where + // build_texture_displacement() returns an empty mesh and finalize() commits nothing. + ctl.update_status(100, status); +} + +void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &eptr) +{ + struct OnExit + { + std::function fn; + ~OnExit() { if (fn) fn(); } + } on_exit{m_on_finished}; + + if (canceled || eptr || m_result.empty()) + return; + + // A bake that moved nothing - no layer could be sampled, or every sample was zero - must not be + // committed: committing is what clears the baked layers' paint, so the user would see the painted + // region simply vanish with no relief in its place and no idea why. Keep the paint and say so. + { + const indexed_triangle_set &out = m_result.its; + bool unchanged = out.indices.size() == m_input.base_mesh.indices.size() && + out.vertices.size() == m_input.base_mesh.vertices.size(); + for (size_t i = 0; unchanged && i < out.vertices.size(); ++i) + unchanged = (out.vertices[i] - m_input.base_mesh.vertices[i]).cwiseAbs().maxCoeff() < 1e-5f; + if (unchanged) { + show_error(nullptr, _u8L("The bake produced no displacement, so nothing was changed and the paint was kept. " + "Check that the painted layer has a texture and a non-zero depth.")); + return; + } + } + + Plater *plater = wxGetApp().plater(); + + const auto commit = [this, plater]() { + ModelVolume *volume = get_model_volume(m_input.volume_id, plater->model().objects); + if (volume == nullptr) + return; + + volume->set_mesh(std::move(m_result)); + volume->set_new_unique_id(); + volume->calculate_convex_hull(); + + // Colour lands in mmu_segmentation_facets, merged *over* whatever is already painted there + // rather than replacing it: a triangle the texture does not colour keeps its existing filament, + // and one the user never painted at all stays at NONE, which already means "the volume's own + // filament". That is what confines the effect to the painted area without having to invent a + // colour for everything outside it. Safe to index straight onto the new mesh - the bake is + // topology-preserving, so triangle i is still triangle i. + if (!m_triangle_color.empty() && m_triangle_color.size() == volume->mesh().its.indices.size()) { + TriangleSelector selector(volume->mesh()); + const TriangleSelector::TriangleSplittingData &existing = volume->mmu_segmentation_facets.get_data(); + if (!existing.bitstream.empty()) + selector.deserialize(existing, false); + for (size_t i = 0; i < m_triangle_color.size(); ++i) + if (m_triangle_color[i] > 0) + selector.set_facet(int(i), EnforcerBlockerType(m_triangle_color[i])); + volume->mmu_segmentation_facets.set(selector); + } + + // Clear the paint mask of every layer that was actually baked so a repeat bake (or the paint + // overlay) doesn't act on triangles that no longer represent the same unbaked surface. The + // texture layer definitions themselves are left untouched so the user can keep sculpting with + // the same textures. + // + // A mask the bake did *not* consume only still means what it did if the topology is unchanged, + // which is true of the classic path (it moves existing vertices) but not of the one-run + // pipeline, which rebuilds and then simplifies the mesh. A mask left behind against the old + // topology is exactly what makes the gizmo reload an empty selector over a non-empty mask and + // then erase it on the next flush - see GLGizmoTextureDisplacement::update_from_model_object(). + const bool topology_changed = m_input.base_mesh.indices.size() != volume->mesh().its.indices.size(); + for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) { + const auto it = std::find_if(m_input.layers.begin(), m_input.layers.end(), + [slot](const TextureDisplacementLayer &l) { return l.slot == slot; }); + if (topology_changed || (it != m_input.layers.end() && !it->empty())) + volume->texture_displacement_facet(slot).reset(); + } + + ModelObject *object = volume->get_object(); + if (object == nullptr) + return; + + if (ObjectList *obj_list = wxGetApp().obj_list()) { + const ModelObjectPtrs &objs = plater->model().objects; + auto it = std::find(objs.begin(), objs.end(), object); + if (it != objs.end()) + obj_list->update_info_items(size_t(it - objs.begin())); + } + + plater->changed_object(*object); + }; + + // Standard mode's Bake is remesh -> subdivide -> displace under a single snapshot, and this job runs + // long after that snapshot's scope has closed. Adding one here would put an undo step *between* the + // subdivision and the displacement: the first Undo would land on a mesh carrying every added + // triangle and no relief at all, and pressing Bake again from there would subdivide that mesh a + // second time. So the caller says who owns the undo step. + if (m_input.take_snapshot) { + Plater::TakeSnapshot snapshot(plater, _u8L("Bake texture displacement"), UndoRedo::SnapshotType::GizmoAction); + commit(); + } else { + commit(); + } + + // The refinement went finer than the budget could keep: the simplification had to take detail back + // out to fit, so what was baked carries less of the texture than the resolution asked for. Said + // here, with the numbers, because it is the only place that knows them - and not as an error + // dialog: the result is a usable mesh, just not the one the settings described. + if (m_stats.budget_limited) { + // Thousands under a million, so a small budget is not reported as "0.1 M". + const auto count = [](size_t n) { + return n >= 1000000 ? Slic3r::format("%1$.1f M", double(n) / 1000000.) : + Slic3r::format("%1% k", (n + 500) / 1000); + }; + wxGetApp().notification_manager()->push_notification( + NotificationType::CustomNotification, NotificationManager::NotificationLevel::WarningNotificationLevel, + Slic3r::format(_u8L("The triangle budget limited the detail: this resolution needs %1% triangles, " + "the budget kept %2%. Raise Budget or use a coarser Resolution for the full detail."), + count(m_stats.triangles_refined), count(m_stats.triangles_budget))); + } +} + +void queue_texture_displacement_bake(const ModelVolume &volume, const TextureColorSettings &color, + std::function on_finished, bool take_snapshot) +{ + TextureDisplacementBakeInput input; + input.color = color; + input.take_snapshot = take_snapshot; + input.volume_id = volume.id(); + input.base_mesh = volume.mesh().its; + input.layers = volume.texture_displacement_layers; + input.options = volume.texture_displacement_options; + input.volume_to_world = texture_displacement_volume_to_world(volume); + for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) + input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data(); + + + auto &worker = wxGetApp().plater()->get_ui_job_worker(); + queue_job(worker, std::make_unique(std::move(input), std::move(on_finished))); +} + +} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.hpp b/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.hpp new file mode 100644 index 0000000000..187df3d0e7 --- /dev/null +++ b/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.hpp @@ -0,0 +1,70 @@ +#ifndef slic3r_TextureDisplacementBakeJob_hpp_ +#define slic3r_TextureDisplacementBakeJob_hpp_ + +#include +#include + +#include "libslic3r/Color.hpp" +#include "libslic3r/ObjectID.hpp" +#include "libslic3r/TextureDisplacement.hpp" +#include "libslic3r/TriangleMesh.hpp" + +#include "Job.hpp" + +namespace Slic3r::GUI { + +// Everything process() needs, captured by value on the main thread when the job is queued so the +// worker thread never touches the live Model concurrently with the UI (mirrors how EmbossJob's +// DataBase is captured before process() runs). +struct TextureDisplacementBakeInput +{ + ObjectID volume_id; + indexed_triangle_set base_mesh; + std::vector layers; + TextureDisplacementFacetsData facets_data; + TextureDisplacementOptions options; + // Mesh coordinates -> world millimetres. Captured here with everything else so the worker never + // reaches back into the live Model for it. See build_texture_displacement(). + Transform3d volume_to_world = Transform3d::Identity(); + // Captured on the main thread. Empty unless some layer is colouring, in which case the bake also + // writes the volume's mmu_segmentation_facets - the same per-triangle filament assignment the MMU + // paint gizmo writes - alongside the displaced geometry. + TextureColorSettings color; + // Whether this job pushes its own undo step when it commits. False when the caller has already + // taken one that is meant to cover the displacement as well - Standard mode's Bake, which remeshes + // and subdivides first and has to undo as a single action. + bool take_snapshot = true; +}; + +// Bakes a volume's painted texture-displacement layers into real mesh geometry in the background, +// then commits the result on the main thread - mirrors EmbossJob's UpdateJob/update_volume() +// bake-and-commit pattern (see EmbossJob.cpp). +class TextureDisplacementBakeJob : public Job +{ +public: + TextureDisplacementBakeJob(TextureDisplacementBakeInput &&input, std::function on_finished); + + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &eptr) override; + +private: + TextureDisplacementBakeInput m_input; + TriangleMesh m_result; + // What the bake spent, for the message it leaves behind when the budget capped the detail. + TextureBakeStats m_stats; + // Per triangle of m_result: the filament to print it in, as an EnforcerBlockerType value + // (0 = leave alone). Empty unless a layer asked for colour. See TextureColorRequest. + std::vector m_triangle_color; + std::function m_on_finished; +}; + +// Captures `volume`'s current mesh/layers/paint data and queues a TextureDisplacementBakeJob on +// the app's UI job worker. `on_finished` is always called once the job settles (success, failure, +// or cancellation), so the caller can clear its own "bake in progress" UI state. Must be called +// from the main thread. +void queue_texture_displacement_bake(const ModelVolume &volume, const TextureColorSettings &color, + std::function on_finished, bool take_snapshot = true); + +} // namespace Slic3r::GUI + +#endif // slic3r_TextureDisplacementBakeJob_hpp_ diff --git a/src/slic3r/GUI/Jobs/TextureDisplacementDebugJob.cpp b/src/slic3r/GUI/Jobs/TextureDisplacementDebugJob.cpp new file mode 100644 index 0000000000..97958ead9d --- /dev/null +++ b/src/slic3r/GUI/Jobs/TextureDisplacementDebugJob.cpp @@ -0,0 +1,113 @@ +#include "TextureDisplacementDebugJob.hpp" + +#include "libslic3r/Model.hpp" + +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/Plater.hpp" + +namespace Slic3r::GUI { + +TextureDisplacementDebugJob::TextureDisplacementDebugJob( + TextureDisplacementDebugInput &&input, std::function)> on_finished) + : m_input(std::move(input)), m_on_finished(std::move(on_finished)) +{ +} + +void TextureDisplacementDebugJob::process(Ctl &ctl) +{ + const std::string status = _u8L("Capturing bake stages"); + ctl.update_status(1, status); + + // Only ever touches m_input (captured by value before this job was queued) and local state - and, + // unlike the bake job, never writes anything back either. + int last_reported = 1; + const auto report = [&ctl, &status, &last_reported](int percent) { + if (ctl.was_canceled()) + return false; + if (percent > last_reported) { + last_reported = percent; + ctl.update_status(percent, status); + } + return true; + }; + + BakeStageRecorder recorder; + recorder.enable(true); + recorder.set_check_topology(m_input.check_topology); + recorder.set_mesh_cap(m_input.mesh_cap); + + indexed_triangle_set mesh = m_input.base_mesh; + TextureDisplacementFacetsData masks = m_input.facets_data; + + // The default pipeline can only move vertices the mesh already has, so its recipe starts with the + // preparation - which is where "first it remeshes, then it refines" actually happens. The + // experimental pipeline refines as part of the bake and has nothing to prepare. + if (m_input.run_prepare && !m_input.options.pipeline_v2) { + const TextureDisplacementPrepareResult prepared = + GLGizmoTextureDisplacement::prepare_mesh(mesh, masks, m_input.layers, m_input.prepare_params, + m_input.color.palette, + // Preparation is roughly half the run; the bake + // takes the progress bar from there. + [&report](int pct) { return report(1 + pct / 2); }, + &recorder); + if (ctl.was_canceled()) + return; + // An empty result means nothing needed doing, which is not a failure - the bake below simply + // runs on the mesh as it stands. paint_lost means the remesh dropped every layer's paint, so + // there is nothing left to displace and the stages recorded so far are the whole story. + if (prepared.paint_lost) { + m_stages = recorder.take(); + ctl.update_status(100, status); + return; + } + if (!prepared.mesh.indices.empty()) { + mesh = prepared.mesh; + masks = prepared.masks; + } + } + + build_texture_displacement(mesh, m_input.layers, masks, m_input.options, + [&report](int pct) { return report(50 + pct / 2); }, + /* color */ nullptr, m_input.volume_to_world, &recorder); + + m_stages = recorder.take(); + ctl.update_status(100, status); // always finish at 100: this is what closes the notification +} + +void TextureDisplacementDebugJob::finalize(bool canceled, std::exception_ptr &eptr) +{ + if (!m_on_finished) + return; + // The handler must run on every outcome - the caller uses it to clear its in-progress latch, and + // an empty vector is its signal that nothing usable came back. + if (canceled || eptr) + m_on_finished({}); + else + m_on_finished(std::move(m_stages)); +} + +void queue_texture_displacement_debug(const ModelVolume &volume, const TextureColorSettings &color, + const TextureDisplacementPrepareParams &prepare_params, + bool run_prepare, bool check_topology, + std::function)> on_finished) +{ + TextureDisplacementDebugInput input; + input.volume_id = volume.id(); + input.base_mesh = volume.mesh().its; + input.layers = volume.texture_displacement_layers; + input.options = volume.texture_displacement_options; + input.prepare_params = prepare_params; + input.run_prepare = run_prepare; + input.check_topology = check_topology; + input.color = color; + input.volume_to_world = texture_displacement_volume_to_world(volume); + for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) + input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data(); + + auto &worker = wxGetApp().plater()->get_ui_job_worker(); + queue_job(worker, std::make_unique(std::move(input), std::move(on_finished))); +} + +} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/TextureDisplacementDebugJob.hpp b/src/slic3r/GUI/Jobs/TextureDisplacementDebugJob.hpp new file mode 100644 index 0000000000..0e7ca5c8a2 --- /dev/null +++ b/src/slic3r/GUI/Jobs/TextureDisplacementDebugJob.hpp @@ -0,0 +1,82 @@ +#ifndef slic3r_TextureDisplacementDebugJob_hpp_ +#define slic3r_TextureDisplacementDebugJob_hpp_ + +#include +#include + +#include "libslic3r/ObjectID.hpp" +#include "libslic3r/TextureBake/TextureBakeDebug.hpp" +#include "libslic3r/TextureDisplacement.hpp" +#include "libslic3r/TriangleMesh.hpp" + +#include "Job.hpp" + +namespace Slic3r { +class ModelVolume; +} + +namespace Slic3r::GUI { + +// Runs a bake with stage capture on and commits nothing. +// +// A bake is a chain of stages that each rewrite the whole mesh - remesh, refine, displace, smooth, or +// for the experimental pipeline subdivide, regularize, relocate, displace, decimate, repair - and when +// the result looks wrong the only useful question is which of them made it wrong. This runs the same +// recipe the Bake button runs, keeps every intermediate mesh, and hands them back for the gizmo to +// step through. +// +// Deliberately separate from TextureDisplacementBakeJob rather than a flag on it: this must never +// touch the Model, and keeping the committing path free of a "but not this time" branch is what +// guarantees that. +struct TextureDisplacementDebugInput +{ + ObjectID volume_id; + indexed_triangle_set base_mesh; + std::vector layers; + TextureDisplacementFacetsData facets_data; + TextureDisplacementOptions options; + // The preparation stages - remesh and adaptive refinement. Only the default pipeline uses them; + // the experimental one refines as part of the bake and ignores this entirely. + TextureDisplacementPrepareParams prepare_params; + bool run_prepare = true; + // Mesh coordinates -> world millimetres, as the bake gets it. The captured stages are brought + // back into mesh coordinates before they are handed over, so the gizmo can draw them directly. + Transform3d volume_to_world = Transform3d::Identity(); + // Only the quantizer is used, and only by the refinement's colour criterion - the debug run + // produces no colour of its own. + TextureColorSettings color; + + // The edge scan behind the open / non-manifold counts is a sort over every half-edge, which on a + // multi-million triangle stage costs more than the stage did. On by default because a stage that + // tore the mesh is exactly what this exists to find. + bool check_topology = true; + // Stages above this keep their counts but not their geometry - see BakeStageRecorder. + size_t mesh_cap = 4'000'000; +}; + +class TextureDisplacementDebugJob : public Job +{ +public: + TextureDisplacementDebugJob(TextureDisplacementDebugInput &&input, + std::function)> on_finished); + + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &eptr) override; + +private: + TextureDisplacementDebugInput m_input; + std::vector m_stages; + std::function)> m_on_finished; +}; + +// Captures `volume`'s current mesh, layers and paint and queues one debug run. `on_finished` always +// runs exactly once on the UI thread, with an empty vector if the run was cancelled or threw, so the +// caller can clear its own in-progress state. Must be called from the main thread. +void queue_texture_displacement_debug(const ModelVolume &volume, const TextureColorSettings &color, + const TextureDisplacementPrepareParams &prepare_params, + bool run_prepare, bool check_topology, + std::function)> on_finished); + +} // namespace Slic3r::GUI + +#endif // slic3r_TextureDisplacementDebugJob_hpp_ diff --git a/src/slic3r/GUI/Jobs/TextureDisplacementPrepareJob.cpp b/src/slic3r/GUI/Jobs/TextureDisplacementPrepareJob.cpp new file mode 100644 index 0000000000..4494594c7d --- /dev/null +++ b/src/slic3r/GUI/Jobs/TextureDisplacementPrepareJob.cpp @@ -0,0 +1,117 @@ +#include "TextureDisplacementPrepareJob.hpp" + +#include +#include + +#include "libslic3r/Model.hpp" +#include "libslic3r/TriangleSelector.hpp" + +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp" +#include "slic3r/Utils/UndoRedo.hpp" + +namespace Slic3r::GUI { + +TextureDisplacementPrepareJob::TextureDisplacementPrepareJob( + TextureDisplacementPrepareInput &&input, std::function on_finished) + : m_input(std::move(input)), m_on_finished(std::move(on_finished)) +{ +} + +void TextureDisplacementPrepareJob::process(Ctl &ctl) +{ + const std::string status = _u8L("Preparing mesh for texture displacement"); + ctl.update_status(1, status); + + // Only ever touches m_input (captured by value before this job was queued) and local state - never + // the live Model - so this is safe to run concurrently with the UI thread. + // + // The progress hook is not just cosmetic: the framework's notification only grows a close button + // once it reaches 100%, and below that it shows Cancel, which is wired through here. Reporting is + // throttled to whole percentage points because every call repaints the notification and wakes the + // idle loop. + int last_reported = 1; + m_result = GLGizmoTextureDisplacement::prepare_mesh(m_input.base_mesh, m_input.masks, m_input.layers, + m_input.params, m_input.color.palette, + [&ctl, &status, &last_reported](int percent) { + if (ctl.was_canceled()) + return false; + if (percent > last_reported) { + last_reported = percent; + ctl.update_status(percent, status); + } + return true; + }); + + ctl.update_status(100, status); // always finish at 100: this is what closes the notification +} + +void TextureDisplacementPrepareJob::finalize(bool canceled, std::exception_ptr &eptr) +{ + TextureDisplacementPrepareOutcome outcome = TextureDisplacementPrepareOutcome::Failed; + struct OnExit + { + std::function fn; + const TextureDisplacementPrepareOutcome *outcome; + ~OnExit() { if (fn) fn(*outcome); } + } on_exit{m_on_finished, &outcome}; + + if (canceled || eptr) + return; + if (m_result.paint_lost) { + outcome = TextureDisplacementPrepareOutcome::PaintLost; + return; + } + if (m_result.mesh.indices.empty()) { + outcome = TextureDisplacementPrepareOutcome::Unchanged; + return; + } + + Plater *plater = wxGetApp().plater(); + ModelVolume *volume = get_model_volume(m_input.volume_id, plater->model().objects); + // The lookup doubles as a staleness check: anything that replaces a volume's mesh also gives it a + // new id (set_new_unique_id()), so a prepare computed from a mesh that has since been replaced - + // by an undo, another bake, or a boolean - simply fails to find its volume and commits nothing. + if (volume == nullptr) + return; + ModelObject *object = volume->get_object(); + if (object == nullptr) + return; + + { + Plater::TakeSnapshot snapshot(plater, m_input.snapshot_name, UndoRedo::SnapshotType::GizmoAction); + + // The four standard paint channels ride across on ModelVolume's own spatial remap. The eight + // texture-displacement masks do not go through it - prepare_mesh() has already carried them, + // exactly, from the source triangles the refinement records - so they are put back after + // restore_painting(), which resets every extra facet before remapping the channels it knows. + std::optional saved_painting = volume->save_painting(); + volume->set_mesh(TriangleMesh(std::move(m_result.mesh))); + volume->set_new_unique_id(); + volume->calculate_convex_hull(); + volume->restore_painting(saved_painting); + for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) + volume->texture_displacement_facet(i).set_data(std::move(m_result.masks[size_t(i)])); + + if (ObjectList *obj_list = wxGetApp().obj_list()) { + const ModelObjectPtrs &objs = plater->model().objects; + auto it = std::find(objs.begin(), objs.end(), object); + if (it != objs.end()) + obj_list->update_info_items(size_t(it - objs.begin())); + } + plater->changed_object(*object); + } + outcome = TextureDisplacementPrepareOutcome::Committed; +} + +void queue_texture_displacement_prepare(TextureDisplacementPrepareInput &&input, + std::function on_finished) +{ + auto &worker = wxGetApp().plater()->get_ui_job_worker(); + queue_job(worker, std::make_unique(std::move(input), std::move(on_finished))); +} + +} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/TextureDisplacementPrepareJob.hpp b/src/slic3r/GUI/Jobs/TextureDisplacementPrepareJob.hpp new file mode 100644 index 0000000000..efe6a8ce75 --- /dev/null +++ b/src/slic3r/GUI/Jobs/TextureDisplacementPrepareJob.hpp @@ -0,0 +1,77 @@ +#ifndef slic3r_TextureDisplacementPrepareJob_hpp_ +#define slic3r_TextureDisplacementPrepareJob_hpp_ + +#include +#include +#include + +#include "Job.hpp" + +#include "libslic3r/Color.hpp" +#include "libslic3r/ObjectID.hpp" +#include "libslic3r/TextureDisplacement.hpp" +#include "libslic3r/TriangleMesh.hpp" + +namespace Slic3r { +class ModelVolume; +} + +namespace Slic3r::GUI { + +// Getting a mesh ready to receive displacement - the isotropic remesh, carrying the paint onto it, and +// the adaptive refinement - off the UI thread. +// +// All three used to run inline behind a wxBusyCursor, which on any part big enough to matter meant tens +// of seconds with the window not repainting and no way to stop it: CGAL's remesher is single threaded +// and its cost grows with the square of 1/target_edge, and the refinement that follows spends a budget +// of hundreds of thousands of triangles. Indistinguishable from a hang, and reported as one. +// +// The work itself is GLGizmoTextureDisplacement::prepare_mesh(), which is pure - it takes an +// indexed_triangle_set and the layers' masks and returns new ones, touching no Model and no GUI - so all +// this job adds is the worker thread, the progress notification with its Cancel button, and the commit. +enum class TextureDisplacementPrepareOutcome +{ + Committed, // the volume now carries the prepared mesh and the paint carried onto it + Unchanged, // nothing needed doing - the mesh already met the criteria; the model was not touched + PaintLost, // the remesh landed but no layer's paint survived it, so nothing was committed + Failed, // cancelled, threw, or the volume went away while the job ran +}; + +struct TextureDisplacementPrepareInput +{ + ObjectID volume_id; + indexed_triangle_set base_mesh; + TextureDisplacementFacetsData masks; + std::vector layers; + TextureDisplacementPrepareParams params; + // Captured on the main thread. Empty when no layer is colouring, in which case the refinement + // skips the colour criterion entirely. Only the *quantizer* is used here: refinement follows + // perceived colour, never the interleaving that realises a mix. + TextureColorSettings color; + // The undo step the commit opens. Standard mode's Bake names it after the bake, because the + // displacement job that follows commits into this same step rather than pushing its own. + std::string snapshot_name; +}; + +class TextureDisplacementPrepareJob : public Job +{ + TextureDisplacementPrepareInput m_input; + TextureDisplacementPrepareResult m_result; + std::function m_on_finished; + +public: + TextureDisplacementPrepareJob(TextureDisplacementPrepareInput &&input, + std::function on_finished); + + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &eptr) override; +}; + +// `on_finished` runs on the UI thread once the result has been committed (or found not to need +// committing), and always runs exactly once. +void queue_texture_displacement_prepare(TextureDisplacementPrepareInput &&input, + std::function on_finished); + +} // namespace Slic3r::GUI + +#endif // slic3r_TextureDisplacementPrepareJob_hpp_ diff --git a/src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.cpp b/src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.cpp new file mode 100644 index 0000000000..1845314f0d --- /dev/null +++ b/src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.cpp @@ -0,0 +1,66 @@ +#include "TextureDisplacementPreviewJob.hpp" + +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp" + +namespace Slic3r::GUI { + +TextureDisplacementPreviewJob::TextureDisplacementPreviewJob(TextureDisplacementPreviewInput &&input, uint64_t generation, + std::shared_ptr> current_generation, + std::function on_finished) + : m_input(std::move(input)), m_generation(generation), m_current_generation(std::move(current_generation)), + m_on_finished(std::move(on_finished)) +{ +} + +void TextureDisplacementPreviewJob::process(Ctl &ctl) +{ + // No ctl.update_status() anywhere in here on purpose - see the class comment. A preview is + // invisible bookkeeping; the only thing on screen should be the preview itself. + + // Only ever touches m_input (captured by value before this job was queued) and local state - + // never the live Model - so this is safe to run concurrently with the UI thread. + TextureColorRequest color_request; + TextureColorRequest *color = nullptr; + if (!m_input.color.empty()) { + color_request.quantize = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette); + if (!m_input.color.palette_pure.empty()) + color_request.quantize_pure = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette_pure); + color_request.resolve = GLGizmoTextureDisplacement::make_mix_resolver( + m_input.color.palette, m_input.color.mix_mode, m_input.color.layer_height, + m_input.color.dither_cell_mm); + color_request.despeckle_passes = m_input.color.despeckle_passes; + color_request.out_triangle = &m_result.triangle_color; + if (color_request.quantize) + color = &color_request; + } + + m_result.mesh = build_texture_displacement(m_input.base_mesh, m_input.layers, m_input.facets_data, + m_input.options, + [this, &ctl](int) { + // Bail the moment this preview stops being the current + // one; build_texture_displacement() then returns an + // empty mesh and finalize() drops it. + return !ctl.was_canceled() && + (!m_current_generation || + m_current_generation->load() == m_generation); + }, + color, m_input.volume_to_world); +} + +void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &eptr) +{ + if (!m_on_finished) + return; + // The handler must run on *every* outcome, cancellation included, because the caller uses it to + // clear its "a job is in flight" latch. Returning early on `canceled` - which is what a cancel_all() + // from Plater (project load/close, app exit) delivers - left that latch stuck true and no preview + // was ever queued again for the rest of the session. An empty result is the caller's signal that + // nothing usable came back; it already handles that. + if (canceled || eptr) + m_on_finished(TextureDisplacementPreviewResult{}, m_generation); + else + m_on_finished(std::move(m_result), m_generation); +} + +} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.hpp b/src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.hpp new file mode 100644 index 0000000000..312cf6593e --- /dev/null +++ b/src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.hpp @@ -0,0 +1,82 @@ +#ifndef slic3r_TextureDisplacementPreviewJob_hpp_ +#define slic3r_TextureDisplacementPreviewJob_hpp_ + +#include +#include +#include +#include +#include + +#include "libslic3r/Color.hpp" +#include "libslic3r/TextureDisplacement.hpp" +#include "libslic3r/TriangleMesh.hpp" + +#include "Job.hpp" + +namespace Slic3r::GUI { + +// Everything process() needs, captured by value on the main thread when the job is queued - +// mirrors TextureDisplacementBakeInput, but a preview never writes back to the Model. +struct TextureDisplacementPreviewInput +{ + indexed_triangle_set base_mesh; + std::vector layers; + TextureDisplacementFacetsData facets_data; + TextureDisplacementOptions options; + // Mesh coordinates -> world millimetres, so the preview is displaced in the same space the bake + // is and the two cannot disagree. See build_texture_displacement(). + Transform3d volume_to_world = Transform3d::Identity(); + // Empty unless a layer is colouring, in which case the preview reports the filament per triangle + // alongside the mesh, so the Normal view shows what the bake will produce - interleaving included. + TextureColorSettings color; +}; + +// A preview result: the displaced mesh, and - when the input carried a palette - one filament index +// per triangle (an EnforcerBlockerType value; 0 means "no colour from the texture"). +struct TextureDisplacementPreviewResult +{ + indexed_triangle_set mesh; + std::vector triangle_color; +}; + +// Computes the true (unbaked) displaced-mesh preview in the background. With several painted +// layers this is real, non-trivial CPU work (PNG sampling, per-layer vertex welding), which used +// to run synchronously on every paint stroke and parameter tweak and made editing feel slow with +// more than one or two layers. Unlike Bake, this never touches the live Model - a preview is +// purely informational, there is nothing to commit. +// +// Deliberately reports no status: the Job framework turns the first update_status() call into an +// on-screen progress notification, and a preview firing one on every stroke and slider release +// buried the user in notifications that only closed at 100%. +class TextureDisplacementPreviewJob : public Job +{ +public: + // `generation` is an opaque token the caller controls (typically an incrementing counter): + // on_finished should only actually be applied by the caller if it still matches the caller's + // current generation when the job completes, so that a burst of edits queuing several of + // these jobs in a row can't have an earlier, now-stale result clobber a later one that + // finishes first. + // + // `current_generation` is the caller's live counter, shared with the worker thread. The job + // polls it *while computing* and aborts as soon as it no longer matches - so a preview that has + // already been superseded stops burning CPU instead of running to completion for a result that + // will only be thrown away. That matters because the UI job worker runs one job at a time in FIFO + // order: without it, a Bake queued behind a handful of stale previews waits for every one of them. + TextureDisplacementPreviewJob(TextureDisplacementPreviewInput &&input, uint64_t generation, + std::shared_ptr> current_generation, + std::function on_finished); + + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &eptr) override; + +private: + TextureDisplacementPreviewInput m_input; + uint64_t m_generation; + std::shared_ptr> m_current_generation; + TextureDisplacementPreviewResult m_result; + std::function m_on_finished; +}; + +} // namespace Slic3r::GUI + +#endif // slic3r_TextureDisplacementPreviewJob_hpp_ diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index bd9327ad61..2622fdbc28 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -103,6 +103,7 @@ #include "Selection.hpp" #include "GLToolbar.hpp" #include "GUI_Preview.hpp" +#include "UVEditorCanvas.hpp" #include "3DBed.hpp" #include "PartPlate.hpp" #include "Camera.hpp" @@ -1797,11 +1798,14 @@ bool Sidebar::priv::switch_diameter_to(const wxString &diameter) Preset& printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); // The combo lists printer variants, and the variant of a mixed-nozzle machine ("0.4+0.6") is no // single extruder's diameter, so the preset's own variant answers first. - if (printer_preset.config.opt_string("printer_variant") == diameter.ToStdString()) { + const std::string &printer_variant = printer_preset.config.opt_string("printer_variant"); + if (printer_variant == diameter.ToStdString()) { return true; } + // A named variant ("0.4 High Flow") shares its diameter with the standard profile, which selecting + // the plain diameter switches back to, so only a preset naming no variant is kept by its diameter. auto* nozzle_diameter = dynamic_cast(printer_preset.config.option("nozzle_diameter")); - if (nozzle_diameter && nozzle_diameter->size() > 0) { + if (printer_variant.empty() && nozzle_diameter && nozzle_diameter->size() > 0) { auto current_nozzle_dia = get_diameter_string(nozzle_diameter->values[0]); // If the selected diameter is the same as current nozzle, don't switch profiles if (current_nozzle_dia == diameter.ToStdString()) { @@ -3933,13 +3937,18 @@ void Sidebar::update_presets(Preset::Type preset_type) combo_flow->Show(combo_flow->GetCount() > 0); }; - auto update_extruder_diameter = [&diameters, &nozzle_diameter](int extruder_index,ExtruderGroup & extruder) { + auto update_extruder_diameter = [&diameters, &nozzle_diameter, &diameter](int extruder_index,ExtruderGroup & extruder) { extruder.combo_diameter->Clear(); if (extruder_index >= int(nozzle_diameter->values.size())) return; int select = -1; // ORCA get the actual nozzle diameter from printer config auto nozzle_dia = get_diameter_string(nozzle_diameter->values[extruder_index]); + // Named variants such as "0.4HS" and "0.4 High Flow" share a physical diameter. + // Retain the variant selection unless the diameter was customized. + const bool keep_variant = diameter.substr(0, diameter.find_first_not_of("0123456789.")) == nozzle_dia && + std::find(diameters.begin(), diameters.end(), diameter) != diameters.end(); + const std::string &selected_variant = keep_variant ? diameter : nozzle_dia; // ORCA try to add nozzle diameter from config if list is empty. fixes blank nozzle combo box when preset has no alias if(!diameters.empty() && diameters[0].empty() && !nozzle_dia.empty()){ diameters[0] = nozzle_dia; @@ -3949,7 +3958,7 @@ void Sidebar::update_presets(Preset::Type preset_type) diameters.push_back(nozzle_dia); } for (size_t i = 0; i < diameters.size(); ++i) { - if (diameters[i] == nozzle_dia) + if (diameters[i] == selected_variant) select = extruder.combo_diameter->GetCount(); extruder.combo_diameter->Append(diameters[i], {}); } @@ -7108,6 +7117,13 @@ struct Plater::priv GLToolbar collapse_toolbar; Preview *preview; AssembleView* assemble_view { nullptr }; + // Docked/resizable 2D pane showing GLGizmoTextureDisplacement's LSCM unwrap of a painted + // patch; a sibling AUI pane alongside "sidebar"/"main", not part of the view3D/preview/ + // assemble_view sizer - see its registration below and Plater::get_uv_editor_canvas(). The + // pane hosts the panel (toolbar + canvas + status line); uv_editor_canvas is its inner canvas, + // cached so the gizmo can reach it directly. + UVEditorPanel* uv_editor_panel { nullptr }; + UVEditorCanvas* uv_editor_canvas { nullptr }; bool first_enter_assemble{ true }; std::unique_ptr notification_manager; @@ -7357,6 +7373,8 @@ struct Plater::priv void undo(); void redo(); + // True, and tells the user, while a background job is working on the model - see the definition. + bool undo_redo_blocked_by_job(); void undo_redo_to(size_t time_to_load); // BBS: backup @@ -7810,6 +7828,26 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) .BottomDockable(false) .BestSize(wxSize(39 * wxGetApp().em_unit(), 90 * wxGetApp().em_unit()))); + // UV editor pane for GLGizmoTextureDisplacement's LSCM unwrap preview - a resizable/dockable + // sibling of "sidebar"/"main" like everything else registered on this same AUI manager, not a + // change to the view3D/preview/assemble_view sizer above. Hidden by default: only relevant + // while that gizmo is active with a layer using the "Unwrap (LSCM)" projection method (see + // Plater::show_uv_editor()), so it stays out of the way of everyone else's window layout. + uv_editor_panel = new UVEditorPanel(q); + uv_editor_canvas = uv_editor_panel->canvas(); + m_aui_mgr.AddPane(uv_editor_panel, wxAuiPaneInfo() + .Name("uv_editor") + .Caption(_L("UV Editor")) + .Right() + .Hide() + .BestSize(wxSize(40 * wxGetApp().em_unit(), 40 * wxGetApp().em_unit()))); + // Closing the pane with its own X has to reach the gizmo, or its next update would simply show the pane again. + q->Bind(wxEVT_AUI_PANE_CLOSE, [this](wxAuiManagerEvent &evt) { + evt.Skip(); + if (evt.GetPane() != nullptr && evt.GetPane()->window == uv_editor_panel && uv_editor_canvas != nullptr) + uv_editor_canvas->run_command(UVEditorCanvas::Command::PaneClosed); + }); + auto* panel_sizer = new wxBoxSizer(wxHORIZONTAL); panel_sizer->Add(view3D, 1, wxEXPAND | wxALL, 0); panel_sizer->Add(preview, 1, wxEXPAND | wxALL, 0); @@ -7851,6 +7889,13 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) BOOST_LOG_TRIVIAL(info) << "Removed floating AUI state from saved window layout for Wayland"; } + // The UV editor is a transient, gizmo-driven pane (see show_uv_editor()); a saved layout + // from a session that happened to close with it open would otherwise restore it visible on + // startup, with nothing painted in it. Force it hidden here so it only ever appears when the + // texture-displacement gizmo asks for it. + if (wxAuiPaneInfo &uv_pane = m_aui_mgr.GetPane("uv_editor"); uv_pane.IsOk()) + uv_pane.Hide(); + sidebar_layout.is_collapsed = !sidebar.IsShown(); } @@ -14993,8 +15038,25 @@ void Plater::priv::take_snapshot(const std::string& snapshot_name, const UndoRed BOOST_LOG_TRIVIAL(info) << "Undo / Redo snapshot taken: " << snapshot_name << ", Undo / Redo stack memory: " << Slic3r::format_memsize_MB(this->undo_redo_stack().memsize()) << log_memory_info(); } +// A background job holds the model it is working on: the texture displacement bake, for one, hands its +// result to the volume when it finishes, and it was queued against the geometry as it was at the time. +// Undoing while it runs restores an older state under it - a different transform, a different mesh - +// and the result then lands on geometry it was never computed for. Undo and redo therefore wait for +// the job, and say so rather than doing nothing. +bool Plater::priv::undo_redo_blocked_by_job() +{ + if (m_worker.is_idle()) + return false; + notification_manager->push_notification(NotificationType::CustomNotification, + NotificationManager::NotificationLevel::RegularNotificationLevel, + _u8L("Cannot undo or redo while an operation is running. Stop it first.")); + return true; +} + void Plater::priv::undo() { + if (this->undo_redo_blocked_by_job()) + return; const std::vector &snapshots = this->undo_redo_stack().snapshots(); auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(this->undo_redo_stack().active_snapshot_time())); // BBS: undo-redo until modify record @@ -15012,6 +15074,8 @@ void Plater::priv::undo() void Plater::priv::redo() { + if (this->undo_redo_blocked_by_job()) + return; const std::vector &snapshots = this->undo_redo_stack().snapshots(); auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(this->undo_redo_stack().active_snapshot_time())); // BBS: undo-redo until modify record @@ -16657,7 +16721,7 @@ void adjust_settings_for_flowrate_calib(ModelObjectPtrs& objects, bool linear, i auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; - /// --- scale --- + /// -- scale -- // model is created for a 0.4 nozzle, scale z with nozzle size. const ConfigOptionFloats* nozzle_diameter_config = printer_config->option("nozzle_diameter"); std::vector extruder_types = printer_config->option("extruder_type")->values; @@ -21134,6 +21198,33 @@ GLCanvas3D* Plater::get_assmeble_canvas3D() return nullptr; } +UVEditorCanvas* Plater::get_uv_editor_canvas() +{ + return p->uv_editor_canvas; +} + +void Plater::show_uv_editor(bool show) +{ + if (p->uv_editor_panel == nullptr) + return; + const wxAuiPaneInfo &pane = p->m_aui_mgr.GetPane(p->uv_editor_panel); + if (!pane.IsOk() || pane.IsShown() == show) + return; + + // Deferred, because GLGizmoTextureDisplacement calls this from its ImGui panel - that is, from + // the middle of the 3D canvas's GL frame. Showing an AUI pane re-lays out the window and + // delivers the resulting size/paint events synchronously, and the UV canvas painting itself + // makes its own surface current in the app's *shared* GL context, which mid-frame is the one + // the 3D canvas is drawing into. Doing the layout once the frame is over avoids that entirely. + CallAfter([this, show]() { + wxAuiPaneInfo &deferred_pane = p->m_aui_mgr.GetPane(p->uv_editor_panel); + if (!deferred_pane.IsOk() || deferred_pane.IsShown() == show) + return; + deferred_pane.Show(show); + p->m_aui_mgr.Update(); + }); +} + GLCanvas3D* Plater::get_current_canvas3D(bool exclude_preview) { return p->get_current_canvas3D(exclude_preview); @@ -22873,8 +22964,9 @@ bool Plater::can_copy_to_clipboard() const return true; } -bool Plater::can_undo() const { return IsShown() && p->is_view3D_shown() && p->undo_redo_stack().has_undo_snapshot(); } -bool Plater::can_redo() const { return IsShown() && p->is_view3D_shown() && p->undo_redo_stack().has_redo_snapshot(); } +// The job check keeps the buttons in step with priv::undo()/redo(), which refuse while one runs. +bool Plater::can_undo() const { return IsShown() && p->is_view3D_shown() && p->m_worker.is_idle() && p->undo_redo_stack().has_undo_snapshot(); } +bool Plater::can_redo() const { return IsShown() && p->is_view3D_shown() && p->m_worker.is_idle() && p->undo_redo_stack().has_redo_snapshot(); } bool Plater::can_reload_from_disk() const { return p->can_reload_from_disk(); } //BBS bool Plater::can_fillcolor() const { return p->can_fillcolor(); } diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 90d0e5e673..ebff1ff90d 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -672,6 +672,13 @@ public: GLCanvas3D* get_assmeble_canvas3D(); wxWindow* get_select_machine_dialog(); + // Docked UV-editor pane used by GLGizmoTextureDisplacement's LSCM projection preview (see + // UVEditorCanvas.hpp). Returns nullptr only before the main window is fully constructed. + class UVEditorCanvas* get_uv_editor_canvas(); + // Shows or hides the UV-editor AUI pane, updating its docked layout accordingly. Safe to call + // repeatedly (e.g. every time the gizmo's active layer/projection method changes). + void show_uv_editor(bool show); + void arrange(); void orient(); void find_new_position(const ModelInstancePtrs &instances); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index c824c16d5d..b6f071fb45 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4396,16 +4396,17 @@ void TabFilament::build() optgroup->append_single_option_line("temperature_vitrification", "material_basic_information#softening-temperature"); optgroup->append_single_option_line("idle_temperature", "material_basic_information#idle-temperature"); Line line = { L("Recommended nozzle temperature"), L("Recommended nozzle temperature range of this filament. 0 means not set") }; - line.append_option(optgroup->get_option("nozzle_temperature_range_low")); - line.append_option(optgroup->get_option("nozzle_temperature_range_high")); + line.append_option(optgroup->get_option("nozzle_temperature_range_low", 0)); + line.append_option(optgroup->get_option("nozzle_temperature_range_high", 0)); optgroup->append_line(line); optgroup->m_on_change = [this, optgroup](t_config_option_key opt_key, boost::any value) { DynamicPrintConfig &filament_config = m_preset_bundle->filaments.get_edited_preset().config; update_dirty(); - if (!m_postpone_update_ui && (opt_key == "nozzle_temperature_range_low" || opt_key == "nozzle_temperature_range_high")) { - m_config_manipulation.check_nozzle_recommended_temperature_range(&filament_config); + const std::string opt_key_without_idx = opt_key.substr(0, opt_key.find('#')); + if (!m_postpone_update_ui && (opt_key_without_idx == "nozzle_temperature_range_low" || opt_key_without_idx == "nozzle_temperature_range_high")) { + m_config_manipulation.check_nozzle_recommended_temperature_range(&filament_config, selected_variant_index()); } on_value_change(opt_key, value); }; @@ -4520,6 +4521,7 @@ void TabFilament::build() DynamicPrintConfig& filament_config = m_preset_bundle->filaments.get_edited_preset().config; update_dirty(); + const std::string opt_key_without_idx = opt_key.substr(0, opt_key.find('#')); /*if (opt_key == "cool_plate_temp" || opt_key == "cool_plate_temp_initial_layer") { m_config_manipulation.check_bed_temperature_difference(BedType::btPC, &filament_config); } @@ -4532,11 +4534,11 @@ void TabFilament::build() else if (opt_key == "textured_plate_temp" || opt_key == "textured_plate_temp_initial_layer") { m_config_manipulation.check_bed_temperature_difference(BedType::btPTE, &filament_config); } - else */if (opt_key == "nozzle_temperature") { - m_config_manipulation.check_nozzle_temperature_range(&filament_config); + else */if (opt_key_without_idx == "nozzle_temperature") { + m_config_manipulation.check_nozzle_temperature_range(&filament_config, selected_variant_index()); } - else if (opt_key == "nozzle_temperature_initial_layer") { - m_config_manipulation.check_nozzle_temperature_initial_layer_range(&filament_config); + else if (opt_key_without_idx == "nozzle_temperature_initial_layer") { + m_config_manipulation.check_nozzle_temperature_initial_layer_range(&filament_config, selected_variant_index()); } on_value_change(opt_key, value); @@ -4571,12 +4573,12 @@ void TabFilament::build() optgroup = page->new_optgroup(L("Part cooling fan"), L"param_cooling_part_fan"); line = { L("Min fan speed threshold"), L("The part cooling fan will run at the minimum fan speed when the estimated layer time is longer than the threshold value. When the layer time is shorter than the threshold, the fan speed will be interpolated between the minimum and maximum fan speed according to layer printing time.") }; line.label_path = "material_cooling#material-part-cooling-fan"; - line.append_option(optgroup->get_option("fan_min_speed")); + line.append_option(optgroup->get_option("fan_min_speed", 0)); line.append_option(optgroup->get_option("fan_cooling_layer_time")); optgroup->append_line(line); line = { L("Max fan speed threshold"), L("The part cooling fan will run at maximum speed when the estimated layer time is shorter than the threshold value.") }; line.label_path = "material_cooling#material-part-cooling-fan"; - line.append_option(optgroup->get_option("fan_max_speed")); + line.append_option(optgroup->get_option("fan_max_speed", 0)); line.append_option(optgroup->get_option("slow_down_layer_time")); optgroup->append_line(line); optgroup->append_single_option_line("reduce_fan_stop_start_freq", "material_cooling#keep-fan-always-on"); @@ -4592,7 +4594,7 @@ void TabFilament::build() optgroup->append_single_option_line("ironing_fan_speed", "material_cooling#ironing-fan-speed"); // ORCA: Add support for ironing fan speed control optgroup = page->new_optgroup(L("Auxiliary part cooling fan"), L"param_cooling_aux_fan"); - optgroup->append_single_option_line("additional_cooling_fan_speed", "material_cooling#auxiliary-part-cooling-fan"); + optgroup->append_single_option_line("additional_cooling_fan_speed", "material_cooling#auxiliary-part-cooling-fan", 0); optgroup = page->new_optgroup(L("Exhaust fan"),L"param_cooling_exhaust"); @@ -4656,7 +4658,7 @@ void TabFilament::build() page = add_options_page(L("Multimaterial"), "custom-gcode_multi_material"); // ORCA: icon only visible on placeholders optgroup = page->new_optgroup(L("Wipe tower parameters"), "param_tower"); - optgroup->append_single_option_line("filament_minimal_purge_on_wipe_tower", "material_multimaterial#multimaterial-wipe-tower-parameters"); + optgroup->append_single_option_line("filament_minimal_purge_on_wipe_tower", "material_multimaterial#multimaterial-wipe-tower-parameters", 0); optgroup->append_single_option_line("filament_tower_interface_pre_extrusion_dist", "material_multimaterial#multimaterial-wipe-tower-parameters"); optgroup->append_single_option_line("filament_tower_interface_pre_extrusion_length", "material_multimaterial#multimaterial-wipe-tower-parameters"); optgroup->append_single_option_line("filament_tower_ironing_area", "material_multimaterial#multimaterial-wipe-tower-parameters"); @@ -4700,9 +4702,9 @@ void TabFilament::build() }); optgroup = page->new_optgroup(L("Tool change parameters with multi extruder MM printers"), "param_toolchange_multi_extruder"); - optgroup->append_single_option_line("filament_multitool_ramming", "material_multimaterial#tool-change-parameters-with-multi-extruder"); - optgroup->append_single_option_line("filament_multitool_ramming_volume", "material_multimaterial#multi-tool-ramming-volume"); - optgroup->append_single_option_line("filament_multitool_ramming_flow", "material_multimaterial#multi-tool-ramming-flow"); + optgroup->append_single_option_line("filament_multitool_ramming", "material_multimaterial#tool-change-parameters-with-multi-extruder", 0); + optgroup->append_single_option_line("filament_multitool_ramming_volume", "material_multimaterial#multi-tool-ramming-volume", 0); + optgroup->append_single_option_line("filament_multitool_ramming_flow", "material_multimaterial#multi-tool-ramming-flow", 0); page = add_options_page(L("Dependencies"), "advanced"); optgroup = page->new_optgroup(L("Compatible printers"), "param_dependencies_printers"); @@ -4770,8 +4772,14 @@ void TabFilament::update_description_lines() // this->update_volumetric_flow_preset_hints(); } +unsigned int TabFilament::selected_variant_index() const +{ + return m_variant_combo ? std::max(0, m_variant_combo->GetSelection()) : 0; +} + void TabFilament::toggle_options() { + const unsigned int variant_index = selected_variant_index(); if (!m_active_page) return; bool is_BBL_printer = false; @@ -4813,7 +4821,7 @@ void TabFilament::toggle_options() } } - toggle_line("additional_cooling_fan_speed", printer_cfg.opt_bool("auxiliary_fan")); + toggle_line("additional_cooling_fan_speed", printer_cfg.opt_bool("auxiliary_fan"), 256 + variant_index); bool support_air_filtration = printer_cfg.opt_bool("support_air_filtration"); for (auto el : {"activate_air_filtration", "during_print_exhaust_fan_speed", "complete_print_exhaust_fan_speed"}) @@ -4829,11 +4837,8 @@ void TabFilament::toggle_options() } if (m_active_page->title() == L("Filament")) { - const int selection = m_variant_combo ? m_variant_combo->GetSelection() : 0; - const unsigned int variant_idx = (unsigned int) std::max(selection, 0); - - bool pa = m_config->opt_bool("enable_pressure_advance", variant_idx); - toggle_option("pressure_advance", pa, 0); + bool pa = m_config->opt_bool("enable_pressure_advance", variant_index); + toggle_option("pressure_advance", pa, 256 + variant_index); //Orca: Enable the plates that should be visible when multi bed support is enabled or a BBL printer is selected; otherwise, enable only the plate visible for the selected bed type. DynamicConfig& proj_cfg = m_preset_bundle->project_config; @@ -4863,7 +4868,7 @@ void TabFilament::toggle_options() // If adaptive PA is not enabled, hide the adaptive PA model section toggle_option("adaptive_pressure_advance", pa, 0); toggle_option("adaptive_pressure_advance_overhangs", pa, 0); - bool has_adaptive_pa = m_config->opt_bool("adaptive_pressure_advance", variant_idx); + bool has_adaptive_pa = m_config->opt_bool("adaptive_pressure_advance", variant_index); toggle_line("adaptive_pressure_advance_overhangs", has_adaptive_pa && pa, 0); toggle_line("adaptive_pressure_advance_model", has_adaptive_pa && pa, 0); toggle_line("adaptive_pressure_advance_bridges", has_adaptive_pa && pa, 0); @@ -4874,9 +4879,9 @@ void TabFilament::toggle_options() toggle_line("activate_chamber_temp_control", printer_cfg.opt_bool("support_chamber_temp_control")); - std::string volumetric_speed_cos = m_config->opt_string("volumetric_speed_coefficients", variant_idx); + std::string volumetric_speed_cos = m_config->opt_string("volumetric_speed_coefficients", variant_index); bool enable_fit = volumetric_speed_cos != "0 0 0 0 0 0"; - toggle_option("filament_adaptive_volumetric_speed", enable_fit, 256 + variant_idx); + toggle_option("filament_adaptive_volumetric_speed", enable_fit, 256 + variant_index); } if (m_active_page->title() == L("Setting Overrides")) @@ -4884,20 +4889,19 @@ void TabFilament::toggle_options() if (m_active_page->title() == L("Multimaterial")) { // Orca: hide specific settings for BBL printers - for (auto el : {"filament_minimal_purge_on_wipe_tower", "filament_loading_speed_start", "filament_loading_speed", + toggle_option("filament_minimal_purge_on_wipe_tower", !is_BBL_printer, 256 + variant_index); + for (auto el : {"filament_loading_speed_start", "filament_loading_speed", "filament_unloading_speed_start", "filament_unloading_speed", "filament_toolchange_delay", "filament_cooling_moves", "filament_cooling_initial_speed", "filament_cooling_final_speed"}) toggle_option(el, !is_BBL_printer); - bool multitool_ramming = m_config->opt_bool("filament_multitool_ramming", 0); - toggle_option("filament_multitool_ramming_volume", multitool_ramming); - toggle_option("filament_multitool_ramming_flow", multitool_ramming); + bool multitool_ramming = m_config->opt_bool("filament_multitool_ramming", variant_index); + toggle_option("filament_multitool_ramming_volume", multitool_ramming, 256 + variant_index); + toggle_option("filament_multitool_ramming_flow", multitool_ramming, 256 + variant_index); bool is_BBL_multi_extruder = is_BBL_printer && printer_cfg.option("nozzle_diameter")->size() > 1; - const int selection = m_variant_combo ? m_variant_combo->GetSelection() : 0; - const int extruder_idx = std::max(selection, 0); - toggle_line("long_retractions_when_ec", is_BBL_multi_extruder, 256 + extruder_idx); - toggle_line("retraction_distances_when_ec", is_BBL_multi_extruder && m_config->opt_bool("long_retractions_when_ec", extruder_idx), 256 + extruder_idx); + toggle_line("long_retractions_when_ec", is_BBL_multi_extruder, 256 + variant_index); + toggle_line("retraction_distances_when_ec", is_BBL_multi_extruder && m_config->opt_bool("long_retractions_when_ec", variant_index), 256 + variant_index); } } diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index fa8a7ae68b..98428b148e 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -616,6 +616,8 @@ private: void add_filament_overrides_page(); void update_filament_overrides_page(const DynamicPrintConfig* printers_config); void update_volumetric_flow_preset_hints(); + // The variant index the variant switch shows, 0 without one. + unsigned int selected_variant_index() const; std::map m_overrides_options; diff --git a/src/slic3r/GUI/TextureLibrary.cpp b/src/slic3r/GUI/TextureLibrary.cpp new file mode 100644 index 0000000000..7cbd50a240 --- /dev/null +++ b/src/slic3r/GUI/TextureLibrary.cpp @@ -0,0 +1,373 @@ +#include "TextureLibrary.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include "libslic3r/PNGReadWrite.hpp" +#include "libslic3r/Utils.hpp" + +#include "libslic3r/AppConfig.hpp" + +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/I18N.hpp" + +namespace Slic3r::GUI { + +namespace { + +// Extensions the picker will list. The shipped folder only ever contains .png; the rest are here +// so a user who drops a .jpg straight into their own folder still sees it (load_texture_image_data() +// converts anything it can open). +bool is_image_file(const boost::filesystem::path &path) +{ + std::string ext = path.extension().string(); + boost::algorithm::to_lower(ext); + return ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp"; +} + +void scan_dir(const boost::filesystem::path &dir, bool is_user, std::vector &out) +{ + boost::system::error_code ec; + if (!boost::filesystem::is_directory(dir, ec)) + return; + + const size_t first = out.size(); + for (boost::filesystem::directory_iterator it(dir, ec), end; it != end && !ec; it.increment(ec)) { + if (!boost::filesystem::is_regular_file(it->path(), ec) || !is_image_file(it->path())) + continue; + out.push_back({ it->path().stem().string(), it->path().string(), is_user }); + } + std::sort(out.begin() + first, out.end(), + [](const TextureLibraryEntry &a, const TextureLibraryEntry &b) { return a.name < b.name; }); +} + +// True if the image carries any colour at all, i.e. some pixel's channels are not all equal. A +// photo saved as RGB but actually grey should still be stored as a grayscale PNG - it is a third of +// the bytes, and the colour feature keys off has_color(), so storing a grey image as RGB would offer +// the user a "colour" that is a row of identical greys. +bool image_has_color(const wxImage &image) +{ + const unsigned char *rgb = image.GetData(); + if (rgb == nullptr) + return false; + const size_t n = size_t(image.GetWidth()) * size_t(image.GetHeight()); + for (size_t i = 0; i < n; ++i) + if (rgb[i * 3] != rgb[i * 3 + 1] || rgb[i * 3 + 1] != rgb[i * 3 + 2]) + return true; + return false; +} + +// Colour images are stored as-is (as an RGB PNG) so the texture can colour the model as well as +// displace it; decode_height_texture() takes the height from their luminance, with the same +// coefficients ConvertToGreyscale() uses, so the relief is identical either way. wxImage writes the +// PNG here rather than Slic3r::png, which only encodes grayscale. +bool encode_color_png_bytes(const wxImage &image, std::vector &out, std::string &error) +{ + if (image.GetWidth() <= 0 || image.GetHeight() <= 0) { + error = _u8L("The selected image is empty."); + return false; + } + wxMemoryOutputStream stream; + // Alpha would be lost on the way into DecodedHeightTexture anyway, and a partly transparent + // texture reading as black relief is worse than reading as its own colour over the background. + wxImage opaque = image; + if (opaque.HasAlpha()) + opaque.ClearAlpha(); + if (!opaque.SaveFile(stream, wxBITMAP_TYPE_PNG)) { + error = _u8L("Failed to prepare the texture for use."); + return false; + } + const size_t size = size_t(stream.GetLength()); + out.resize(size); + stream.CopyTo(out.data(), size); + if (out.empty()) { + error = _u8L("Failed to prepare the texture for use."); + return false; + } + return true; +} + +// Slic3r::png only writes PNGs to a file, so the encode round-trips through a temp file rather than +// staying in memory. It happens once per import / per texture pick, not per frame, so the I/O is +// not worth avoiding with a second PNG encoder. +bool encode_gray_png_bytes(const wxImage &image, std::vector &out, std::string &error) +{ + const wxImage gray = image.ConvertToGreyscale(); + const int w = gray.GetWidth(); + const int h = gray.GetHeight(); + if (w <= 0 || h <= 0) { + error = _u8L("The selected image is empty."); + return false; + } + + // wxImage always stores 3 bytes per pixel; after ConvertToGreyscale the three are equal. + std::vector pixels(size_t(w) * size_t(h)); + const unsigned char *rgb = gray.GetData(); + for (size_t i = 0; i < pixels.size(); ++i) + pixels[i] = rgb[i * 3]; + + const boost::filesystem::path tmp = boost::filesystem::temp_directory_path() + / boost::filesystem::unique_path("orca_texdisp_%%%%%%%%.png"); + if (!Slic3r::png::write_gray_to_file(tmp.string(), size_t(w), size_t(h), pixels)) { + error = _u8L("Failed to prepare the texture for use."); + return false; + } + + { + std::ifstream ifs(tmp.string(), std::ios::binary); + out.assign(std::istreambuf_iterator(ifs), std::istreambuf_iterator()); + } + boost::system::error_code ec; + boost::filesystem::remove(tmp, ec); + + if (out.empty()) { + error = _u8L("Failed to prepare the texture for use."); + return false; + } + return true; +} + +// Grayscale in, grayscale out; colour in, colour out. +bool encode_png_bytes(const wxImage &image, std::vector &out, std::string &error) +{ + return image_has_color(image) ? encode_color_png_bytes(image, out, error) + : encode_gray_png_bytes(image, out, error); +} + +std::vector read_file_bytes(const std::string &path) +{ + std::ifstream ifs(path, std::ios::binary); + return std::vector(std::istreambuf_iterator(ifs), std::istreambuf_iterator()); +} + +// True if these bytes are a PNG libslic3r can decode, i.e. can be stored on a layer as-is. Mirrors +// exactly what decode_height_texture() accepts: 8-bit grayscale, or 8-bit colour (whose luminance is +// the height and whose RGB is available to colour the model). Anything else is converted on load. +bool is_supported_height_map(const std::vector &bytes) +{ + if (bytes.empty()) + return false; + const png::ReadBuf rbuf{ bytes.data(), bytes.size() }; + if (!png::is_png(rbuf)) + return false; + png::ImageGreyscale gray; + if (png::decode_png(rbuf, gray) && gray.cols > 0 && gray.rows > 0) + return true; + png::ImageColorscale color; + return png::decode_colored_png(rbuf, color) && color.cols > 0 && color.rows > 0 && + color.bytes_per_pixel >= 3 && + color.buf.size() == color.cols * color.rows * size_t(color.bytes_per_pixel); +} + +std::vector g_library; +bool g_library_scanned = false; + +// The user's own textures, most recently used first, kept in the app config. File names are stored rather +// than paths so the order survives a moved data directory; '/' separates them because no platform allows +// it inside a file name. Bounded so a long-lived config does not grow a line without end. +const char *const RECENT_SECTION = "texture_displacement"; +const char *const RECENT_KEY = "recent_textures"; +const size_t RECENT_MAX = 200; + +std::vector recent_texture_names() +{ + std::vector names; + if (wxGetApp().app_config == nullptr) + return names; + const std::string value = wxGetApp().app_config->get(RECENT_SECTION, RECENT_KEY); + for (size_t begin = 0; begin <= value.size();) { + const size_t end = std::min(value.find('/', begin), value.size()); + if (end > begin) + names.push_back(value.substr(begin, end - begin)); + begin = end + 1; + } + return names; +} + +void store_recent_texture_names(std::vector names) +{ + if (wxGetApp().app_config == nullptr) + return; + if (names.size() > RECENT_MAX) + names.resize(RECENT_MAX); + std::string value; + for (const std::string &name : names) + value += (value.empty() ? "" : "/") + name; + wxGetApp().app_config->set(RECENT_SECTION, RECENT_KEY, value); +} + +// Reorders the user's entries (the tail of `library`) by recent use. Stable, so textures never used keep +// the name order scan_dir() gave them, after every used one. +void sort_user_textures(std::vector &library) +{ + const std::vector names = recent_texture_names(); + std::unordered_map rank; + for (size_t i = 0; i < names.size(); ++i) + rank.emplace(names[i], int(i)); + const auto rank_of = [&rank](const TextureLibraryEntry &e) { + const auto it = rank.find(boost::filesystem::path(e.path).filename().string()); + return it == rank.end() ? std::numeric_limits::max() : it->second; + }; + const auto first_user = std::find_if(library.begin(), library.end(), [](const TextureLibraryEntry &e) { return e.is_user; }); + std::stable_sort(first_user, library.end(), + [&rank_of](const TextureLibraryEntry &a, const TextureLibraryEntry &b) { return rank_of(a) < rank_of(b); }); +} + +// True if `path` is a file directly inside user_texture_dir() - the only place textures may be removed +// from, or have their use recorded. +bool in_user_texture_dir(const std::string &path) +{ + const std::string dir = user_texture_dir(); + if (dir.empty()) + return false; + boost::system::error_code ec; + const bool same = boost::filesystem::equivalent(boost::filesystem::path(path).parent_path(), + boost::filesystem::path(dir), ec); + return !ec && same; +} + +} // namespace + +std::string user_texture_dir() +{ + const boost::filesystem::path dir = boost::filesystem::path(Slic3r::data_dir()) / "textures" / "displacement"; + boost::system::error_code ec; + boost::filesystem::create_directories(dir, ec); + if (ec) { + BOOST_LOG_TRIVIAL(error) << "Could not create the user texture directory " << dir.string() << ": " << ec.message(); + return {}; + } + return dir.string(); +} + +const std::vector &texture_library(bool force_rescan) +{ + if (g_library_scanned && !force_rescan) + return g_library; + + g_library.clear(); + scan_dir(boost::filesystem::path(Slic3r::resources_dir()) / "textures" / "displacement", false, g_library); + const std::string user_dir = user_texture_dir(); + if (!user_dir.empty()) + scan_dir(boost::filesystem::path(user_dir), true, g_library); + sort_user_textures(g_library); + + g_library_scanned = true; + return g_library; +} + +void touch_user_texture(const std::string &path) +{ + if (!in_user_texture_dir(path)) + return; + const std::string name = boost::filesystem::path(path).filename().string(); + std::vector names = recent_texture_names(); + names.erase(std::remove(names.begin(), names.end(), name), names.end()); + names.insert(names.begin(), name); + store_recent_texture_names(std::move(names)); + if (g_library_scanned) + sort_user_textures(g_library); +} + +bool remove_user_texture(const std::string &path, std::string &error) +{ + if (!in_user_texture_dir(path)) { + error = _u8L("Only your own imported textures can be removed."); + return false; + } + boost::system::error_code ec; + boost::filesystem::remove(boost::filesystem::path(path), ec); + if (ec) { + BOOST_LOG_TRIVIAL(error) << "Could not delete texture " << path << ": " << ec.message(); + error = _u8L("Could not delete the texture file."); + return false; + } + + const std::string name = boost::filesystem::path(path).filename().string(); + std::vector names = recent_texture_names(); + names.erase(std::remove(names.begin(), names.end(), name), names.end()); + store_recent_texture_names(std::move(names)); + texture_library(true); // drop it from the list + return true; +} + +std::optional import_texture_to_library(const std::string &source_path, std::string &error) +{ + const std::string user_dir = user_texture_dir(); + if (user_dir.empty()) { + error = _u8L("Could not create the folder for imported textures."); + return std::nullopt; + } + + wxImage image; + if (!image.LoadFile(from_u8(source_path)) || !image.IsOk()) { + error = _u8L("Could not load the selected image."); + return std::nullopt; + } + + std::vector bytes; + if (!encode_png_bytes(image, bytes, error)) + return std::nullopt; + + // Never overwrite an existing texture (the user's or, if they picked the same name twice, their + // own earlier import) - uniquify instead. + const std::string stem = boost::filesystem::path(source_path).stem().string(); + boost::filesystem::path dest = boost::filesystem::path(user_dir) / (stem + ".png"); + for (int i = 2; boost::filesystem::exists(dest); ++i) + dest = boost::filesystem::path(user_dir) / (stem + " (" + std::to_string(i) + ").png"); + + { + boost::nowide::ofstream ofs(dest.string(), std::ios::binary); + ofs.write(reinterpret_cast(bytes.data()), std::streamsize(bytes.size())); + if (!ofs.good()) { + error = _u8L("Failed to save the imported texture."); + return std::nullopt; + } + } + + texture_library(true); // pick the new file up + const std::string dest_str = dest.string(); + for (const TextureLibraryEntry &e : g_library) + if (e.path == dest_str) + return e; + + error = _u8L("Failed to save the imported texture."); + return std::nullopt; +} + +std::shared_ptr> load_texture_image_data(const std::string &path, std::string &error) +{ + std::vector bytes = read_file_bytes(path); + if (bytes.empty()) { + error = _u8L("Could not read the texture file."); + return nullptr; + } + if (is_supported_height_map(bytes)) + return std::make_shared>(std::move(bytes)); + + // Not a PNG at all (a jpg somebody copied into the folder by hand, say): convert it the same way + // an import would - keeping its colour if it has any - but leave the file on disk alone. + wxImage image; + if (!image.LoadFile(from_u8(path)) || !image.IsOk()) { + error = _u8L("Could not load the selected image."); + return nullptr; + } + std::vector converted; + if (!encode_png_bytes(image, converted, error)) + return nullptr; + return std::make_shared>(std::move(converted)); +} + +} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/TextureLibrary.hpp b/src/slic3r/GUI/TextureLibrary.hpp new file mode 100644 index 0000000000..65b35bcea8 --- /dev/null +++ b/src/slic3r/GUI/TextureLibrary.hpp @@ -0,0 +1,62 @@ +#ifndef slic3r_TextureLibrary_hpp_ +#define slic3r_TextureLibrary_hpp_ + +#include +#include +#include +#include + +namespace Slic3r::GUI { + +// One selectable height-map texture in the texture-displacement gizmo's texture picker. +struct TextureLibraryEntry +{ + std::string name; // display name (the file's stem, e.g. "Wood Grain") + std::string path; // absolute path on disk + bool is_user; // imported by the user, as opposed to shipped with OrcaSlicer +}; + +// Every available height-map texture: the ones shipped in resources/textures/displacement first, sorted +// by name, then the user's own from /textures/displacement - most recently used first (see +// touch_user_texture()), then any never used, by name. +// +// The two live in separate directories deliberately: an app update replaces the resources tree +// wholesale, so anything the user imported has to sit somewhere that update can never overwrite or +// delete. `is_user` is what the picker uses to show them under separate headings. +// +// Scanned once and cached. Pass force_rescan after an import, or to pick up a file the user dropped +// into either folder by hand while the app was running. +const std::vector &texture_library(bool force_rescan = false); + +// Moves one of the user's own textures to the front of the recently-used order texture_library() lists +// them in, and stores that order in the app config so it survives a restart. No-op for anything that is +// not in user_texture_dir(). +void touch_user_texture(const std::string &path); + +// Deletes one of the user's own imported textures from user_texture_dir() and forgets it. Anything outside +// that folder is refused, so a shipped texture can never be removed this way. Layers already using the +// texture are unaffected: they hold the image bytes themselves, and a project stores those. Returns false +// with `error` set on failure. +bool remove_user_texture(const std::string &path, std::string &error); + +// /textures/displacement, created if it does not exist yet. Empty string on failure. +std::string user_texture_dir(); + +// Reads any image format wxWidgets can open, converts it to the 8-bit grayscale PNG that +// libslic3r's decode_height_texture() understands, and saves it into user_texture_dir() (uniquified +// if that name is taken). The conversion has to happen here rather than in libslic3r, which has no +// image toolkit and so only ever handles the one already-normalized format. +// +// Returns the newly imported entry, or nullopt with `error` set. `source_path` is only read. +std::optional import_texture_to_library(const std::string &source_path, std::string &error); + +// Encoded bytes of `path`, ready to hand to TextureDisplacementLayer::image_data. Files already in +// the supported 8-bit grayscale PNG form (everything in the two library folders, by construction) +// are passed through verbatim; anything else - e.g. a colour PNG the user copied into the folder +// by hand - is converted on the fly, so a valid image never silently produces a blank layer. +// Returns nullptr with `error` set if the file cannot be read or decoded at all. +std::shared_ptr> load_texture_image_data(const std::string &path, std::string &error); + +} // namespace Slic3r::GUI + +#endif // slic3r_TextureLibrary_hpp_ diff --git a/src/slic3r/GUI/TextureProjectorFrame.cpp b/src/slic3r/GUI/TextureProjectorFrame.cpp new file mode 100644 index 0000000000..cfa29967ad --- /dev/null +++ b/src/slic3r/GUI/TextureProjectorFrame.cpp @@ -0,0 +1,97 @@ +#include "TextureProjectorFrame.hpp" + +#include + +#include +#include + +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/I18N.hpp" + +namespace Slic3r { namespace GUI { + +TextureProjectorFrame::TextureProjectorFrame(wxWindow *parent) + : wxFrame(parent, wxID_ANY, _L("Projection frame - drag over the model, then Apply"), wxDefaultPosition, + wxSize(360, 360), + // Caption and resize border so moving and sizing are the native gestures the user + // already knows - "align it by moving the window" only works if the window moves the + // ordinary way. FLOAT_ON_PARENT keeps it above the 3D view without the antisocial + // always-on-top-of-everything behaviour of wxSTAY_ON_TOP. + wxCAPTION | wxRESIZE_BORDER | wxCLOSE_BOX | wxFRAME_NO_TASKBAR | wxFRAME_FLOAT_ON_PARENT) +{ + SetBackgroundStyle(wxBG_STYLE_PAINT); + Bind(wxEVT_PAINT, &TextureProjectorFrame::on_paint, this); + // A resize changes the gate, so the texture has to be re-stretched under it. + Bind(wxEVT_SIZE, [this](wxSizeEvent &evt) { Refresh(); evt.Skip(); }); + SetTransparent(wxByte(m_alpha)); + + // Hide rather than destroy: the gizmo owns this window's lifetime, and reopening should keep the + // frame exactly where it was left - its position is the placement. + Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent &evt) { + if (evt.CanVeto()) { + evt.Veto(); + Hide(); + } else + evt.Skip(); + }); +} + +void TextureProjectorFrame::set_texture(const std::vector &gray, int width, int height) +{ + if (width <= 0 || height <= 0 || gray.size() < size_t(width) * size_t(height)) { + m_bitmap = wxBitmap(); + Refresh(); + return; + } + + wxImage img(width, height); + unsigned char *dst = img.GetData(); + for (size_t i = 0, n = size_t(width) * size_t(height); i < n; ++i) { + const unsigned char v = gray[i]; + dst[i * 3 + 0] = v; + dst[i * 3 + 1] = v; + dst[i * 3 + 2] = v; + } + m_bitmap = wxBitmap(img); + Refresh(); +} + +void TextureProjectorFrame::set_opacity(int alpha) +{ + m_alpha = std::clamp(alpha, 20, 255); + SetTransparent(wxByte(m_alpha)); + Refresh(); +} + +wxRect TextureProjectorFrame::client_rect_on_screen() const +{ + const wxSize sz = GetClientSize(); + return wxRect(ClientToScreen(wxPoint(0, 0)), sz); +} + +void TextureProjectorFrame::on_paint(wxPaintEvent &) +{ + wxPaintDC dc(this); + const wxSize sz = GetClientSize(); + if (sz.x <= 0 || sz.y <= 0) + return; + + dc.SetBackground(wxBrush(wxColour(20, 20, 20))); + dc.Clear(); + + if (m_bitmap.IsOk()) { + // Stretched to fill the client area rather than kept at its own aspect: the gate maps to the + // uv unit square whatever its shape, so a non-square window genuinely does project a + // stretched texture. Showing it any other way would misrepresent the bake. + wxImage scaled = m_bitmap.ConvertToImage().Scale(sz.x, sz.y, wxIMAGE_QUALITY_NORMAL); + dc.DrawBitmap(wxBitmap(scaled), 0, 0, false); + } + + // The border is the projection's hard edge, so it is drawn explicitly - with the window + // translucent, the native frame alone reads poorly against a busy 3D scene. + dc.SetPen(wxPen(wxColour(0, 200, 180), 2)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRectangle(0, 0, sz.x, sz.y); +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/TextureProjectorFrame.hpp b/src/slic3r/GUI/TextureProjectorFrame.hpp new file mode 100644 index 0000000000..784062680d --- /dev/null +++ b/src/slic3r/GUI/TextureProjectorFrame.hpp @@ -0,0 +1,56 @@ +#ifndef slic3r_TextureProjectorFrame_hpp_ +#define slic3r_TextureProjectorFrame_hpp_ + +// The projection-frame overlay for TextureProjectionMethod::ViewProjected. +// +// A semi-transparent, resizable window that the user drags over the 3D view like a slide projector's +// gate: whatever the model shows through this window is what the texture is projected onto, and the +// window's border is the hard edge of the projection. "Apply" then reads the window's client +// rectangle, converts it into the 3D canvas's own pixel space, and builds an exact projective map +// from it (see GLGizmoTextureDisplacement::apply_projection_frame()). +// +// The window itself is deliberately dumb - it owns no placement state and reports nothing +// continuously. Its position and size *are* the placement, and they are read on demand at Apply, +// which is also when the (expensive) visible-facet selection runs. Moving the window is therefore +// free, and nothing recomputes until the user asks for it. +// +// Plain 2D (wxGraphicsContext), not a wxGLCanvas: a second GL canvas would have to share the app's +// one real wxGLContext, the cause of bugs #10 and #14 in TEXTURE_DISPLACEMENT.md. A paint-DC window +// has no such failure mode, and this one only ever draws a bitmap and a border. + +#include + +#include +#include + +namespace Slic3r { namespace GUI { + +class TextureProjectorFrame : public wxFrame +{ +public: + explicit TextureProjectorFrame(wxWindow *parent); + + // The same 8-bit grayscale pixels build_texture_displacement() samples, so what is aligned here + // is what gets baked. Pass width/height <= 0 to clear it. + void set_texture(const std::vector &grayscale_pixels, int width, int height); + + // Whole-window opacity, 0..255. Low enough to see the model through it, high enough to judge + // where the texture lands - the useful range is roughly 60..200. + void set_opacity(int alpha); + int opacity() const { return m_alpha; } + + // The client area (the gate itself, excluding caption and borders) in screen coordinates. This + // is what the projection is built from, so it deliberately excludes the window decorations - + // the user aligns what they see, which is the client area. + wxRect client_rect_on_screen() const; + +private: + wxBitmap m_bitmap; + int m_alpha = 140; + + void on_paint(wxPaintEvent &); +}; + +}} // namespace Slic3r::GUI + +#endif // slic3r_TextureProjectorFrame_hpp_ diff --git a/src/slic3r/GUI/UVEditorCanvas.cpp b/src/slic3r/GUI/UVEditorCanvas.cpp new file mode 100644 index 0000000000..dd0432bc95 --- /dev/null +++ b/src/slic3r/GUI/UVEditorCanvas.cpp @@ -0,0 +1,2150 @@ +#include "UVEditorCanvas.hpp" + +#include +#include +#include +#include + +#include + +#include +#include + +#include "3DScene.hpp" +#include "BitmapCache.hpp" +#include "GLShader.hpp" +#include "GUI_App.hpp" +#include "I18N.hpp" +#include "OpenGLManager.hpp" +#include "Plater.hpp" +#include "wxExtensions.hpp" +#include "Widgets/CheckBox.hpp" +#include "Widgets/SpinInput.hpp" +#include "libslic3r/AppConfig.hpp" +#include "libslic3r/Utils.hpp" + +namespace Slic3r::GUI { + +namespace { +// Roughly Blender's UV/Image editor palette, which is what this pane is measured against. The UV_ +// prefix is not decoration: COLOR_BACKGROUND (and several other COLOR_*) are Win32 system-colour +// macros from WinUser.h, and an unprefixed name here expands to an integer literal mid-declaration. +const ColorRGBA UV_COLOR_BG = { 0.16f, 0.16f, 0.16f, 1.f }; +// The texture outside every island is washed towards the background, so the islands read as the lit areas +// whatever the texture looks like - a white brick texture no longer swallows a light outline. +const ColorRGBA UV_COLOR_OUTSIDE = { 0.16f, 0.16f, 0.16f, 0.72f }; +const ColorRGBA UV_COLOR_GRID = { 1.f, 1.f, 1.f, 0.08f }; +// Every stroke that must stay legible is drawn twice: a wider near-black halo, then the colour on top, so it +// holds up on light and dark texels alike. +const ColorRGBA UV_COLOR_HALO = { 0.06f, 0.06f, 0.07f, 1.f }; +const ColorRGBA UV_COLOR_TILE_OUTLINE = { 0.70f, 0.70f, 0.72f, 1.f }; +const ColorRGBA UV_COLOR_WIRE = { 1.f, 1.f, 1.f, 0.30f }; // interior edges, unselected +const ColorRGBA UV_COLOR_WIRE_SHADOW = { 0.f, 0.f, 0.f, 0.35f }; // 1 px offset under the wire +const ColorRGBA UV_COLOR_BOUNDARY = { 0.93f, 0.93f, 0.94f, 1.f }; +const ColorRGBA UV_COLOR_HOVER = { 0.72f, 0.98f, 0.93f, 1.f }; // what a click would grab +const ColorRGBA UV_COLOR_HOVER_FILL = { 1.f, 1.f, 1.f, 0.12f }; +// No wash on unselected islands: the texture inside them is exactly what gets baked, so it shows unaltered. +const ColorRGBA UV_COLOR_FILL = { 1.f, 1.f, 1.f, 0.f }; +const ColorRGBA UV_COLOR_SEL_FILL = { 0.15f, 0.85f, 0.75f, 0.24f }; +const ColorRGBA UV_COLOR_SEL_WIRE = { 0.55f, 1.f, 0.92f, 0.55f }; +const ColorRGBA UV_COLOR_SEL_BOUNDARY = { 0.16f, 0.90f, 0.78f, 1.f }; // the app teal, brightened to read on texture +const ColorRGBA UV_COLOR_DIAL = { 1.f, 0.85f, 0.2f, 0.9f }; // rotation protractor (#11) + +constexpr float SNAP_PIXELS = 28.f; // how close a boundary vertex has to come before it sticks (#2) + +// wxWidgets reports this canvas' size in *logical* points, while the GL drawable behind it is sized +// in device pixels. On the backends where those differ under HiDPI (the same pair GLCanvas3D guards +// its RetinaHelper with) a viewport built straight from GetSize() covers only the bottom-left +// 1/scale of the drawable, which is exactly where the whole editor ended up drawn, shrunken. +// Mouse coordinates arrive in logical points, so only the viewport needs converting - every other +// GetSize() use here is compared against event coordinates and must stay logical. +wxSize gl_drawable_size(const wxWindow *win, const wxSize &logical_size) +{ +#if defined(__APPLE__) || defined(__WXGTK3__) + const double scale = (win != nullptr) ? win->GetContentScaleFactor() : 1.0; + if (scale > 0.0) + return wxSize(std::max(1, int(std::lround(logical_size.GetWidth() * scale))), + std::max(1, int(std::lround(logical_size.GetHeight() * scale)))); +#else + (void) win; +#endif + return wxSize(std::max(1, logical_size.GetWidth()), std::max(1, logical_size.GetHeight())); +} + +// The pixel format this canvas is created with has to match the one the app's single shared +// wxGLContext was created against (that of View3D's canvas, from OpenGLManager::create_wxglcanvas()), +// so this mirrors that attribute list *including its multisampling*: WGL requires the HDC passed to +// wglMakeCurrent() to have the same pixel format as the one the context was created with, and a +// differing sample count is a differing pixel format. Getting only the non-multisample half of this +// right still leaves SetCurrent() failing, which is silent - the canvas then just shows whatever +// was last in its backbuffer, i.e. nothing. +std::vector gl_attrib_list() +{ + int antialiasing_samples = 4; + if (const AppConfig *app_config = wxGetApp().app_config; app_config != nullptr) { + const std::string value = app_config->get(SETTING_OPENGL_AA_SAMPLES); + if (value == "0" || value == "2" || value == "4" || value == "8" || value == "16") + antialiasing_samples = ::atoi(value.c_str()); + } + // OpenGLManager's own auto-detection has already run by now (View3D is created before this + // canvas), so this only reads its verdict rather than re-detecting. + if (!OpenGLManager::can_multisample()) + antialiasing_samples = 0; + + return { + WX_GL_RGBA, + WX_GL_DOUBLEBUFFER, + WX_GL_MIN_RED, 8, + WX_GL_MIN_GREEN, 8, + WX_GL_MIN_BLUE, 8, + WX_GL_MIN_ALPHA, 8, + WX_GL_DEPTH_SIZE, 24, + WX_GL_STENCIL_SIZE, 8, + WX_GL_SAMPLE_BUFFERS, antialiasing_samples > 0 ? GL_TRUE : GL_FALSE, + WX_GL_SAMPLES, antialiasing_samples, + 0 + }; +} + +// Wide lines are only *required* to be supported in a compatibility profile; a core-profile driver is +// allowed to reject anything but 1.0 with GL_INVALID_VALUE. In practice every desktop driver we care +// about honours it, so ask and swallow the error rather than giving up thickness everywhere. +void set_line_width(float width) +{ + ::glLineWidth(width); + ::glGetError(); +} + +uint64_t undirected_edge_key(int a, int b) +{ + if (a > b) + std::swap(a, b); + return (uint64_t(uint32_t(a)) << 32) | uint32_t(b); +} + +// Signed-area test, so it works whichever way round the triangle is wound. +bool point_in_triangle(const Vec2f &p, const Vec2f &a, const Vec2f &b, const Vec2f &c) +{ + const auto cross = [](const Vec2f &u, const Vec2f &v) { return u.x() * v.y() - u.y() * v.x(); }; + const float d0 = cross(b - a, p - a); + const float d1 = cross(c - b, p - b); + const float d2 = cross(a - c, p - c); + const bool has_neg = (d0 < 0.f) || (d1 < 0.f) || (d2 < 0.f); + const bool has_pos = (d0 > 0.f) || (d1 > 0.f) || (d2 > 0.f); + return !(has_neg && has_pos); +} + +// Shortest signed difference between two angles, so a rotation gesture crossing +/-pi doesn't jump. +float angle_delta(float from, float to) +{ + float d = to - from; + while (d > float(M_PI)) + d -= 2.f * float(M_PI); + while (d < -float(M_PI)) + d += 2.f * float(M_PI); + return d; +} + +// A 2D affine as the 4x4 the "flat" shader's view_model_matrix wants. +Transform3d to_transform3d(const UVEditorCanvas::IslandTransform &m) +{ + Transform3d t = Transform3d::Identity(); + t(0, 0) = m(0, 0); t(0, 1) = m(0, 1); t(0, 3) = m(0, 2); + t(1, 0) = m(1, 0); t(1, 1) = m(1, 1); t(1, 3) = m(1, 2); + return t; +} +} // namespace + +UVEditorCanvas::UVEditorCanvas(wxWindow *parent) + : wxGLCanvas(parent, wxID_ANY, gl_attrib_list().data(), wxDefaultPosition, wxDefaultSize, wxWANTS_CHARS) +{ + // The GL canvas paints its entire surface, so background erasing is unnecessary (and would + // otherwise race with our own rendering) - same setup as OpenGLManager::create_wxglcanvas(). + SetBackgroundStyle(wxBG_STYLE_PAINT); + + // Shares the app's one real GL context (see class comment) rather than creating an + // independent one - this is the same call View3D/Preview/AssembleView make in + // GUI_Preview.cpp, and is what lets this canvas reuse wxGetApp().get_shader(...) and GLModel. + m_context = wxGetApp().init_glcontext(*this); + + Bind(wxEVT_PAINT, &UVEditorCanvas::on_paint, this); + Bind(wxEVT_SIZE, &UVEditorCanvas::on_size, this); + Bind(wxEVT_LEFT_DOWN, &UVEditorCanvas::on_mouse, this); + Bind(wxEVT_LEFT_UP, &UVEditorCanvas::on_mouse, this); + Bind(wxEVT_RIGHT_DOWN, &UVEditorCanvas::on_mouse, this); + Bind(wxEVT_RIGHT_UP, &UVEditorCanvas::on_mouse, this); + Bind(wxEVT_MIDDLE_DOWN, &UVEditorCanvas::on_mouse, this); + Bind(wxEVT_MIDDLE_UP, &UVEditorCanvas::on_mouse, this); + Bind(wxEVT_MOTION, &UVEditorCanvas::on_mouse, this); + Bind(wxEVT_MOUSEWHEEL, &UVEditorCanvas::on_mouse, this); + Bind(wxEVT_LEAVE_WINDOW, &UVEditorCanvas::on_leave, this); + Bind(wxEVT_KEY_DOWN, &UVEditorCanvas::on_key, this); + Bind(wxEVT_ERASE_BACKGROUND, &UVEditorCanvas::on_erase_background, this); +} + +// NOTE (applies to set_background_texture() too): these are called from the texture-displacement +// gizmo's ImGui panel, i.e. from *inside* the main 3D canvas's own render pass. They must therefore +// only ever mark state dirty and schedule a repaint - rendering inline from here would make this +// canvas's GL surface current in the middle of the 3D canvas's frame (and did: it was also called +// while this pane was still hidden, where wxGLCanvas::SetCurrent() refuses to switch at all and the +// GL calls that followed simply landed on the 3D canvas instead). +void UVEditorCanvas::set_islands(Islands islands) +{ + // Only frame the view when a patch first appears, not on every stroke that extends one - the + // latter would keep yanking the view out from under a user who has panned or zoomed. + m_needs_fit |= m_islands.indices.empty(); + // Re-uploads that keep the same island/vertex count are just a refresh (e.g. a committed vertex edit + // re-applied), not a re-segmentation, so the selection and the picked sub-element stay valid and are + // worth keeping. A change in either count means the charts were renumbered and both are meaningless. + const bool same_structure = m_islands.island_count == islands.island_count && + m_islands.uvs.size() == islands.uvs.size(); + m_islands = std::move(islands); + if (m_selected_island >= m_islands.island_count) + m_selected_island = -1; + if (!same_structure) { + // A fresh unwrap renumbers charts, so any previous multi-selection is meaningless: drop it, then + // keep the primary (if still valid) as a single-island selection so the highlight is consistent. + m_selection.clear(); + if (m_selected_island >= 0) + m_selection.push_back(m_selected_island); + // Vertex/edge picks index into the old unwrap; drop them too. + m_active_vertex = -1; + m_active_edge = { -1, -1 }; + m_sel_vertices.clear(); + m_sel_edges.clear(); + m_hover_island = m_hover_vertex = -1; + m_hover_edge = { -1, -1 }; + } + + // Boundary vertices and edges, bucketed per island: the vertices for snapping, the edges for the outline. + const size_t n_islands = size_t(std::max(m_islands.island_count, 0)); + m_island_boundary_verts.assign(n_islands, {}); + m_island_boundary_edges.assign(n_islands, {}); + std::vector seen(m_islands.uvs.size(), false); + for (const auto &[a, b] : m_islands.boundary_edges) { + if (a < 0 || b < 0 || size_t(a) >= m_islands.uvs.size() || size_t(b) >= m_islands.uvs.size()) + continue; + const int island = m_islands.vertex_island[size_t(a)]; + if (island < 0 || size_t(island) >= n_islands) + continue; + m_island_boundary_edges[size_t(island)].emplace_back(a, b); + for (const int v : { a, b }) + if (!seen[size_t(v)]) { + seen[size_t(v)] = true; + m_island_boundary_verts[size_t(island)].push_back(v); + } + } + + // Triangles and raw bounds per island, for picking. + m_island_tris.assign(n_islands, {}); + m_island_raw_bounds.assign(n_islands, { Vec2f::Constant(std::numeric_limits::max()), + Vec2f::Constant(std::numeric_limits::lowest()) }); + for (size_t t = 0; t < m_islands.indices.size(); ++t) { + const Vec3i32 &tri = m_islands.indices[t]; + if (tri.minCoeff() < 0 || size_t(tri.maxCoeff()) >= m_islands.uvs.size()) + continue; + const int island = m_islands.vertex_island[size_t(tri[0])]; + if (island < 0 || size_t(island) >= n_islands) + continue; + m_island_tris[size_t(island)].push_back(int(t)); + auto &[lo, hi] = m_island_raw_bounds[size_t(island)]; + for (int k = 0; k < 3; ++k) { + lo = lo.cwiseMin(m_islands.uvs[size_t(tri[k])]); + hi = hi.cwiseMax(m_islands.uvs[size_t(tri[k])]); + } + } + + m_mesh_dirty = true; + m_background_quad_dirty = true; // the backdrop is sized to the unwrap, so it moved too + Refresh(); +} + +void UVEditorCanvas::set_island_transforms(std::vector transforms) +{ + m_transforms = std::move(transforms); + m_background_quad_dirty = true; // content_bounds() depends on where the islands ended up + Refresh(); +} + +void UVEditorCanvas::set_island_fill_colors(std::vector colors) +{ + m_island_fill_colors = std::move(colors); + Refresh(); +} + +void UVEditorCanvas::set_uv_transform(float tiling_scale, float rotation_deg, bool tile_enabled, bool tile_mirrored) +{ + m_tiling_scale = (std::abs(tiling_scale) > 1e-6f) ? tiling_scale : 1.f; + m_rotation_deg = rotation_deg; + m_tile_enabled = tile_enabled; + m_tile_mirrored = tile_mirrored; + m_background_quad_dirty = true; +} + +void UVEditorCanvas::set_background_texture(const std::vector &grayscale_pixels, int width, int height) +{ + if (width <= 0 || height <= 0 || grayscale_pixels.size() != size_t(width) * size_t(height)) { + m_background_width = m_background_height = 0; + m_background_pixels.clear(); + } else { + m_background_width = width; + m_background_height = height; + m_background_pixels.assign(size_t(width) * size_t(height) * 4, 255); + for (size_t i = 0; i < grayscale_pixels.size(); ++i) { + const unsigned char g = grayscale_pixels[i]; + m_background_pixels[i * 4 + 0] = g; + m_background_pixels[i * 4 + 1] = g; + m_background_pixels[i * 4 + 2] = g; + m_background_pixels[i * 4 + 3] = 255; + } + } + m_background_dirty = true; + Refresh(); +} + +void UVEditorCanvas::reset_view() +{ + m_needs_fit = true; + Refresh(); +} + +void UVEditorCanvas::run_command(Command cmd, float value) +{ + switch (cmd) { + case Command::FrameAll: reset_view(); return; + case Command::ToggleSnap: m_snap_enabled = !m_snap_enabled; update_status(); return; + case Command::SetSelectMode: + // Switched here straight away so the strip and the canvas agree at once; the gizmo is still told, + // because it is what keeps the mode when the canvas is next refreshed from the layer. + set_select_mode(static_cast(std::clamp(int(value), 0, 2))); + break; + default: break; + } + // Everything else needs the layer data the canvas doesn't hold; hand it to the gizmo. + if (m_on_command) + m_on_command(cmd, value); +} + +void UVEditorCanvas::set_pane_state(PaneState state) +{ + m_pane_state = std::move(state); + update_status(); + if (m_on_pane_state) + m_on_pane_state(m_pane_state); +} + +void UVEditorCanvas::update_status() +{ + if (!m_on_status) + return; + + wxString msg; + switch (m_gesture) { + case Gesture::MoveIsland: msg = _L("Moving island | release to drop, Esc to cancel"); break; + case Gesture::RotateIsland: + case Gesture::RotateIslandModal: + msg = wxString::Format(_L("Rotating island: %d° | Shift = snap 15°, click to confirm, Esc to cancel"), + int(std::lround(m_rot_display_deg))); + break; + case Gesture::ScaleIslandModal: msg = _L("Scaling island | click to confirm, Esc to cancel"); break; + case Gesture::MoveVertex: msg = _L("Moving vertex | release to drop"); break; + case Gesture::MoveEdge: msg = _L("Moving edge | release to drop"); break; + case Gesture::Pan: msg = _L("Panning"); break; + case Gesture::None: + default: + if (!has_islands()) + msg = m_pane_state.has_layer ? _L("Paint the area on the model, then press Unwrap") : + _L("Pick Unwrap as a texture layer's mapping to edit its UVs here"); + else if (m_select_mode == SelectMode::Vertex) + msg = m_sel_vertices.size() > 1 ? + wxString::Format(_L("%d vertices selected | drag = move together, Shift/Ctrl click = add/remove"), + int(m_sel_vertices.size())) : + _L("Vertex mode: drag a vertex to reshape | Shift/Ctrl click = multi-select, wheel = zoom, Home = frame"); + else if (m_select_mode == SelectMode::Edge) + msg = m_sel_edges.size() > 1 ? + wxString::Format(_L("%d edges selected | drag = move together, Shift/Ctrl click = add/remove"), + int(m_sel_edges.size())) : + _L("Edge mode: drag an island edge to reshape | Shift/Ctrl click = multi-select, wheel = zoom, Home = frame"); + else if (m_selection.size() > 1) + msg = wxString::Format(_L("%d islands selected | drag = move together, Shift/Ctrl click = add/remove, R/S = rotate/scale primary"), + int(m_selection.size())); + else if (m_selected_island >= 0) + msg = wxString::Format(_L("Island %d selected | drag = move, R = rotate, S = scale, Shift/Ctrl click = multi-select, Home = frame"), + m_selected_island + 1); + else + msg = _L("Click an island to select | Shift/Ctrl click = multi-select, wheel = zoom, middle-drag = pan, Home = frame all"); + if (m_snap_enabled) + msg += _L(" | snap ON"); + break; + } + m_on_status(msg); +} + +Vec2f UVEditorCanvas::island_uv(size_t vertex) const +{ + const Vec2f &raw = m_islands.uvs[vertex]; + const int island = m_islands.vertex_island[vertex]; + if (island < 0 || size_t(island) >= m_transforms.size()) + return raw; + const IslandTransform &m = m_transforms[size_t(island)]; + return m.block<2, 2>(0, 0) * raw + m.col(2); +} + +void UVEditorCanvas::content_bounds(Vec2f &min_uv, Vec2f &max_uv) const +{ + // Always include the texture's first tile, so an unwrap that happens to be tiny, or absent, + // still leaves something sensibly framed on screen. + min_uv = Vec2f(0.f, 0.f); + max_uv = Vec2f(1.f, 1.f); + for (size_t i = 0; i < m_islands.uvs.size(); ++i) { + const Vec2f uv = island_uv(i); + min_uv = min_uv.cwiseMin(uv); + max_uv = max_uv.cwiseMax(uv); + } +} + +void UVEditorCanvas::framed_bounds(Vec2f &min_uv, Vec2f &max_uv) const +{ + content_bounds(min_uv, max_uv); + // With tiling on, the backdrop is snapped out to whole tiles, so it is bigger than the raw + // bounds - and by a different amount on each side. Framing the raw bounds therefore left that + // backdrop visibly off-centre: hanging past one edge of the pane with dead space against the + // other. Frame what is drawn instead. (Tiling off draws only the first tile, which the bounds + // already contain, so there is nothing to snap.) + if (m_tile_enabled) { + min_uv = Vec2f(std::floor(min_uv.x()), std::floor(min_uv.y())); + max_uv = Vec2f(std::ceil(max_uv.x()), std::ceil(max_uv.y())); + } +} + +void UVEditorCanvas::fit_view_to_content() +{ + Vec2f min_uv, max_uv; + framed_bounds(min_uv, max_uv); + + const wxSize size = GetSize(); + const float aspect = float(std::max(1, size.GetWidth())) / float(std::max(1, size.GetHeight())); + const Vec2f half = 0.5f * (max_uv - min_uv); + + m_pan = 0.5f * (min_uv + max_uv); + // m_zoom is the half-extent shown across the *shorter* pane edge (see view_half_extents()), so + // each axis' required half-extent has to be converted back into that unit before the larger of + // the two is taken. Sizing off the bigger axis alone, as this did, ignores the pane's shape and + // zooms out further than either axis needs on anything but a square pane. The 1.1 leaves a + // margin so the outermost island edge is not flush against the frame. + m_zoom = std::max(1.1f * std::max(half.x() / std::max(aspect, 1.f), half.y() * std::min(aspect, 1.f)), + 0.05f); + m_needs_fit = false; +} + +void UVEditorCanvas::view_half_extents(float &half_w, float &half_h) const +{ + const wxSize size = GetSize(); + const float w = float(std::max(1, size.GetWidth())); + const float h = float(std::max(1, size.GetHeight())); + + // m_zoom is the half-extent visible across the *shorter* edge; the longer edge shows + // proportionally more. Both axes have to be driven off the same uniform scale, or the content + // comes out squashed on one of the two pane orientations. + const float aspect = w / h; + half_w = m_zoom * std::max(aspect, 1.f); + half_h = m_zoom * std::max(1.f / aspect, 1.f); +} + +Vec2f UVEditorCanvas::screen_to_uv(const wxPoint &px) const +{ + const wxSize size = GetSize(); + const float w = float(std::max(1, size.GetWidth())); + const float h = float(std::max(1, size.GetHeight())); + + float half_w, half_h; + view_half_extents(half_w, half_h); + + // Inverse of the projection in render(): x maps straight through, y is negated there so that v + // runs down the screen - which means screen-down and v-increasing agree, and this is a plain + // scale on both axes. + return Vec2f(m_pan.x() + (2.f * float(px.x) / w - 1.f) * half_w, + m_pan.y() + (2.f * float(px.y) / h - 1.f) * half_h); +} + +int UVEditorCanvas::island_at(const Vec2f &uv) const +{ + int found = -1; + for (int island = 0; island < int(m_island_tris.size()); ++island) { + // The raw bounds through the island's affine: the box of the four transformed corners contains the island. + const auto &[lo, hi] = m_island_raw_bounds[size_t(island)]; + if (lo.x() > hi.x()) + continue; + const IslandTransform m = size_t(island) < m_transforms.size() ? m_transforms[size_t(island)] : + IslandTransform(IslandTransform::Identity()); + Vec2f bmin = Vec2f::Constant(std::numeric_limits::max()), bmax = -bmin; + for (const Vec2f &corner : { lo, hi, Vec2f(lo.x(), hi.y()), Vec2f(hi.x(), lo.y()) }) { + const Vec2f p = m.block<2, 2>(0, 0) * corner + m.col(2); + bmin = bmin.cwiseMin(p); + bmax = bmax.cwiseMax(p); + } + if (uv.x() < bmin.x() || uv.y() < bmin.y() || uv.x() > bmax.x() || uv.y() > bmax.y()) + continue; + + for (const int t : m_island_tris[size_t(island)]) { + const Vec3i32 &tri = m_islands.indices[size_t(t)]; + if (!point_in_triangle(uv, island_uv(size_t(tri[0])), island_uv(size_t(tri[1])), island_uv(size_t(tri[2])))) + continue; + // Islands are allowed to overlap, so a point can be inside several. Keep whichever is already + // selected - otherwise a drag of a partly-covered island would be stolen mid-gesture by the + // one on top of it. + if (is_selected(island)) + return island; + found = island; + break; + } + } + return found; +} + +void UVEditorCanvas::update_hover(const wxPoint &pos) +{ + int island = -1, vertex = -1; + std::pair edge{ -1, -1 }; + if (has_islands()) { + const Vec2f uv = screen_to_uv(pos); + switch (m_select_mode) { + case SelectMode::Island: island = island_at(uv); break; + case SelectMode::Vertex: vertex = vertex_at(uv); break; + case SelectMode::Edge: edge = edge_at(uv); break; + } + } + if (island != m_hover_island || vertex != m_hover_vertex || edge != m_hover_edge) { + m_hover_island = island; + m_hover_vertex = vertex; + m_hover_edge = edge; + Refresh(); + } +} + +void UVEditorCanvas::set_select_mode(SelectMode mode) +{ + if (m_select_mode == mode) + return; + m_select_mode = mode; + m_active_vertex = -1; + m_active_edge = { -1, -1 }; + m_hover_island = m_hover_vertex = -1; + m_hover_edge = { -1, -1 }; + m_sel_vertices.clear(); + m_sel_edges.clear(); + m_gesture = Gesture::None; + update_status(); + Refresh(); +} + +int UVEditorCanvas::vertex_at(const Vec2f &uv) const +{ + // Screen-space threshold, so the pick feels the same at every zoom. + const wxSize size = GetSize(); + const float uv_per_px = 2.f * m_zoom / float(std::max(1, std::min(size.GetWidth(), size.GetHeight()))); + const float threshold = SNAP_PIXELS * uv_per_px; + + int best = -1; + float best_d = threshold * threshold; + for (size_t i = 0; i < m_islands.uvs.size(); ++i) { + const float d = (island_uv(i) - uv).squaredNorm(); + if (d < best_d) { + best_d = d; + best = int(i); + } + } + return best; +} + +std::pair UVEditorCanvas::edge_at(const Vec2f &uv) const +{ + const wxSize size = GetSize(); + const float uv_per_px = 2.f * m_zoom / float(std::max(1, std::min(size.GetWidth(), size.GetHeight()))); + const float threshold = SNAP_PIXELS * uv_per_px; + + // Point-to-segment distance in texture-UV space, against the island outlines (the edges the pane + // exists to show). Interior edges are left alone: the boundary is what a user reshapes. + const auto seg_dist_sq = [](const Vec2f &p, const Vec2f &a, const Vec2f &b) { + const Vec2f ab = b - a; + const float l2 = ab.squaredNorm(); + const float t = (l2 > 1e-12f) ? std::clamp((p - a).dot(ab) / l2, 0.f, 1.f) : 0.f; + return (p - (a + ab * t)).squaredNorm(); + }; + std::pair best{ -1, -1 }; + float best_d = threshold * threshold; + for (const auto &[a, b] : m_islands.boundary_edges) { + if (a < 0 || b < 0 || size_t(a) >= m_islands.uvs.size() || size_t(b) >= m_islands.uvs.size()) + continue; + const float d = seg_dist_sq(uv, island_uv(size_t(a)), island_uv(size_t(b))); + if (d < best_d) { + best_d = d; + best = { a, b }; + } + } + return best; +} + +void UVEditorCanvas::move_vertex_raw(int v, const Vec2f &delta_uv) +{ + if (v < 0 || size_t(v) >= m_islands.uvs.size()) + return; + const int island = m_islands.vertex_island[size_t(v)]; + // The gesture happens in texture-UV space; the stored coordinate is the raw unwrap. Undo the + // island's own linear map (which includes the layer's tiling scale + rotation) to get there. + Eigen::Matrix2f lin = Eigen::Matrix2f::Identity(); + if (island >= 0 && size_t(island) < m_transforms.size()) + lin = m_transforms[size_t(island)].block<2, 2>(0, 0); + const float det = lin.determinant(); + const Vec2f raw_delta = (std::abs(det) > 1e-12f) ? Vec2f(lin.inverse() * delta_uv) : delta_uv; + m_islands.uvs[size_t(v)] += raw_delta; + if (island >= 0 && size_t(island) < m_island_raw_bounds.size()) { // keep the pick box around the moved vertex + auto &[lo, hi] = m_island_raw_bounds[size_t(island)]; + lo = lo.cwiseMin(m_islands.uvs[size_t(v)]); + hi = hi.cwiseMax(m_islands.uvs[size_t(v)]); + } + m_mesh_dirty = true; // the edited raw uv is redrawn from rebuild_island_models() next frame +} + +float UVEditorCanvas::island_rotation_deg(int island) const +{ + // The rotation baked into the island's affine, in the same y-down UV convention the gesture uses. + // First column is scale*(cos, sin); its angle is the island's on-screen orientation. + if (island < 0 || size_t(island) >= m_transforms.size()) + return 0.f; + const IslandTransform &m = m_transforms[size_t(island)]; + return std::atan2(m(1, 0), m(0, 0)) * 180.f / float(M_PI); +} + +Vec2f UVEditorCanvas::island_centroid(int island) const +{ + Vec2f sum = Vec2f::Zero(); + int count = 0; + for (size_t i = 0; i < m_islands.uvs.size(); ++i) + if (m_islands.vertex_island[i] == island) { + sum += island_uv(i); + ++count; + } + return (count > 0) ? Vec2f(sum / float(count)) : Vec2f::Zero(); +} + +Vec2f UVEditorCanvas::uv_delta_to_unwrap(const Vec2f &delta_uv) const +{ + // apply_uv_transform() maps unwrap -> uv as uv = R(unwrap / tiling) + offset, so the inverse of + // a *delta* (the translation drops out) is unwrap = tiling * R^-1(delta_uv). + const float rad = m_rotation_deg * float(M_PI) / 180.f; + const float cs = std::cos(rad), sn = std::sin(rad); + return Vec2f(delta_uv.x() * cs + delta_uv.y() * sn, -delta_uv.x() * sn + delta_uv.y() * cs) * m_tiling_scale; +} + +Vec2f UVEditorCanvas::snap_correction(int island) const +{ + if (!m_snap_enabled || island < 0 || size_t(island) >= m_island_boundary_verts.size()) + return Vec2f::Zero(); + + // A screen-space threshold, so the magnet feels the same at every zoom level. + const wxSize size = GetSize(); + const float uv_per_px = 2.f * m_zoom / float(std::max(1, std::min(size.GetWidth(), size.GetHeight()))); + const float threshold = SNAP_PIXELS * uv_per_px; + + float best_dist_sq = threshold * threshold; + Vec2f best = Vec2f::Zero(); + for (const int mine : m_island_boundary_verts[size_t(island)]) { + const Vec2f a = island_uv(size_t(mine)); + for (size_t other = 0; other < m_island_boundary_verts.size(); ++other) { + if (int(other) == island) + continue; + for (const int theirs : m_island_boundary_verts[other]) { + const Vec2f d = island_uv(size_t(theirs)) - a; + const float dist_sq = d.squaredNorm(); + if (dist_sq < best_dist_sq) { + best_dist_sq = dist_sq; + best = d; + } + } + } + } + return best; +} + +std::vector UVEditorCanvas::selected_edge_endpoints() const +{ + std::vector verts; + for (const auto &[a, b] : m_sel_edges) + for (const int v : { a, b }) + if (v >= 0 && std::find(verts.begin(), verts.end(), v) == verts.end()) + verts.push_back(v); + return verts; +} + +void UVEditorCanvas::end_gesture() +{ + const bool was_editing = m_gesture == Gesture::MoveIsland || m_gesture == Gesture::RotateIsland || + m_gesture == Gesture::RotateIslandModal || m_gesture == Gesture::ScaleIslandModal; + + // Stick to a neighbour at the end of a move, rather than magnetically fighting the cursor + // throughout it - a snap that keeps re-applying mid-drag is very hard to pull *out* of. + if (m_gesture == Gesture::MoveIsland && m_on_island_edit) { + const Vec2f correction = snap_correction(m_selected_island); + if (!correction.isZero()) + m_on_island_edit(m_selected_island, uv_delta_to_unwrap(correction), 0.f, 1.f, false); + } + + if (was_editing && m_on_island_edit) + m_on_island_edit(m_selected_island, Vec2f::Zero(), 0.f, 1.f, /* finished */ true); + + // Commit a vertex/edge edit once, on release: hand the owner the affected unwrapped vertices and + // their new raw-unwrap coordinates so it can store the overrides and re-solve the bake preview. + if ((m_gesture == Gesture::MoveVertex || m_gesture == Gesture::MoveEdge) && m_vertex_edit_moved && + m_on_vertex_edit) { + std::vector> edits; + const auto add = [&](int v) { + if (v >= 0 && size_t(v) < m_islands.uvs.size()) + edits.emplace_back(v, m_islands.uvs[size_t(v)]); + }; + // Commit every element of the multi-selection, not just the primary, so a group drag stores all + // the moved vertices' overrides. Falls back to the primary if the selection is somehow empty. + if (m_gesture == Gesture::MoveVertex) { + if (m_sel_vertices.empty()) + add(m_active_vertex); + else + for (const int v : m_sel_vertices) + add(v); + } else { + const std::vector endpoints = selected_edge_endpoints(); + if (endpoints.empty()) { + add(m_active_edge.first); + add(m_active_edge.second); + } else + for (const int v : endpoints) + add(v); + } + if (!edits.empty()) + m_on_vertex_edit(edits); + } + + m_gesture = Gesture::None; + m_rot_raw_deg = 0.f; + m_rot_applied_deg = 0.f; + m_modal_scale_accum = 1.f; + if (HasCapture()) + ReleaseMouse(); +} + +void UVEditorCanvas::on_key(wxKeyEvent &evt) +{ + const int key = evt.GetKeyCode(); + + // Undo/redo while the pane has focus. Island edits already take a Plater snapshot per gesture (see + // GLGizmoTextureDisplacement::on_island_edited), so this just drives the same global history; the + // gizmo re-pushes the restored island transforms into the canvas on the reload that follows. + if (evt.ControlDown() && (key == 'Z' || key == 'z')) { + if (evt.ShiftDown()) wxGetApp().plater()->redo(); + else wxGetApp().plater()->undo(); + return; + } + if (evt.ControlDown() && (key == 'Y' || key == 'y')) { + wxGetApp().plater()->redo(); + return; + } + + if (m_gesture == Gesture::RotateIslandModal || m_gesture == Gesture::ScaleIslandModal) { + if (key == WXK_ESCAPE) { + // Put the island back exactly where the modal gesture found it, then finish. + if (m_on_island_edit) { + if (m_gesture == Gesture::RotateIslandModal && m_rot_applied_deg != 0.f) + m_on_island_edit(m_selected_island, Vec2f::Zero(), -m_rot_applied_deg, 1.f, false); + if (m_gesture == Gesture::ScaleIslandModal && m_modal_scale_accum != 1.f) + m_on_island_edit(m_selected_island, Vec2f::Zero(), 0.f, 1.f / m_modal_scale_accum, false); + } + end_gesture(); + Refresh(); + return; + } + if (key == WXK_RETURN || key == WXK_NUMPAD_ENTER) { + end_gesture(); + Refresh(); + return; + } + } + + // Frame everything. The unwrap is packed in mm and then divided by the layer's tile size, so it + // can easily sit tens of tiles away from the texture's first one - panning back by hand from + // there is hopeless, and without this there would be no way to reach reset_view() at all. + if (key == WXK_HOME || key == 'F' || key == 'f') { + reset_view(); + return; + } + + // Blender's modal transforms: R / S, then the transform follows the mouse until it is confirmed + // with a click or Enter, or abandoned with Esc. + if (m_selected_island >= 0 && m_select_mode == SelectMode::Island && m_gesture == Gesture::None && + (key == 'R' || key == 'r' || key == 'S' || key == 's')) { + const Vec2f uv = screen_to_uv(ScreenToClient(wxGetMousePosition())); + const Vec2f centre = island_centroid(m_selected_island); + const Vec2f rel = uv - centre; + if (key == 'R' || key == 'r') { + m_gesture = Gesture::RotateIslandModal; + m_rot_raw_deg = 0.f; + m_rot_applied_deg = 0.f; + m_rot_base_deg = island_rotation_deg(m_selected_island); + m_rot_display_deg = m_rot_base_deg; + m_gesture_last_angle = std::atan2(rel.y(), rel.x()); + } else { + m_gesture = Gesture::ScaleIslandModal; + m_modal_scale_accum = 1.f; + m_gesture_last_dist = std::max(rel.norm(), 1e-6f); + } + return; + } + + evt.Skip(); +} + +void UVEditorCanvas::on_mouse(wxMouseEvent &evt) +{ + const wxEventType type = evt.GetEventType(); + const wxPoint pos = evt.GetPosition(); + + // Track the pointer so the +/- add-remove hint can be drawn next to it in Vertex/Edge mode. + m_cursor_px = pos; + m_cursor_inside = true; + if (type == wxEVT_MOTION && m_gesture == Gesture::None) { + update_hover(pos); + if (m_select_mode != SelectMode::Island) + Refresh(); // animate the hint (and its +/- flip) as the pointer/modifiers move + } + + // Key events (R/S/Home) only arrive if this canvas has focus, and clicking it is the natural way + // to ask for it - the pane is not in the tab order. + if (type == wxEVT_LEFT_DOWN || type == wxEVT_RIGHT_DOWN || type == wxEVT_MIDDLE_DOWN) + SetFocus(); + + // A modal R/S is confirmed by any click, exactly as in Blender. + if ((m_gesture == Gesture::RotateIslandModal || m_gesture == Gesture::ScaleIslandModal) && + (type == wxEVT_LEFT_DOWN || type == wxEVT_RIGHT_DOWN)) { + end_gesture(); + Refresh(); + return; + } + + if (type == wxEVT_LEFT_DOWN) { + const Vec2f uv = screen_to_uv(pos); + m_drag_last_px = pos; + m_gesture_last_uv = uv; + if (m_select_mode == SelectMode::Vertex) { + const int hit = vertex_at(uv); + m_active_edge = { -1, -1 }; + m_vertex_edit_moved = false; + // Same Shift-adds / Ctrl-toggles / plain-replaces rules as island selection, so a group of + // vertices can be picked and dragged together. + if (hit >= 0) { + if (evt.ShiftDown()) { + if (!is_vertex_selected(hit)) + m_sel_vertices.push_back(hit); + } else if (evt.ControlDown()) { + if (auto it = std::find(m_sel_vertices.begin(), m_sel_vertices.end(), hit); it != m_sel_vertices.end()) + m_sel_vertices.erase(it); + else + m_sel_vertices.push_back(hit); + } else if (!is_vertex_selected(hit)) { + m_sel_vertices.assign(1, hit); + } + } else if (!evt.ShiftDown() && !evt.ControlDown()) { + m_sel_vertices.clear(); + } + // Primary = the clicked vertex only if it is (still) selected; a Ctrl-deselect just toggles. + m_active_vertex = (hit >= 0 && is_vertex_selected(hit)) ? hit : -1; + m_gesture = (m_active_vertex >= 0) ? Gesture::MoveVertex : Gesture::Pan; + } else if (m_select_mode == SelectMode::Edge) { + const std::pair hit = edge_at(uv); + m_active_vertex = -1; + m_vertex_edit_moved = false; + if (hit.first >= 0) { + if (evt.ShiftDown()) { + if (!is_edge_selected(hit)) + m_sel_edges.push_back(hit); + } else if (evt.ControlDown()) { + if (auto it = std::find(m_sel_edges.begin(), m_sel_edges.end(), hit); it != m_sel_edges.end()) + m_sel_edges.erase(it); + else + m_sel_edges.push_back(hit); + } else if (!is_edge_selected(hit)) { + m_sel_edges.assign(1, hit); + } + } else if (!evt.ShiftDown() && !evt.ControlDown()) { + m_sel_edges.clear(); + } + m_active_edge = (hit.first >= 0 && is_edge_selected(hit)) ? hit : std::pair{ -1, -1 }; + m_gesture = (m_active_edge.first >= 0) ? Gesture::MoveEdge : Gesture::Pan; + } else { + const int hit = island_at(uv); + if (hit >= 0) { + if (evt.ShiftDown()) { + // Shift adds to the selection (and makes the clicked one the new primary). + if (!is_selected(hit)) + m_selection.push_back(hit); + m_selected_island = hit; + } else if (evt.ControlDown()) { + // Ctrl toggles: clicking a selected island removes it (the requested "deselect"), + // clicking an unselected one adds it. + if (auto it = std::find(m_selection.begin(), m_selection.end(), hit); it != m_selection.end()) { + m_selection.erase(it); + m_selected_island = m_selection.empty() ? -1 : m_selection.back(); + } else { + m_selection.push_back(hit); + m_selected_island = hit; + } + } else { + // Plain click: keep the whole selection if the clicked island is already part of it (so + // a drag moves the group), otherwise collapse to just this one. + if (!is_selected(hit)) + m_selection.assign(1, hit); + m_selected_island = hit; + } + } else if (!evt.ShiftDown() && !evt.ControlDown()) { + // Clicking empty space with no modifier clears the selection and pans. + m_selection.clear(); + m_selected_island = -1; + } + m_gesture = (m_selected_island >= 0) ? Gesture::MoveIsland : Gesture::Pan; + } + CaptureMouse(); + Refresh(); + } else if (type == wxEVT_RIGHT_DOWN && m_selected_island >= 0 && m_select_mode == SelectMode::Island) { + const Vec2f rel = screen_to_uv(pos) - island_centroid(m_selected_island); + m_gesture = Gesture::RotateIsland; + m_rot_raw_deg = 0.f; + m_rot_applied_deg = 0.f; + m_rot_base_deg = island_rotation_deg(m_selected_island); + m_rot_display_deg = m_rot_base_deg; + m_gesture_last_angle = std::atan2(rel.y(), rel.x()); + CaptureMouse(); + } else if (type == wxEVT_MIDDLE_DOWN) { + m_gesture = Gesture::Pan; + m_drag_last_px = pos; + CaptureMouse(); + } else if (type == wxEVT_LEFT_UP || type == wxEVT_RIGHT_UP || type == wxEVT_MIDDLE_UP) { + if (m_gesture != Gesture::RotateIslandModal && m_gesture != Gesture::ScaleIslandModal) { + end_gesture(); + Refresh(); + } + } else if (type == wxEVT_MOTION) { + switch (m_gesture) { + case Gesture::Pan: { + const wxSize size = GetSize(); + const float scale = 2.f * m_zoom / float(std::max(1, std::min(size.GetWidth(), size.GetHeight()))); + // Both axes point the same way on screen as in UV space (v runs down), so dragging the + // content along with the cursor is a subtraction on both. + m_pan.x() -= float(pos.x - m_drag_last_px.x) * scale; + m_pan.y() -= float(pos.y - m_drag_last_px.y) * scale; + m_drag_last_px = pos; + Refresh(); + break; + } + case Gesture::MoveIsland: { + const Vec2f uv = screen_to_uv(pos); + if (m_on_island_edit) + m_on_island_edit(m_selected_island, uv_delta_to_unwrap(uv - m_gesture_last_uv), 0.f, 1.f, false); + m_gesture_last_uv = uv; + Refresh(); + break; + } + case Gesture::MoveVertex: { + const Vec2f uv = screen_to_uv(pos); + const Vec2f delta = uv - m_gesture_last_uv; + // Move the whole selection by the same uv-space delta (each vertex converts it through its + // own island transform in move_vertex_raw). Falls back to the primary if none is selected. + if (m_sel_vertices.empty()) + move_vertex_raw(m_active_vertex, delta); + else + for (const int v : m_sel_vertices) + move_vertex_raw(v, delta); + m_gesture_last_uv = uv; + m_vertex_edit_moved = true; + Refresh(); + break; + } + case Gesture::MoveEdge: { + const Vec2f uv = screen_to_uv(pos); + const Vec2f delta = uv - m_gesture_last_uv; + // Move every unique endpoint of every selected edge once. Falls back to the primary edge. + const std::vector endpoints = selected_edge_endpoints(); + if (endpoints.empty()) { + move_vertex_raw(m_active_edge.first, delta); + move_vertex_raw(m_active_edge.second, delta); + } else + for (const int v : endpoints) + move_vertex_raw(v, delta); + m_gesture_last_uv = uv; + m_vertex_edit_moved = true; + Refresh(); + break; + } + case Gesture::RotateIsland: + case Gesture::RotateIslandModal: { + const Vec2f rel = screen_to_uv(pos) - island_centroid(m_selected_island); + const float angle = std::atan2(rel.y(), rel.x()); + // Accumulate the raw mouse rotation incrementally so it survives crossing +/-180 degrees. + m_rot_raw_deg += angle_delta(m_gesture_last_angle, angle) * 180.f / float(M_PI); + m_gesture_last_angle = angle; + + // With Shift, quantise to *global* 15-degree marks (0/15/30...), i.e. snap the island's + // absolute on-screen orientation, not 15 degrees relative to wherever it started (#10) - + // snapping the target rather than each delta is what keeps it from juddering on a step. + constexpr float STEP = 15.f; + const float absolute = m_rot_base_deg + m_rot_raw_deg; + const float target_abs = evt.ShiftDown() ? std::round(absolute / STEP) * STEP : absolute; + const float deg = target_abs - (m_rot_base_deg + m_rot_applied_deg); + if (m_on_island_edit && deg != 0.f) + m_on_island_edit(m_selected_island, Vec2f::Zero(), deg, 1.f, false); + m_rot_applied_deg = target_abs - m_rot_base_deg; + m_rot_display_deg = target_abs; + Refresh(); + break; + } + case Gesture::ScaleIslandModal: { + const Vec2f rel = screen_to_uv(pos) - island_centroid(m_selected_island); + const float dist = std::max(rel.norm(), 1e-6f); + const float factor = dist / m_gesture_last_dist; + if (m_on_island_edit && factor != 1.f) + m_on_island_edit(m_selected_island, Vec2f::Zero(), 0.f, factor, false); + m_modal_scale_accum *= factor; + m_gesture_last_dist = dist; + Refresh(); + break; + } + default: break; + } + } else if (type == wxEVT_MOUSEWHEEL) { + // Zoom about the cursor, not the view centre - otherwise zooming in on something off to the + // side walks it straight out of the frame. + const Vec2f before = screen_to_uv(pos); + const float factor = std::pow(0.9f, float(evt.GetWheelRotation()) / float(evt.GetWheelDelta())); + m_zoom = std::clamp(m_zoom * factor, 0.001f, 5000.f); + const Vec2f after = screen_to_uv(pos); + m_pan += before - after; + if (m_gesture == Gesture::None) + update_hover(pos); + Refresh(); + } +} + +void UVEditorCanvas::rebuild_island_models() +{ + m_mesh_dirty = false; + m_island_wireframe.clear(); + m_island_fill.clear(); + + const int islands = std::max(m_islands.island_count, 0); + if (islands == 0 || m_islands.uvs.empty() || m_islands.indices.empty()) + return; + + m_island_wireframe.resize(size_t(islands)); + m_island_fill.resize(size_t(islands)); + + const int vertex_count = int(m_islands.uvs.size()); + + // Charts have disjoint vertex sets (compute_patch_unwrap() duplicates seam vertices per chart), + // so every vertex belongs to exactly one island and this partition is clean. + std::unordered_map is_boundary; + is_boundary.reserve(m_islands.boundary_edges.size() * 2); + for (const auto &[a, b] : m_islands.boundary_edges) + is_boundary[undirected_edge_key(a, b)] = true; + + // Named, not size_t(islands) inline: the latter is a most-vexing-parse and declares a function + // (a single identifier in the parens reads as a parameter name), which is what every + // "subscript requires array or pointer type" error on these vectors was. + const size_t n_islands = size_t(islands); + + // Global vertex index -> index within its own island's buffers. + std::vector local(m_islands.uvs.size(), -1); + std::vector> island_verts(n_islands); + for (size_t i = 0; i < m_islands.uvs.size(); ++i) { + const int island = m_islands.vertex_island[i]; + if (island < 0 || island >= islands) + continue; + local[i] = int(island_verts[size_t(island)].size()); + island_verts[size_t(island)].push_back(m_islands.uvs[i]); + } + + std::vector wire(n_islands), fill(n_islands); + for (int c = 0; c < islands; ++c) { + wire[size_t(c)].format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + fill[size_t(c)].format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + for (GLModel::Geometry *g : { &wire[size_t(c)], &fill[size_t(c)] }) { + g->reserve_vertices(island_verts[size_t(c)].size()); + for (const Vec2f &uv : island_verts[size_t(c)]) + g->add_vertex(Vec3f(uv.x(), uv.y(), 0.f)); + } + } + + for (const Vec3i32 &tri : m_islands.indices) { + if (tri.minCoeff() < 0 || tri.maxCoeff() >= vertex_count) + continue; + const int island = m_islands.vertex_island[size_t(tri[0])]; + if (island < 0 || island >= islands) + continue; + + fill[size_t(island)].add_triangle(unsigned(local[size_t(tri[0])]), unsigned(local[size_t(tri[1])]), + unsigned(local[size_t(tri[2])])); + // Interior edges only: the island outline is drawn separately, brighter and on top, so drawing + // it here as well would just dim it by blending against itself. + for (int i = 0; i < 3; ++i) { + const int a = tri[i], b = tri[(i + 1) % 3]; + if (!is_boundary.count(undirected_edge_key(a, b))) + wire[size_t(island)].add_line(unsigned(local[size_t(a)]), unsigned(local[size_t(b)])); + } + } + + for (int c = 0; c < islands; ++c) { + if (!wire[size_t(c)].is_empty()) + m_island_wireframe[size_t(c)].init_from(std::move(wire[size_t(c)])); + if (!fill[size_t(c)].is_empty()) + m_island_fill[size_t(c)].init_from(std::move(fill[size_t(c)])); + } +} + +void UVEditorCanvas::rebuild_grid() +{ + // One line per UV unit (i.e. per texture tile), plus a subdivision when zoomed in far enough that + // whole tiles would be too coarse to read. + const float span = 2.f * m_zoom; + float step = 1.f; + while (step > 0.01f && span / step > 40.f) + step *= 2.f; + while (span / step < 4.f) + step *= 0.5f; + + // Cover exactly the currently-visible view rectangle (plus a one-step margin), recomputed on every + // render. The old version only rebuilt when `step` changed, so panning at a fixed zoom left the grid + // frozen at wherever it was last built - which is the "not in whole area / not always rendered while + // moving" the user saw. The grid is only a few dozen lines, so rebuilding it per frame is cheap. + float half_w, half_h; + view_half_extents(half_w, half_h); + const Vec2f lo = m_pan - Vec2f(half_w, half_h) - Vec2f(step, step); + const Vec2f hi = m_pan + Vec2f(half_w, half_h) + Vec2f(step, step); + m_grid_step = step; + + const int first_x = int(std::floor(lo.x() / step)), last_x = int(std::ceil(hi.x() / step)); + const int first_y = int(std::floor(lo.y() / step)), last_y = int(std::ceil(hi.y() / step)); + if (last_x - first_x > 4000 || last_y - first_y > 4000) + return; // degenerate zoom; not worth drawing a grid nobody can see + + GLModel::Geometry grid; + grid.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + unsigned index = 0; + const auto line = [&](const Vec2f &a, const Vec2f &b) { + grid.add_vertex(Vec3f(a.x(), a.y(), 0.f)); + grid.add_vertex(Vec3f(b.x(), b.y(), 0.f)); + grid.add_line(index, index + 1); + index += 2; + }; + for (int i = first_x; i <= last_x; ++i) + line(Vec2f(float(i) * step, lo.y()), Vec2f(float(i) * step, hi.y())); + for (int i = first_y; i <= last_y; ++i) + line(Vec2f(lo.x(), float(i) * step), Vec2f(hi.x(), float(i) * step)); + + m_grid_glmodel.reset(); + if (!grid.is_empty()) + m_grid_glmodel.init_from(std::move(grid)); +} + +void UVEditorCanvas::rebuild_rotation_dial() +{ + m_dial_glmodel.reset(); + const bool rotating = (m_gesture == Gesture::RotateIsland || m_gesture == Gesture::RotateIslandModal); + if (!rotating || m_selected_island < 0) + return; + + const wxSize size = GetSize(); + const float uv_per_px = 2.f * m_zoom / float(std::max(1, std::min(size.GetWidth(), size.GetHeight()))); + const float radius = 90.f * uv_per_px; // ~constant on-screen size regardless of zoom + const Vec2f centre = island_centroid(m_selected_island); + + GLModel::Geometry dial; + dial.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + unsigned index = 0; + const auto line = [&](const Vec2f &a, const Vec2f &b) { + dial.add_vertex(Vec3f(a.x(), a.y(), 0.f)); + dial.add_vertex(Vec3f(b.x(), b.y(), 0.f)); + dial.add_line(index, index + 1); + index += 2; + }; + const auto on_ring = [&](float deg, float r) { + const float a = deg * float(M_PI) / 180.f; + return centre + r * Vec2f(std::cos(a), std::sin(a)); + }; + + // The ring itself. + constexpr int SEG = 72; + for (int i = 0; i < SEG; ++i) + line(on_ring(float(i) * 360.f / SEG, radius), on_ring(float(i + 1) * 360.f / SEG, radius)); + // A tick every 15 degrees (the snap marks), longer on the cardinals. + for (int d = 0; d < 360; d += 15) { + const bool cardinal = (d % 90) == 0; + line(on_ring(float(d), radius * (cardinal ? 0.80f : 0.90f)), on_ring(float(d), radius * (cardinal ? 1.08f : 1.0f))); + } + // The needle at the island's current absolute angle. + line(centre, on_ring(m_rot_display_deg, radius * 1.12f)); + + if (!dial.is_empty()) + m_dial_glmodel.init_from(std::move(dial)); +} + +void UVEditorCanvas::rebuild_background_quad() +{ + m_background_glmodel.reset(); + m_background_quad_dirty = false; + if (m_background_width <= 0 || m_background_height <= 0) + return; + + Vec2f lo, hi; + if (m_tile_enabled) { + // Whole tiles, so the backdrop's edge lands on a tile boundary instead of slicing a brick in + // half. framed_bounds() applies exactly this, and the view is framed on its result - the two + // must not drift apart or the backdrop stops being centred in the pane. + framed_bounds(lo, hi); + } else { + // Tiling off: the sampler reads 0 outside the first tile and nothing else exists, so the + // backdrop is exactly that one tile (GL_CLAMP_TO_BORDER in render() gives it the same + // black surround, rather than smearing the edge texels outward). + lo = Vec2f(0.f, 0.f); + hi = Vec2f(1.f, 1.f); + } + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3T2 }; + init_data.reserve_vertices(4); + init_data.reserve_indices(6); + // Texcoord == position: the height sampling maps one whole texture onto each unit square of uv + // (see DecodedHeightTexture::sample()), so uv *is* the texture coordinate. That also puts the + // texture's first pixel row at v = 0, exactly where sample() reads it. + init_data.add_vertex(Vec3f(lo.x(), lo.y(), 0.f), Vec2f(lo.x(), lo.y())); + init_data.add_vertex(Vec3f(hi.x(), lo.y(), 0.f), Vec2f(hi.x(), lo.y())); + init_data.add_vertex(Vec3f(hi.x(), hi.y(), 0.f), Vec2f(hi.x(), hi.y())); + init_data.add_vertex(Vec3f(lo.x(), hi.y(), 0.f), Vec2f(lo.x(), hi.y())); + init_data.add_triangle(0, 1, 2); + init_data.add_triangle(0, 2, 3); + m_background_glmodel.init_from(std::move(init_data)); +} + +void UVEditorCanvas::rebuild_background_texture() +{ + m_background_texture.reset(); + m_background_dirty = false; + if (m_background_width > 0 && m_background_height > 0) + m_background_texture.load_from_raw_data(m_background_pixels, (unsigned int) m_background_width, + (unsigned int) m_background_height, false); + // The quad's extent doesn't depend on the pixels, but it does have to exist alongside them. + m_background_quad_dirty = true; +} + +void UVEditorCanvas::on_paint(wxPaintEvent & /*evt*/) +{ + wxPaintDC dc(this); + render(); +} + +void UVEditorCanvas::on_size(wxSizeEvent &evt) +{ + evt.Skip(); + // Refresh() alone only *schedules* a repaint, which Windows can coalesce/delay until a live + // resize drag ends, leaving stale wrong-aspect-ratio content on screen throughout the drag. + // Update() flushes it immediately - still through the normal paint path (unlike calling + // render() directly), which matters because this canvas shares the app's one GL context with + // the 3D view and must not hijack it outside its own paint. + Refresh(); + Update(); +} + +void UVEditorCanvas::on_leave(wxMouseEvent &evt) +{ + evt.Skip(); + if (m_cursor_inside) { + m_cursor_inside = false; + // The +/- hint and the hover highlight were following the cursor; drop them now the pointer is gone. + if (m_gesture == Gesture::None) { + m_hover_island = m_hover_vertex = -1; + m_hover_edge = { -1, -1 }; + } + Refresh(); + } +} + +void UVEditorCanvas::render() +{ + if (m_context == nullptr || !IsShownOnScreen()) + return; + + // wxGLCanvas::SetCurrent() genuinely fails (returns false) on a canvas that isn't shown on + // screen, and can also fail on a pixel-format mismatch with the context - and every GL call + // made afterwards would then run against whichever context *is* current, which here is the main + // 3D canvas mid-frame. Bail instead of corrupting it. + if (!SetCurrent(*m_context)) + return; + + // The context is shared with the 3D view, which leaves its own state behind: GLCanvas3D turns face culling on at + // init and many of its passes turn it back on, and the pane's projection flips Y, so every triangle drawn here + // faces away from it. With culling left on, the texture and the islands simply vanish - which is why they showed + // the first time and were gone, fully or partly, after the 3D view had drawn. A scissor rectangle left on would + // clip the clear and everything else the same way. Set what this pass relies on; put the 3D view's back after. + const GLboolean prev_cull = ::glIsEnabled(GL_CULL_FACE); + const GLboolean prev_scissor = ::glIsEnabled(GL_SCISSOR_TEST); + const GLboolean prev_stencil = ::glIsEnabled(GL_STENCIL_TEST); + glsafe(::glDisable(GL_CULL_FACE)); + glsafe(::glDisable(GL_SCISSOR_TEST)); + glsafe(::glDisable(GL_STENCIL_TEST)); + glsafe(::glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)); + glsafe(::glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE)); + + if (m_gl_reset_pending) { + m_gl_reset_pending = false; + m_mesh_dirty = true; + m_background_dirty = true; // re-uploads m_background_pixels, or drops the texture if there are none + m_tile_outline_glmodel.reset(); + m_vertex_marker_glmodel.reset(); + m_dim_quad_glmodel.reset(); + m_stroke_glmodel.reset(); + m_stencil_bits = -1; + } + if (m_mesh_dirty) + rebuild_island_models(); + if (m_background_dirty) + rebuild_background_texture(); + if (m_background_quad_dirty) + rebuild_background_quad(); + if (m_needs_fit) + fit_view_to_content(); + rebuild_grid(); + rebuild_rotation_dial(); + + const wxSize size = GetSize(); // logical points; the on-screen handle sizes below use it + const wxSize viewport = gl_drawable_size(this, size); + glsafe(::glViewport(0, 0, viewport.GetWidth(), viewport.GetHeight())); + // Line widths below are authored in logical points (they are chosen against the same scale the + // hit-test thresholds use), so they take the same logical -> device conversion as the viewport. + const float px_scale = float(viewport.GetWidth()) / float(std::max(1, size.GetWidth())); + const auto line_width = [px_scale](float w) { set_line_width(w * px_scale); }; + glsafe(::glClearColor(UV_COLOR_BG.r(), UV_COLOR_BG.g(), UV_COLOR_BG.b(), 1.f)); + glsafe(::glClearStencil(0)); + glsafe(::glStencilMask(0xFF)); + glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)); + glsafe(::glDisable(GL_DEPTH_TEST)); + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + + // Orthographic, centered on m_pan. Y is negated so v runs down the screen, which is what puts + // the texture's first pixel row (v = 0, see rebuild_background_quad()) at the top rather than + // upside down. + float half_w, half_h; + view_half_extents(half_w, half_h); + + Transform3d projection_matrix = Transform3d::Identity(); + projection_matrix(0, 0) = 1.0 / std::max(double(half_w), 1e-6); + projection_matrix(1, 1) = -1.0 / std::max(double(half_h), 1e-6); + Transform3d view_matrix = Transform3d::Identity(); + view_matrix.translate(Vec3d(-double(m_pan.x()), -double(m_pan.y()), 0.0)); + + const auto draw_raw = [&](GLModel &model, const ColorRGBA &color, const Transform3d &view_model, const Transform3d &projection) { + if (!model.is_initialized()) + return; + GLShaderProgram *shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + shader->start_using(); + shader->set_uniform("view_model_matrix", view_model); + shader->set_uniform("projection_matrix", projection); + model.set_color(color); + model.render(); + shader->stop_using(); + }; + const auto draw = [&](GLModel &model, const ColorRGBA &color, const Transform3d &model_matrix) { + draw_raw(model, color, view_matrix * model_matrix, projection_matrix); + }; + const Transform3d identity = Transform3d::Identity(); + + if (m_background_glmodel.is_initialized()) { + if (GLShaderProgram *shader = wxGetApp().get_shader("flat_texture")) { + shader->start_using(); + shader->set_uniform("view_model_matrix", view_matrix); + shader->set_uniform("projection_matrix", projection_matrix); + glsafe(::glActiveTexture(GL_TEXTURE0)); + glsafe(::glBindTexture(GL_TEXTURE_2D, m_background_texture.get_id())); + // Repeat exactly the way DecodedHeightTexture::sample() does, so the backdrop under an + // island really is the texels that island will sample. With tiling off, sample() returns + // 0 outside the first tile - a black border, not a smeared edge, which is what + // CLAMP_TO_BORDER reproduces (GLTexture's own default of GL_REPEAT would not). + const GLint wrap = m_tile_enabled ? (m_tile_mirrored ? GL_MIRRORED_REPEAT : GL_REPEAT) : GL_CLAMP_TO_BORDER; + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap)); + if (!m_tile_enabled) { + constexpr GLfloat border[4] = { 0.f, 0.f, 0.f, 1.f }; + glsafe(::glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border)); + } + m_background_glmodel.render(); + glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); + shader->stop_using(); + } + } + + const auto island_matrix = [this, &identity](int c) { + return (size_t(c) < m_transforms.size()) ? to_transform3d(m_transforms[size_t(c)]) : identity; + }; + const float uv_per_px = 2.f * m_zoom / float(std::max(1, std::min(size.GetWidth(), size.GetHeight()))); + + // Strokes are built as quads `width_px` wide on screen, rebuilt every paint at the current zoom. Square caps + // cover the joints of an outline; the colours passed here are opaque, so the overlapping caps don't show. + using Segments = std::vector>; + const auto draw_segments = [&](const Segments &segments, float width_px, const ColorRGBA &color) { + if (segments.empty()) + return; + const float half = 0.5f * width_px * uv_per_px; + GLModel::Geometry quads; + quads.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + quads.reserve_vertices(segments.size() * 4); + quads.reserve_indices(segments.size() * 6); + unsigned base = 0; + for (const auto &[a, b] : segments) { + const Vec2f d = b - a; + const float len = d.norm(); + if (len <= 1e-12f) + continue; + const Vec2f t = d * (half / len); + const Vec2f n(-t.y(), t.x()); + for (const Vec2f &p : { Vec2f(a - t + n), Vec2f(a - t - n), Vec2f(b + t - n), Vec2f(b + t + n) }) + quads.add_vertex(Vec3f(p.x(), p.y(), 0.f)); + quads.add_triangle(base, base + 1, base + 2); + quads.add_triangle(base, base + 2, base + 3); + base += 4; + } + m_stroke_glmodel.reset(); + if (quads.is_empty()) + return; + m_stroke_glmodel.init_from(std::move(quads)); + draw(m_stroke_glmodel, color, identity); + }; + const auto draw_stroke = [&](const Segments &segments, float width_px, const ColorRGBA &color, float halo_px) { + draw_segments(segments, width_px + 2.f * halo_px, UV_COLOR_HALO); + draw_segments(segments, width_px, color); + }; + const auto edge_segments = [this](const std::vector> &edges) { + Segments out; + out.reserve(edges.size()); + for (const auto &[a, b] : edges) + if (a >= 0 && b >= 0 && size_t(a) < m_islands.uvs.size() && size_t(b) < m_islands.uvs.size()) + out.emplace_back(island_uv(size_t(a)), island_uv(size_t(b))); + return out; + }; + + // Dim the texture everywhere but inside the islands: the fills go into the stencil only, then one + // full-viewport quad washes the rest towards the background. Skipped without a stencil buffer, where the + // quad would cover the islands too. + if (m_stencil_bits < 0) { + GLint bits = 0; + ::glGetError(); // drop anything pending, so the check below sees only this query + ::glGetIntegerv(GL_STENCIL_BITS, &bits); // compatibility profiles + if (::glGetError() != GL_NO_ERROR) { + bits = 0; + ::glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_STENCIL, GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, &bits); + ::glGetError(); + } + m_stencil_bits = std::max(0, int(bits)); + } + if (has_islands() && !m_island_fill.empty() && m_stencil_bits > 0) { + if (!m_dim_quad_glmodel.is_initialized()) { + GLModel::Geometry q; + q.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + q.add_vertex(Vec3f(-1.f, -1.f, 0.f)); + q.add_vertex(Vec3f(1.f, -1.f, 0.f)); + q.add_vertex(Vec3f(1.f, 1.f, 0.f)); + q.add_vertex(Vec3f(-1.f, 1.f, 0.f)); + q.add_triangle(0, 1, 2); + q.add_triangle(0, 2, 3); + m_dim_quad_glmodel.init_from(std::move(q)); + } + glsafe(::glEnable(GL_STENCIL_TEST)); + glsafe(::glStencilFunc(GL_ALWAYS, 1, 0xFF)); + glsafe(::glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE)); + glsafe(::glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE)); + for (int c = 0; c < int(m_island_fill.size()); ++c) + draw(m_island_fill[size_t(c)], ColorRGBA::WHITE(), island_matrix(c)); + glsafe(::glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE)); + glsafe(::glStencilFunc(GL_EQUAL, 0, 0xFF)); + glsafe(::glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP)); + draw_raw(m_dim_quad_glmodel, UV_COLOR_OUTSIDE, identity, identity); + glsafe(::glDisable(GL_STENCIL_TEST)); + } + + line_width(1.f); + draw(m_grid_glmodel, UV_COLOR_GRID, identity); + + // The texture's first tile. Always drawn, even with nothing painted, so the pane always has a + // fixed landmark: the unwrap is packed in mm and divided by the tile size, so it is routinely + // many tiles away from here, and without this there is no way to tell "the islands are somewhere + // else" apart from "there are no islands". + draw_stroke({ { Vec2f(0.f, 0.f), Vec2f(1.f, 0.f) }, { Vec2f(1.f, 0.f), Vec2f(1.f, 1.f) }, + { Vec2f(1.f, 1.f), Vec2f(0.f, 1.f) }, { Vec2f(0.f, 1.f), Vec2f(0.f, 0.f) } }, + 1.5f, UV_COLOR_TILE_OUTLINE, 1.f); + + // Island fills. Unselected islands get none, so the texture that will be baked shows unaltered; the one a + // click would grab gets a light wash, the selection a teal one. A distortion heatmap (set_island_fill_colors) + // replaces the default for unselected islands and is left alone by the hover wash. + for (int c = 0; c < int(m_island_fill.size()); ++c) { + ColorRGBA fill = size_t(c) < m_island_fill_colors.size() ? m_island_fill_colors[size_t(c)] : UV_COLOR_FILL; + if (is_selected(c)) + fill = UV_COLOR_SEL_FILL; + else if (c == m_hover_island && m_island_fill_colors.empty()) + fill = UV_COLOR_HOVER_FILL; + if (fill.a() > 0.f) + draw(m_island_fill[size_t(c)], fill, island_matrix(c)); + } + + // Interior edges stay GL lines (a patch can have a million of them), each with a dark copy one pixel down + // and right, so the light wire still reads on a light texture. + Transform3d shadow_projection = projection_matrix; + shadow_projection.pretranslate(Vec3d(2.0 / std::max(1, size.GetWidth()), -2.0 / std::max(1, size.GetHeight()), 0.0)); + line_width(1.f); + for (int c = 0; c < int(m_island_wireframe.size()); ++c) { + draw_raw(m_island_wireframe[size_t(c)], UV_COLOR_WIRE_SHADOW, view_matrix * island_matrix(c), shadow_projection); + draw(m_island_wireframe[size_t(c)], is_selected(c) ? UV_COLOR_SEL_WIRE : UV_COLOR_WIRE, island_matrix(c)); + } + + // The island outlines - i.e. exactly the edges the seam angle cut the patch along - because "where does one + // island end and the next begin" is the single thing this pane exists to answer. Plain first, then the hovered + // one, then the selection, so a highlighted outline is never overdrawn by its neighbour's. + { + std::vector> plain, hovered, selected; + for (int c = 0; c < int(m_island_boundary_edges.size()); ++c) { + auto &dst = is_selected(c) ? selected : (c == m_hover_island ? hovered : plain); + dst.insert(dst.end(), m_island_boundary_edges[size_t(c)].begin(), m_island_boundary_edges[size_t(c)].end()); + } + draw_stroke(edge_segments(plain), 1.5f, UV_COLOR_BOUNDARY, 1.25f); + draw_stroke(edge_segments(hovered), 2.5f, UV_COLOR_HOVER, 1.5f); + draw_stroke(edge_segments(selected), 3.f, UV_COLOR_SEL_BOUNDARY, 1.75f); + } + + // The rotation protractor, on top of everything while a rotation gesture is live (#11). + if (m_dial_glmodel.is_initialized()) { + line_width(2.f); + draw(m_dial_glmodel, UV_COLOR_DIAL, identity); + line_width(1.f); + } + + // Vertex/Edge mode: the element under the cursor in the hover colour, and the selection as teal - a selected + // edge drawn over its whole length, a vertex as a square handle, both on a dark halo. + if (!m_vertex_marker_glmodel.is_initialized()) { + GLModel::Geometry q; + q.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + q.reserve_vertices(4); + q.reserve_indices(6); + q.add_vertex(Vec3f(-0.5f, -0.5f, 0.f)); + q.add_vertex(Vec3f(0.5f, -0.5f, 0.f)); + q.add_vertex(Vec3f(0.5f, 0.5f, 0.f)); + q.add_vertex(Vec3f(-0.5f, 0.5f, 0.f)); + q.add_triangle(0, 1, 2); + q.add_triangle(0, 2, 3); + m_vertex_marker_glmodel.init_from(std::move(q)); + } + const auto draw_marker = [&](int v, float px, const ColorRGBA &color) { + if (v < 0 || size_t(v) >= m_islands.uvs.size()) + return; + const Vec2f p = island_uv(size_t(v)); + Transform3d m = Transform3d::Identity(); + m.translate(Vec3d(double(p.x()), double(p.y()), 0.0)); + m.scale(double(px * uv_per_px)); + draw(m_vertex_marker_glmodel, color, m); + }; + const auto draw_handle = [&](int v, const ColorRGBA &color, float px) { + draw_marker(v, px + 4.f, UV_COLOR_HALO); + draw_marker(v, px, color); + }; + if (m_select_mode == SelectMode::Vertex) { + if (m_hover_vertex >= 0 && !is_vertex_selected(m_hover_vertex)) + draw_handle(m_hover_vertex, UV_COLOR_HOVER, 7.f); + // Every element of the multi-selection, falling back to the primary while the set is still empty. + if (m_sel_vertices.empty()) + draw_handle(m_active_vertex, UV_COLOR_SEL_BOUNDARY, 8.f); + else + for (const int v : m_sel_vertices) + draw_handle(v, UV_COLOR_SEL_BOUNDARY, 8.f); + } else if (m_select_mode == SelectMode::Edge) { + if (m_hover_edge.first >= 0 && !is_edge_selected(m_hover_edge)) + draw_stroke(edge_segments({ m_hover_edge }), 3.f, UV_COLOR_HOVER, 2.f); + std::vector> edges = m_sel_edges; + if (edges.empty() && m_active_edge.first >= 0) + edges.push_back(m_active_edge); + draw_stroke(edge_segments(edges), 4.f, UV_COLOR_SEL_BOUNDARY, 2.f); + for (const auto &[a, b] : edges) { + draw_handle(a, UV_COLOR_SEL_BOUNDARY, 6.f); + draw_handle(b, UV_COLOR_SEL_BOUNDARY, 6.f); + } + } + + // Add/remove hint next to the cursor in Vertex/Edge mode: a green '+' when a click will add to the + // selection (plain or Shift), a red '-' when Ctrl is held and a click will remove one - the UV-side + // twin of the 3D paint cursor's own sign. + if (m_select_mode != SelectMode::Island && m_cursor_inside) { + const Vec2f centre = screen_to_uv(m_cursor_px) + Vec2f(14.f, -14.f) * uv_per_px; // up-right of the pointer + const float r = 6.f * uv_per_px; + const bool removing = wxGetKeyState(WXK_CONTROL); + Segments sign{ { centre - Vec2f(r, 0.f), centre + Vec2f(r, 0.f) } }; // the '-' bar, shared by both signs + if (!removing) + sign.emplace_back(centre - Vec2f(0.f, r), centre + Vec2f(0.f, r)); // the extra stroke that makes it a '+' + draw_stroke(sign, 2.5f, removing ? ColorRGBA(1.f, 0.36f, 0.30f, 1.f) : ColorRGBA(0.40f, 0.92f, 0.50f, 1.f), 1.f); + } + + // The GL context is shared with the 3D view; leave the bits we touched as we found them. + glsafe(::glDisable(GL_BLEND)); + glsafe(::glEnable(GL_DEPTH_TEST)); + if (prev_cull) + glsafe(::glEnable(GL_CULL_FACE)); + if (prev_scissor) + glsafe(::glEnable(GL_SCISSOR_TEST)); + if (prev_stencil) + glsafe(::glEnable(GL_STENCIL_TEST)); + + SwapBuffers(); + + // Cheap, and render() runs on every gesture change (each Refresh), so the status line stays + // current without threading update_status() through every mouse/key transition. The panel + // guards against relayout when the text is unchanged. + update_status(); +} + +// ----------------------------------------------- +// UVEditorPanel +// ----------------------------------------------- + +namespace { +enum : int { + ID_UV_FRAME = wxID_HIGHEST + 4200, + ID_UV_SNAP, + ID_UV_AVG_SCALE, + ID_UV_CUT, + ID_UV_JOIN, + ID_UV_UNJOIN, + ID_UV_UNWRAP, + ID_UV_MARK_SEAMS, + ID_UV_SEAM_PATH, + ID_UV_CLEAR_SEAMS, + ID_UV_CLEAR_EDITS, + ID_UV_SELECT_ISLAND, // + SelectMode + ID_UV_SELECT_VERTEX, + ID_UV_SELECT_EDGE, + ID_UV_BG_HEIGHT, // + Background + ID_UV_BG_CHECKER, + ID_UV_BG_DISTORTION, + ID_UV_PICK_TEXTURE, +}; + +// An icon from resources/images, rasterised at the window's real pixel density. On GTK3 create_scaled_bitmap() +// renders at the DIP size and lets GTK upscale that on a HiDPI screen, which is what made these small icons blurry; +// here the SVG is rendered at device pixels instead and tagged with the scale, so it is drawn 1:1. (Windows renders +// at device pixels already, and macOS through BitmapCache's own backing scale.) +wxBitmap pane_icon(wxWindow *win, const std::string &name, int size_dip) +{ +#ifdef __WXGTK3__ + if (const double scale = win->GetContentScaleFactor(); scale > 1.0) { + static BitmapCache cache; + const unsigned px = unsigned(std::lround(size_dip * scale)); + if (wxBitmap *bmp = cache.load_svg(name, 0, px, false, wxGetApp().dark_mode()); bmp != nullptr && bmp->IsOk()) + return wxBitmap(bmp->ConvertToImage(), -1, scale); + } +#endif + return create_scaled_bitmap(name, win, size_dip); +} + +// The size a bitmap takes on screen, in the units a wxDC draws in on this platform. +wxSize drawn_size(const wxBitmap &bmp) +{ +#ifdef __WXGTK3__ + return bmp.GetLogicalSize(); +#else + return ScalableBitmap::GetBmpSize(bmp); +#endif +} + +// `bmp` with its alpha scaled down, for a disabled control. Keeps the bitmap's scale factor, so it stays sharp. +wxBitmap faded(const wxBitmap &bmp, double alpha) +{ + wxImage image = bmp.ConvertToImage(); + if (!image.HasAlpha()) + image.InitAlpha(); + unsigned char *a = image.GetAlpha(); + for (int i = 0, n = image.GetWidth() * image.GetHeight(); i < n; ++i) + a[i] = (unsigned char) std::lround(a[i] * alpha); + return wxBitmap(image, -1, bmp.GetScaleFactor()); +} + +// The pane's colours, matching the texture displacement panel's ImGui style in both themes. +struct PaneColors +{ + wxColour bg, ink, dim, rule, frame, hover; + static PaneColors current() + { + if (wxGetApp().dark_mode()) + return { wxColour(0x2d, 0x2d, 0x31), wxColour(0xef, 0xef, 0xf0), wxColour(0x90, 0x90, 0x96), + wxColour(0x3d, 0x3d, 0x45), wxColour(0x36, 0x36, 0x3c), wxColour(0x49, 0x49, 0x50) }; + return { wxColour(0xff, 0xff, 0xff), wxColour(0x32, 0x3a, 0x3d), wxColour(0x7c, 0x82, 0x82), + wxColour(0xed, 0xed, 0xed), wxColour(0xce, 0xce, 0xce), wxColour(0xee, 0xee, 0xee) }; + } +}; + +wxColour mix(const wxColour &a, const wxColour &b, double t) +{ + const auto ch = [t](unsigned char x, unsigned char y) { return (unsigned char) std::lround(x + (y - x) * t); }; + return wxColour(ch(a.Red(), b.Red()), ch(a.Green(), b.Green()), ch(a.Blue(), b.Blue())); +} +} // namespace + +// An icon button - square, or with a label beside the icon - drawn the way the texture displacement panel draws +// its own: a 1 px frame, a teal frame over a teal tint while a toggle is on, a solid teal fill for the one +// accent action, and an optional amber dot for "needs attention". Drawn by hand rather than with wxButton / +// wxToggleButton so the pane looks the same on every platform and in both themes. +// +// A click emits wxEVT_BUTTON with the button's id; for a toggle the event's int is the new state. The owner +// may overwrite that state again with SetValue() - which is how radio groups and gizmo-owned flags work. +class UVToolButton : public wxWindow +{ +public: + UVToolButton(wxWindow *parent, wxWindowID id, const std::string &icon, const wxString &label, const wxString &tip, + bool toggle, bool accent = false, int size_dip = 26) + : wxWindow(parent, id, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE | wxFULL_REPAINT_ON_RESIZE) + , m_icon_name(icon), m_icon_dip(size_dip >= 26 ? 16 : 14), m_label(label), m_toggle(toggle), m_accent(accent) + , m_size_dip(size_dip) + { + SetBackgroundStyle(wxBG_STYLE_PAINT); + SetToolTip(tip); + if (accent) { + wxFont font = GetFont(); + font.MakeBold(); + SetFont(font); + } + Bind(wxEVT_PAINT, &UVToolButton::on_paint, this); + Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent &) { m_hover = true; Refresh(); }); + Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent &) { m_hover = false; m_pressed = false; Refresh(); }); + Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent &) { + if (IsEnabled()) { + m_pressed = true; + Refresh(); + } + }); + Bind(wxEVT_LEFT_UP, [this](wxMouseEvent &e) { + const bool was_pressed = m_pressed; + m_pressed = false; + Refresh(); + if (!was_pressed || !IsEnabled() || !GetClientRect().Contains(e.GetPosition())) + return; + if (m_toggle) + m_on = !m_on; + wxCommandEvent evt(wxEVT_BUTTON, GetId()); + evt.SetEventObject(this); + evt.SetInt(m_on ? 1 : 0); + ProcessWindowEvent(evt); + }); + SetMinSize(DoGetBestSize()); + } + + void SetValue(bool on) + { + if (on != m_on) { + m_on = on; + Refresh(); + } + } + bool GetValue() const { return m_on; } + // Shows a ready-made bitmap instead of the SVG icon (the layer thumbnail). Its scale factor sets its size. + void SetBitmap(const wxBitmap &bmp) + { + m_bitmap = bmp; + Refresh(); + } + void SetBadge(bool badge) + { + if (badge != m_badge) { + m_badge = badge; + Refresh(); + } + } + bool Enable(bool enable = true) override + { + const bool changed = wxWindow::Enable(enable); + if (changed) + Refresh(); + return changed; + } + +protected: + wxSize DoGetBestSize() const override + { + const int h = FromDIP(m_size_dip); + if (m_label.empty()) + return wxSize(h, h); + const int icon_w = m_icon_name.empty() ? 0 : FromDIP(m_icon_dip) + FromDIP(6); + return wxSize(FromDIP(9) + icon_w + GetTextExtent(m_label).x + FromDIP(10), h); + } + +private: + void on_paint(wxPaintEvent &) + { + wxAutoBufferedPaintDC dc(this); + const PaneColors c = PaneColors::current(); + const wxRect r = GetClientRect(); + const wxColour teal(0x00, 0x96, 0x88); + const bool enabled = IsEnabled(); + + wxColour fill = c.bg, border = c.frame, text = c.ink; + if (m_accent) { + fill = (m_hover && enabled) ? wxColour(0x26, 0xa6, 0x9a) : teal; + border = fill; + text = *wxWHITE; + } else if (m_on) { + fill = mix(c.bg, teal, 0.22); + border = teal; + } else if (m_hover && enabled) { + fill = c.hover; + } + if (m_pressed) + fill = mix(fill, teal, 0.18); + + dc.SetBackground(wxBrush(c.bg)); + dc.Clear(); + dc.SetBrush(wxBrush(fill)); + dc.SetPen(wxPen(border, 1)); + dc.DrawRoundedRectangle(r, FromDIP(2)); + + // Rasterised on first paint rather than in the constructor: the content scale is only reliable once the + // window is on screen, and it changes when the window moves to a screen with a different one. + if (!m_icon_name.empty() && (!m_icon.IsOk() || m_icon_scale != GetContentScaleFactor())) { + m_icon_scale = GetContentScaleFactor(); + m_icon = pane_icon(this, m_icon_name, m_icon_dip); + } + const wxBitmap &bmp = m_bitmap.IsOk() ? m_bitmap : m_icon; + int x = 0; + if (bmp.IsOk()) { + const wxSize bs = drawn_size(bmp); + x = m_label.empty() ? (r.width - bs.x) / 2 : FromDIP(9); + dc.DrawBitmap(enabled ? bmp : faded(bmp, 0.35), x, (r.height - bs.y) / 2, true); + x += bs.x + FromDIP(6); + } else { + x = FromDIP(9); + } + if (!m_label.empty()) { + dc.SetFont(GetFont()); + dc.SetTextForeground(enabled ? text : mix(text, c.bg, 0.55)); + dc.DrawText(m_label, x, (r.height - dc.GetTextExtent(m_label).y) / 2); + } + if (m_badge) { + const int d = FromDIP(9); + dc.SetPen(wxPen(c.bg, FromDIP(2))); + dc.SetBrush(wxBrush(wxColour(0xe0, 0xa4, 0x4a))); + dc.DrawCircle(r.width - d / 2 - 1, d / 2 + 1, d / 2); + } + } + + std::string m_icon_name; + int m_icon_dip = 16; + wxBitmap m_icon; // m_icon_name rasterised for m_icon_scale + double m_icon_scale = 0.; + wxBitmap m_bitmap; + wxString m_label; + bool m_toggle = false; + bool m_accent = false; + int m_size_dip = 26; + bool m_on = false; + bool m_badge = false; + bool m_hover = false; + bool m_pressed = false; +}; + +UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) +{ + const PaneColors c = PaneColors::current(); + const int gap = FromDIP(6); + const int pad = FromDIP(8); + SetBackgroundColour(c.bg); + + const auto rule = [&](const wxSize &size) { + auto *w = new wxWindow(this, wxID_ANY, wxDefaultPosition, size); + w->SetBackgroundColour(c.rule); + w->SetMinSize(size); + return w; + }; + const auto text = [&](const wxString &label, const wxColour &colour, long style = 0) { + auto *t = new wxStaticText(this, wxID_ANY, label, wxDefaultPosition, wxDefaultSize, style); + t->SetForegroundColour(colour); + t->SetBackgroundColour(c.bg); + return t; + }; + + // ---- header: the layer being edited, the background under the islands, and Unwrap ---- + auto *header = new wxBoxSizer(wxHORIZONTAL); + m_thumb = new UVToolButton(this, ID_UV_PICK_TEXTURE, std::string(), wxEmptyString, + _L("Change texture - choose another one from the texture library"), false, false, 26); + m_layer_name = text(wxEmptyString, c.ink, wxST_ELLIPSIZE_END); + { + wxFont font = m_layer_name->GetFont(); + font.MakeBold(); + m_layer_name->SetFont(font); + } + // The name opens the library too, like the texture's name in the layer card. + m_layer_name->SetCursor(wxCursor(wxCURSOR_HAND)); + m_layer_name->SetToolTip(m_thumb->GetToolTipText()); + m_layer_name->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent &) { + if (m_canvas->pane_state().has_layer) + m_canvas->run_command(UVEditorCanvas::Command::PickTexture); + }); + m_layer_name->SetMinSize(wxSize(FromDIP(30), -1)); + m_tile = text(wxEmptyString, c.dim); + header->Add(m_thumb, 0, wxALIGN_CENTER_VERTICAL); + header->Add(m_layer_name, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, gap); + header->Add(m_tile, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, gap); + + const char *const bg_icons[3] = { "texture_displacement_uv_bg_height", "texture_displacement_checker", + "texture_displacement_distortion" }; + const wxString bg_tips[3] = { _L("Height map - show the layer's texture under the islands"), + _L("Checker - a test grid; squares stay square where the unwrap does not stretch"), + _L("Distortion - colour each island by how much the unwrap stretches it") }; + for (int i = 0; i < 3; ++i) { + m_background[i] = new UVToolButton(this, ID_UV_BG_HEIGHT + i, bg_icons[i], wxEmptyString, bg_tips[i], true, false, 22); + header->Add(m_background[i], 0, wxALIGN_CENTER_VERTICAL | wxLEFT, i == 0 ? gap : FromDIP(3)); + } + header->Add(rule(wxSize(1, FromDIP(18))), 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, gap); + m_unwrap = new UVToolButton(this, ID_UV_UNWRAP, "texture_displacement_map_unwrap", _L("Unwrap"), wxEmptyString, false, true, 24); + header->Add(m_unwrap, 0, wxALIGN_CENTER_VERTICAL); + + // ---- settings: what the next Unwrap will do ---- + auto *settings = new wxBoxSizer(wxHORIZONTAL); + auto *seam_label = text(_L("Seam angle"), c.dim); + m_seam_angle = new ::SpinInput(this, wxEmptyString, wxString::FromUTF8("°"), wxDefaultPosition, wxSize(FromDIP(76), FromDIP(24)), 0, + 5, 90, 40); + const wxString seam_tip = _L("Edges sharper than this are cut, and the pieces either side of them are flattened separately. " + "Lower it to cut more: each piece then lies flat with less stretching, at the cost of the " + "texture not running continuously across the cut. Takes effect at the next Unwrap."); + seam_label->SetToolTip(seam_tip); + m_seam_angle->SetToolTip(seam_tip); + m_connect = new ::CheckBox(this); + auto *connect_label = text(_L("Connect islands"), c.ink); + const wxString connect_tip = _L("Lay the unwrap out as a connected net: pieces that share an edge are unfolded next to each " + "other (a cube becomes a joined net rather than six loose squares). They stay separate " + "islands, so you can still move any of them by hand afterwards."); + m_connect->SetToolTip(connect_tip); + connect_label->SetToolTip(connect_tip); + settings->Add(seam_label, 0, wxALIGN_CENTER_VERTICAL); + settings->Add(m_seam_angle, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, gap); + settings->AddStretchSpacer(); + settings->Add(m_connect, 0, wxALIGN_CENTER_VERTICAL); + settings->Add(connect_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4)); + + // ---- tool strip ---- + auto *strip = new wxBoxSizer(wxVERTICAL); + const auto tool = [&](int id, const char *icon, const wxString &tip, bool toggle) { + auto *b = new UVToolButton(this, id, icon, wxEmptyString, tip, toggle); + strip->Add(b, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, FromDIP(4)); + return b; + }; + // 11 px either side of a group rule (7 below plus the next button's own 4), against 4 px between buttons. + const auto strip_rule = [&]() { + auto *r = rule(wxSize(FromDIP(20), 1)); + strip->Add(r, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, FromDIP(11)); + strip->AddSpacer(FromDIP(7)); + }; + m_select[0] = tool(ID_UV_SELECT_ISLAND, "texture_displacement_uv_select_island", _L("Island - move, rotate and scale whole islands"), true); + m_select[1] = tool(ID_UV_SELECT_VERTEX, "texture_displacement_uv_select_vertex", _L("Vertex - drag vertices to reshape; Shift/Ctrl to multi-select"), true); + m_select[2] = tool(ID_UV_SELECT_EDGE, "texture_displacement_uv_select_edge", _L("Edge - drag edges to reshape; Shift/Ctrl to multi-select"), true); + strip_rule(); + m_mark_seams = tool(ID_UV_MARK_SEAMS, "texture_displacement_uv_seam", + _L("Mark seams - click edges on the model to cut the unwrap along them. The edge under the cursor is " + "highlighted yellow; click to mark it red, click a red edge again to unmark it. Painting is paused " + "while this is on."), + true); + m_seam_path = tool(ID_UV_SEAM_PATH, "texture_displacement_uv_path", + _L("Path - instead of clicking every edge, click a start point and then an end point: the whole " + "shortest path between them is seamed at once. Available while marking seams."), + true); + m_clear_seams = tool(ID_UV_CLEAR_SEAMS, "texture_displacement_cross", _L("Clear seams - remove every seam marked on this layer"), false); + strip_rule(); + m_avg_scale = tool(ID_UV_AVG_SCALE, "texture_displacement_uv_avg_scale", _L("Average scale - give every island the same texel density"), false); + m_cut = tool(ID_UV_CUT, "texture_displacement_uv_cut", _L("Cut - split the selected island across its long axis"), false); + m_join = tool(ID_UV_JOIN, "texture_displacement_uv_join", _L("Join - unfold the selected island onto its nearest neighbour along their shared edge"), false); + m_unjoin = tool(ID_UV_UNJOIN, "texture_displacement_uv_unjoin", _L("Unjoin - send the selected island back to its own packed position"), false); + strip->AddStretchSpacer(); + m_clear_edits = tool(ID_UV_CLEAR_EDITS, "texture_displacement_uv_clear_edits", + _L("Clear UV edits - discard all manual vertex/edge moves and return the unwrap to its automatic shape"), false); + m_snap = tool(ID_UV_SNAP, "texture_displacement_uv_snap", _L("Snap - stick islands together when dragging one against another"), true); + m_frame = tool(ID_UV_FRAME, "texture_displacement_uv_frame", _L("Frame all islands (Home)"), false); + strip->AddSpacer(FromDIP(4)); + + m_canvas = new UVEditorCanvas(this); + auto *body = new wxBoxSizer(wxHORIZONTAL); + body->Add(strip, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(4)); + body->Add(rule(wxSize(1, -1)), 0, wxEXPAND); + body->Add(m_canvas, 1, wxEXPAND); + + // ---- status line: the current gesture on the left, the unwrap summary on the right ---- + m_status = text(wxEmptyString, c.dim, wxST_ELLIPSIZE_END); + m_status->SetMinSize(wxSize(FromDIP(40), -1)); + m_stats = text(wxEmptyString, c.dim); + auto *status = new wxBoxSizer(wxHORIZONTAL); + status->Add(m_status, 1, wxALIGN_CENTER_VERTICAL); + status->Add(m_stats, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, gap); + + auto *sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(header, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, pad); + sizer->Add(settings, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + sizer->Add(rule(wxSize(-1, 1)), 0, wxEXPAND | wxTOP, gap); + sizer->Add(body, 1, wxEXPAND); + sizer->Add(rule(wxSize(-1, 1)), 0, wxEXPAND); + sizer->Add(status, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, FromDIP(5)); + SetSizer(sizer); + + Bind(wxEVT_BUTTON, &UVEditorPanel::on_tool, this); + // Both settings compare against the last state the gizmo pushed before sending anything: apply_state() + // writes them back, and without the check that write would bounce straight back to the gizmo as an edit. + m_seam_angle->Bind(wxEVT_SPINCTRL, [this](wxCommandEvent &) { + const int value = m_seam_angle->GetValue(); + if (value != int(std::lround(m_canvas->pane_state().seam_angle_deg))) + m_canvas->run_command(UVEditorCanvas::Command::SetSeamAngle, float(value)); + }); + const auto send_connect = [this]() { + if (m_connect->GetValue() != m_canvas->pane_state().connect_islands) + m_canvas->run_command(UVEditorCanvas::Command::SetConnectIslands, m_connect->GetValue() ? 1.f : 0.f); + }; + m_connect->Bind(wxEVT_TOGGLEBUTTON, [send_connect](wxCommandEvent &e) { + send_connect(); + e.Skip(); + }); + connect_label->Bind(wxEVT_LEFT_DOWN, [this, send_connect](wxMouseEvent &) { + if (!m_connect->IsEnabled()) + return; + m_connect->SetValue(!m_connect->GetValue()); + send_connect(); + }); + + m_canvas->set_status_callback([this](const wxString &label) { + if (m_status != nullptr && m_status->GetLabel() != label) { + m_status->SetLabel(label); + m_status->Refresh(); + refresh_selection_tools(); // a selection or mode change is what changes the status line + } + }); + m_canvas->set_pane_state_callback([this](const UVEditorCanvas::PaneState &state) { apply_state(state); }); + apply_state(m_canvas->pane_state()); + + // AUI shows and hides the pane through this panel. Rebuild the canvas's GPU objects when it comes back, so + // the texture and islands never depend on surviving the canvas's native window being hidden. + Bind(wxEVT_SHOW, [this](wxShowEvent &e) { + e.Skip(); + if (e.IsShown()) + m_canvas->invalidate_gl(); + }); +} + +void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s) +{ + const wxString name = s.has_layer ? s.layer_name : _L("No layer mapped with Unwrap"); + const wxString tile = s.has_layer ? wxString::Format(_L("%.1f mm tile"), s.tile_mm) : wxString(); + const bool relayout = m_layer_name->GetLabel() != name || m_tile->GetLabel() != tile; + m_layer_name->SetLabel(name); + m_tile->SetLabel(tile); + + if (s.thumbnail_px > 0 && s.thumbnail_rgb.size() == size_t(s.thumbnail_px) * size_t(s.thumbnail_px) * 3) { + // Scaled to device pixels and tagged with the content scale, so HiDPI screens get every pixel of it. + const double scale = GetContentScaleFactor(); + const int px = std::max(1, int(std::lround(FromDIP(20) * scale))); + wxImage image(s.thumbnail_px, s.thumbnail_px, false); + std::copy(s.thumbnail_rgb.begin(), s.thumbnail_rgb.end(), image.GetData()); + m_thumb->SetBitmap(wxBitmap(image.Scale(px, px, wxIMAGE_QUALITY_HIGH), -1, scale)); + } else { + m_thumb->SetBitmap(wxNullBitmap); + } + m_thumb->Enable(s.has_layer); + + for (int i = 0; i < 3; ++i) { + m_background[i]->SetValue(int(s.background) == i); + m_background[i]->Enable(s.has_layer); + } + m_unwrap->Enable(s.has_layer); + m_unwrap->SetBadge(s.unwrap_stale); + m_unwrap->SetToolTip(s.unwrap_stale ? + _L("Out of date - the paint, the seams or the seam angle changed since this unwrap was made. " + "Press to unwrap again.") : + _L("Flatten the painted area into UV islands. It is computed only when you press this, not on " + "every edit - so paint, change the seam angle or mark seams first, then press Unwrap.")); + + if (m_seam_angle->GetValue() != int(std::lround(s.seam_angle_deg))) + m_seam_angle->SetValue(int(std::lround(s.seam_angle_deg))); + m_seam_angle->Enable(s.has_layer); + m_connect->SetValue(s.connect_islands); + m_connect->Enable(s.has_layer); + + m_mark_seams->SetValue(s.mark_seams); + m_mark_seams->Enable(s.has_layer); + m_seam_path->SetValue(s.seam_path); + m_seam_path->Enable(s.has_layer && s.mark_seams); + m_clear_seams->Enable(s.has_layer && s.has_seams); + m_clear_edits->Enable(s.has_layer && s.has_uv_edits); + + m_stats->SetLabel(s.unwrapped ? wxString::Format(_L("%d islands, %s faces"), s.island_count, + wxString(std::to_string(s.face_count))) : + wxString()); + refresh_selection_tools(); + if (relayout) + Layout(); +} + +void UVEditorPanel::refresh_selection_tools() +{ + const bool has_islands = m_canvas->has_islands(); + const int mode = int(m_canvas->select_mode()); + for (int i = 0; i < 3; ++i) { + m_select[i]->SetValue(i == mode); + m_select[i]->Enable(has_islands); + } + const bool island_picked = has_islands && m_canvas->select_mode() == UVEditorCanvas::SelectMode::Island && + m_canvas->selected_island() >= 0; + m_avg_scale->Enable(has_islands); + m_cut->Enable(island_picked); + m_join->Enable(island_picked); + m_unjoin->Enable(island_picked); + m_snap->Enable(has_islands); + m_snap->SetValue(m_canvas->snap_enabled()); + m_frame->Enable(has_islands); +} + +void UVEditorPanel::on_tool(wxCommandEvent &evt) +{ + using Command = UVEditorCanvas::Command; + const int id = evt.GetId(); + const bool on = evt.GetInt() != 0; + switch (id) { + case ID_UV_FRAME: m_canvas->run_command(Command::FrameAll); break; + case ID_UV_SNAP: m_canvas->run_command(Command::ToggleSnap); break; + case ID_UV_AVG_SCALE: m_canvas->run_command(Command::AverageScale); break; + case ID_UV_CUT: m_canvas->run_command(Command::CutSelectedIsland); break; + case ID_UV_JOIN: m_canvas->run_command(Command::JoinSelected); break; + case ID_UV_UNJOIN: m_canvas->run_command(Command::UnjoinSelected); break; + case ID_UV_UNWRAP: m_canvas->run_command(Command::Unwrap); break; + case ID_UV_MARK_SEAMS: m_canvas->run_command(Command::SetMarkSeams, on ? 1.f : 0.f); break; + case ID_UV_SEAM_PATH: m_canvas->run_command(Command::SetSeamPath, on ? 1.f : 0.f); break; + case ID_UV_CLEAR_SEAMS: m_canvas->run_command(Command::ClearSeams); break; + case ID_UV_CLEAR_EDITS: m_canvas->run_command(Command::ClearUVEdits); break; + case ID_UV_PICK_TEXTURE: m_canvas->run_command(Command::PickTexture); break; + case ID_UV_SELECT_ISLAND: + case ID_UV_SELECT_VERTEX: + case ID_UV_SELECT_EDGE: m_canvas->run_command(Command::SetSelectMode, float(id - ID_UV_SELECT_ISLAND)); break; + case ID_UV_BG_HEIGHT: + case ID_UV_BG_CHECKER: + case ID_UV_BG_DISTORTION: + // Shown straight away; the gizmo confirms it with its next state push. + for (int i = 0; i < 3; ++i) + m_background[i]->SetValue(i == id - ID_UV_BG_HEIGHT); + m_canvas->run_command(Command::SetBackground, float(id - ID_UV_BG_HEIGHT)); + break; + default: evt.Skip(); return; + } + refresh_selection_tools(); +} + +} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/UVEditorCanvas.hpp b/src/slic3r/GUI/UVEditorCanvas.hpp new file mode 100644 index 0000000000..0dd088c4e8 --- /dev/null +++ b/src/slic3r/GUI/UVEditorCanvas.hpp @@ -0,0 +1,441 @@ +#ifndef slic3r_UVEditorCanvas_hpp_ +#define slic3r_UVEditorCanvas_hpp_ + +#include +#include +#include +#include + +// Must come before wx/glcanvas.h in any translation unit that includes this header: glcanvas.h +// pulls in the platform's real GL/gl.h, and glad/gl.h errors out if that happens first (it wants +// to be the one to define the standard include guards GL/gl.h itself defines). +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libslic3r/Point.hpp" +#include "GLModel.hpp" +#include "GLTexture.hpp" + +// Orca's own widgets (slic3r/GUI/Widgets), declared at global scope. +class SpinInput; +class CheckBox; + +namespace Slic3r::GUI { + +// Standalone 2D viewer/editor for a flattened (UV-unwrapped) mesh patch - shows the result of +// GLGizmoTextureDisplacement's LSCM projection method as its own resizable pane (see Plater's +// "uv_editor" AUI pane) rather than folding 2D UV-space rendering into the main 3D viewport. +// +// Islands can be laid out by hand, roughly the way Blender's UV editor works: click one to select +// it, drag to move it, right-drag or press R to rotate it, S to scale it, both about its own centre. +// Islands are free to overlap - nothing re-packs them behind the user's back. The texture underneath +// is always drawn upright and axis-aligned, and it is the islands that move over it, which is what +// makes "rotate this island" a meaningful gesture rather than just spinning the whole texture. +// +// **Geometry is uploaded in the unwrap's own (raw, mm) coordinates, once**, and each island is drawn +// through its own affine matrix passed as a shader uniform. That matters: a patch can easily run to +// a million triangles, and the earlier design - which pre-transformed every UV on the CPU and +// re-uploaded the whole wireframe on every mouse-move event - made a drag cost a couple of hundred +// milliseconds per frame. Moving an island now touches a 2x3 matrix and nothing else. +// +// Uses the app's single shared wxGLContext (via wxGetApp().init_glcontext(), the same call +// View3D/Preview/AssembleView each make in GUI_Preview.cpp) rather than an independent context of +// its own, specifically so it can reuse the app's already-registered "flat"/"flat_texture" +// shaders and GLModel as-is - GLModel::render() looks up its shader via a GUI_App-wide "current +// shader", which only means anything for canvases sharing the app's one real GL context. +class UVEditorCanvas : public wxGLCanvas +{ +public: + explicit UVEditorCanvas(wxWindow *parent); + + // Maps one island's raw unwrap coordinate to a texture UV: the island's own hand placement and + // then the layer's tiling/rotation/offset, composed into a single affine (columns: x basis, + // y basis, translation). + using IslandTransform = Eigen::Matrix; + + // The unwrap to display, in the unwrap's own mm coordinates - *not* texture UVs. Changing this + // is the expensive path (it rebuilds every vertex buffer), so it must only be called when the + // unwrap itself changes, never merely because an island moved. Pass an empty `indices` to show + // nothing. + struct Islands + { + std::vector uvs; + std::vector indices; + std::vector vertex_island; // per uv + std::vector> boundary_edges; // island outlines, indices into uvs + int island_count = 0; + }; + void set_islands(Islands islands); + + // The cheap path: one transform per island. Safe to call on every mouse-move of a drag. + void set_island_transforms(std::vector transforms); + + // One fill colour per island, overriding the default light-green wash - used to paint the UV + // distortion heatmap over the islands when the gizmo's "Distortion" check mode is on (#7/#14). + // Pass empty to go back to the default wash. Cheap: it never touches a vertex buffer. + void set_island_fill_colors(std::vector colors); + + // The layer's own tiling scale and rotation. Needed to map a gesture, which happens in texture-UV + // space, back into the unwrap's mm space - which is where a TextureIsland's offset actually + // lives (see apply_uv_transform()). The tile settings come along because the background has to + // repeat exactly the way the height sampler does, or the pane would stop showing what gets baked. + void set_uv_transform(float tiling_scale, float rotation_deg, bool tile_enabled, bool tile_mirrored); + + // Same 8-bit grayscale pixels build_texture_displacement()'s height sampling uses, shown + // beneath the wireframe (expanded to RGBA on upload) so the unwrap can be checked against the + // texture it will actually sample. Pass width/height <= 0 to clear it. + void set_background_texture(const std::vector &grayscale_pixels, int width, int height); + + // Snap a dragged island's boundary to a neighbouring island's when they come close (#2). Off is + // the honest default for overlap-friendly layouts; the pane toolbar toggles it. + void set_snap_enabled(bool enabled) { m_snap_enabled = enabled; } + bool snap_enabled() const { return m_snap_enabled; } + + // High-level actions the pane's controls trigger. The canvas handles the view-only ones (framing, + // the snap toggle) itself and forwards the rest to whoever owns the island data (the gizmo), via + // the command callback - the canvas has the selection and the view, the gizmo has the layer. + // `value` carries the new setting for the Set* commands (an angle, a flag as 0/1, or an index into + // SelectMode / Background) and is 0 otherwise. + enum class Command { + FrameAll, ToggleSnap, AverageScale, CutSelectedIsland, ProjectFromView, JoinSelected, UnjoinSelected, + Unwrap, SetSeamAngle, SetConnectIslands, SetSelectMode, SetMarkSeams, SetSeamPath, ClearSeams, ClearUVEdits, + SetBackground, PickTexture, PaneClosed + }; + void run_command(Command cmd, float value = 0.f); + using CommandFn = std::function; + void set_command_callback(CommandFn fn) { m_on_command = std::move(fn); } + + // What the canvas shows under the islands; mirrors the gizmo's Normal/Checker/Distortion views. + enum class Background { Height, Checker, Distortion }; + + // Everything the pane's own controls show that the gizmo owns: the active layer and its unwrap settings. + // The gizmo pushes it whenever any of it may have changed, and the pane redraws its header, settings row + // and tool strip from it - so the controls never hold state of their own that could drift from the layer. + struct PaneState + { + bool has_layer = false; // an active layer mapped with Unwrap + wxString layer_name; + float tile_mm = 0.f; + std::vector thumbnail_rgb; // thumbnail_px square, 3 bytes per pixel; empty for none + int thumbnail_px = 0; + float seam_angle_deg = 40.f; + bool connect_islands = true; + bool unwrapped = false; // an unwrap exists for this layer + bool unwrap_stale = false; // paint, seams or the seam angle changed since it was made + bool mark_seams = false; + bool seam_path = false; + bool has_seams = false; + bool has_uv_edits = false; + Background background = Background::Height; + int island_count = 0; + size_t face_count = 0; + }; + void set_pane_state(PaneState state); + const PaneState &pane_state() const { return m_pane_state; } + using PaneStateFn = std::function; + void set_pane_state_callback(PaneStateFn fn) { m_on_pane_state = std::move(fn); } + + // Called whenever the one-line status/hint text changes (current gesture + the shortcuts that + // apply right now), so the pane can show it Blender-style along the bottom. + using StatusFn = std::function; + void set_status_callback(StatusFn fn) { m_on_status = std::move(fn); } + + // Reports an island edit as it happens. The deltas are *incremental* (one mouse event's worth) + // and already converted into the units a TextureIsland stores - unwrap mm, degrees, and a scale + // *factor* to multiply the island's existing scale by. They are incremental on purpose: the owner + // applies them and hands back fresh transforms, and if the gesture tracked geometry rather than + // raw mouse motion that round trip would feed back into itself. `finished` marks the end of a + // gesture, so the owner can rebuild the 3D preview once rather than on every motion event. + using IslandEditFn = + std::function; + void set_island_edit_callback(IslandEditFn fn) { m_on_island_edit = std::move(fn); } + + // What a click grabs: a whole island (move/rotate/scale, groups move together), a single vertex, or + // a single edge (both its endpoints). Vertex/Edge are free-form UV editing - they move the actual + // unwrap coordinates, which the owner then folds into the layer's per-vertex UV overrides so the + // change is baked, not just shown (see set_vertex_edit_callback). + enum class SelectMode { Island, Vertex, Edge }; + void set_select_mode(SelectMode mode); + SelectMode select_mode() const { return m_select_mode; } + + // Reports a committed vertex/edge edit: the list of (unwrapped-vertex index, its new raw-unwrap + // coordinate in mm). Fired once, on mouse release, since it re-solves the displacement preview; the + // pane shows the edit live from its own geometry in the meantime. The owner maps the unwrapped index + // to a mesh vertex (via the unwrap's source_vertex) and stores the override. + using UVVertexEditFn = std::function> &edits)>; + void set_vertex_edit_callback(UVVertexEditFn fn) { m_on_vertex_edit = std::move(fn); } + + // The primary (last-clicked) island, still the pivot for rotate/scale and the target of the + // single-island toolbar commands (Cut/Join/Unjoin). -1 if nothing is selected. + int selected_island() const { return m_selected_island; } + // Whether there is an unwrap on screen at all. + bool has_islands() const { return m_islands.island_count > 0 && !m_islands.indices.empty(); } + // The full multi-selection (Shift adds, Ctrl toggles). Always contains m_selected_island when it is + // >= 0. The gizmo reads this to decide which islands a drag moves together, unioned with each + // selected island's join group. + const std::vector &selected_islands() const { return m_selection; } + void reset_view(); + // Rebuilds every GPU object at the next paint, from the data the canvas keeps on the CPU. Called when the + // pane is shown again, so a hidden-and-reshown canvas never depends on GL objects surviving its native + // window being torn down. + void invalidate_gl() + { + m_gl_reset_pending = true; + Refresh(); + } + +private: + void on_paint(wxPaintEvent &evt); + void on_size(wxSizeEvent &evt); + void on_mouse(wxMouseEvent &evt); + void on_key(wxKeyEvent &evt); + void on_leave(wxMouseEvent &evt); // drops the +/- cursor hint when the pointer leaves the canvas + void on_erase_background(wxEraseEvent &evt) {} // required to avoid flicker on MSW, deliberately a no-op + + void render(); + void rebuild_island_models(); + void rebuild_background_texture(); + void rebuild_background_quad(); + void rebuild_grid(); + // The UV region worth looking at: every island, plus always at least the texture's first tile, so + // there is something sensibly framed even before anything is painted. + void content_bounds(Vec2f &min_uv, Vec2f &max_uv) const; + // What is actually *drawn*, which is content_bounds() snapped out to whole tiles whenever the + // backdrop tiles (see rebuild_background_quad()). Both the framing and the backdrop go through + // this so they cannot disagree. + void framed_bounds(Vec2f &min_uv, Vec2f &max_uv) const; + // Frames framed_bounds(). Bound to Home, and run once each time an unwrap first appears. + void fit_view_to_content(); + + // Half-extents of the visible UV region. Split out because both rendering and every mouse + // gesture need them, and they have to agree exactly or picking lands in the wrong place. + void view_half_extents(float &half_w, float &half_h) const; + Vec2f screen_to_uv(const wxPoint &px) const; + // Raw unwrap coordinate -> texture UV, through the island's own transform. + Vec2f island_uv(size_t vertex) const; + // The island under `uv`, or -1. Prefers the current selection when islands overlap, so that + // dragging one that sits under another doesn't hand the drag to its neighbour halfway through. + int island_at(const Vec2f &uv) const; + // Nearest unwrapped vertex to `uv` within a screen-space threshold, or -1 (Vertex mode picking). + int vertex_at(const Vec2f &uv) const; + // Nearest island-boundary edge to `uv` within a screen-space threshold, as its two unwrapped-vertex + // indices, or {-1,-1} (Edge mode picking). + std::pair edge_at(const Vec2f &uv) const; + // Moves one unwrapped vertex by a texture-UV delta, converting it back into the vertex's own raw + // unwrap space through its island's inverse transform, and marks the mesh dirty so it redraws. + void move_vertex_raw(int unwrapped_vertex, const Vec2f &delta_uv); + Vec2f island_centroid(int island) const; + // Converts a delta in texture-UV space into the unwrap's mm space, undoing the layer's scale and + // rotation - the inverse of what apply_uv_transform() did on the way in. + Vec2f uv_delta_to_unwrap(const Vec2f &delta_uv) const; + // The correction that would bring the selected island's nearest boundary vertex onto a boundary + // vertex of some *other* island, in texture-UV space. Zero if nothing is within reach (#2). + Vec2f snap_correction(int island) const; + void end_gesture(); + // Rebuilds the status line from the current gesture/selection and pushes it to m_on_status. + void update_status(); + // Re-picks what a click at `pos` would grab in the current select mode, and repaints when that changed. + void update_hover(const wxPoint &pos); + + wxGLContext *m_context = nullptr; // owned by OpenGLManager/GUI_App, not by this canvas + + Islands m_islands; + std::vector m_transforms; + // Per-island fill colour override (distortion heatmap); empty means use the default wash (#7). + std::vector m_island_fill_colors; + // Boundary vertices per island, for snapping - a patch's boundary is a tiny fraction of it, and + // rescanning the whole uv array on every snap test would not be. + std::vector> m_island_boundary_verts; + // Per island: its outline edges, its triangles (indices into m_islands.indices) and the bounding box of its + // raw coordinates - so picking tests one island's triangles only when the cursor is inside its box. + std::vector>> m_island_boundary_edges; + std::vector> m_island_tris; + std::vector> m_island_raw_bounds; + + bool m_mesh_dirty = true; + bool m_gl_reset_pending = false; + // One set of models per island, so an island can be drawn through its own transform. Built once + // per unwrap, never on a drag. Outlines are not among them: they are drawn as screen-width quads, + // rebuilt every paint (see render()), because GL wide lines are not reliably wider than 1 px. + std::vector m_island_wireframe; // interior edges + std::vector m_island_fill; // filled; also the stencil mask that keeps islands undimmed + GLModel m_stroke_glmodel; // scratch model for the quads of one stroke pass + GLModel m_dim_quad_glmodel; // full-viewport quad that dims the texture outside the islands + int m_stencil_bits = -1; // of the default framebuffer; -1 = not queried yet + + GLModel m_tile_outline_glmodel; // the texture's first tile, [0,1]^2 - the "you are here" + GLModel m_grid_glmodel; + float m_grid_step = 0.f; // the UV step m_grid_glmodel was built for; 0 = not built + + float m_tiling_scale = 1.f; + float m_rotation_deg = 0.f; + bool m_tile_enabled = true; + bool m_tile_mirrored = false; + bool m_snap_enabled = false; + + std::vector m_background_pixels; // RGBA, expanded from the grayscale input + int m_background_width = 0, m_background_height = 0; + bool m_background_dirty = false; // the pixels need (re)uploading + bool m_background_quad_dirty = true; // only the quad's extent changed + GLTexture m_background_texture; + // Covers content_bounds(), not just [0,1]: the unwrap is packed in mm and then divided by the + // layer's tile size, so it routinely spans many tiles, and a single-unit-square backdrop would + // leave most of the islands sitting over bare background. Texcoord == position, so the GL wrap + // mode repeats it exactly the way DecodedHeightTexture::sample() does. + GLModel m_background_glmodel; + + // 2D pan/zoom. m_pan is the UV-space point at the center of the view, m_zoom half the UV-space + // extent visible across the shorter screen edge. v runs *down* the screen, matching both the + // texture's own row order and every other UV editor's convention. + Vec2f m_pan = Vec2f(0.5f, 0.5f); + float m_zoom = 0.75f; + bool m_needs_fit = true; // fit the view to the next unwrap that arrives + + enum class Gesture + { + None, + Pan, + MoveIsland, + RotateIsland, // right-drag: rotation tracks the mouse, ends when the button is released + RotateIslandModal, // 'R': rotation tracks the mouse until a click confirms or Esc cancels + ScaleIslandModal, // 'S': likewise, distance from the centre drives the scale + MoveVertex, // Vertex mode: drag one unwrapped vertex + MoveEdge, // Edge mode: drag both endpoints of one boundary edge + }; + Gesture m_gesture = Gesture::None; + + SelectMode m_select_mode = SelectMode::Island; + // The sub-element being edited in Vertex/Edge mode (unwrapped-vertex indices), or -1/{-1,-1}. This + // is the *primary* (last-picked) element of the multi-selection below. + int m_active_vertex = -1; + std::pair m_active_edge{ -1, -1 }; + // Multi-selection for Vertex/Edge modes, mirroring the island selection: plain click replaces, Shift + // adds, Ctrl toggles, and a drag moves the whole set together. m_active_vertex/m_active_edge stay the + // primary. Kept as small vectors (tiny, and order doesn't matter here). + std::vector m_sel_vertices; + std::vector> m_sel_edges; + bool is_vertex_selected(int v) const + { + return std::find(m_sel_vertices.begin(), m_sel_vertices.end(), v) != m_sel_vertices.end(); + } + bool is_edge_selected(const std::pair &e) const + { + return std::find(m_sel_edges.begin(), m_sel_edges.end(), e) != m_sel_edges.end(); + } + // Unique unwrapped-vertex endpoints of every selected edge (an endpoint shared by two selected edges + // is returned once, so a drag doesn't move it twice). + std::vector selected_edge_endpoints() const; + // Last known mouse position over the canvas, and whether the pointer is currently inside it. Used to + // draw the +/- add/remove sign next to the cursor in Vertex/Edge mode. + wxPoint m_cursor_px{ 0, 0 }; + bool m_cursor_inside = false; + // Set once a Vertex/Edge drag actually moves, so a bare click (select without drag) doesn't commit a + // no-op edit and take an undo snapshot for nothing. + bool m_vertex_edit_moved = false; + // Lazily-built small filled square, drawn at an edited/hovered vertex as a handle. + GLModel m_vertex_marker_glmodel; + // What a click would grab right now, highlighted so the user sees the target before clicking. + int m_hover_island = -1; + int m_hover_vertex = -1; + std::pair m_hover_edge{ -1, -1 }; + int m_selected_island = -1; + // The full multi-selection; m_selected_island is its primary (last-clicked) member. Kept as a small + // vector rather than a set because it is tiny and iteration order (primary last) is convenient. + std::vector m_selection; + bool is_selected(int island) const + { + return std::find(m_selection.begin(), m_selection.end(), island) != m_selection.end(); + } + wxPoint m_drag_last_px; + Vec2f m_gesture_last_uv = Vec2f::Zero(); + float m_gesture_last_angle = 0.f; + float m_gesture_last_dist = 0.f; + // Rotation is tracked as two running totals over the gesture: the raw mouse rotation, and how much + // has actually been applied. With Shift held the applied total is quantised to 15-degree steps + // (Blender-style angle snapping), so the two diverge - and driving the applied total off the raw + // one, rather than snapping each incremental delta, is what makes the snap stable instead of + // juddering. The raw/applied split also survives crossing +/-180 degrees, which a single wrapped + // angle would not. m_rot_applied doubles as the modal-rotate undo amount for Esc. + float m_rot_raw_deg = 0.f; + float m_rot_applied_deg = 0.f; + // The island's absolute on-screen rotation when the gesture began (decoded from its transform), so + // Shift can snap to *global* 15-degree marks (0/15/30...) rather than 15 degrees relative to + // wherever the island happened to start (#10). Also drives the angle read-out and the dial. + float m_rot_base_deg = 0.f; + float m_rot_display_deg = 0.f; // current absolute angle, for the status line and dial needle + float m_modal_scale_accum = 1.f; // so Esc can undo exactly what the modal scale applied, as a factor + + // A protractor drawn around the island while it rotates: a ring, a tick every 15 degrees, and a + // needle at the current angle, so the rotation is legible (#11). Rebuilt each frame during a + // rotation gesture (cheap: a few hundred short lines) and left empty otherwise. + GLModel m_dial_glmodel; + void rebuild_rotation_dial(); + // The island's current absolute rotation in degrees, decoded from its transform's first column. + float island_rotation_deg(int island) const; + + IslandEditFn m_on_island_edit; + UVVertexEditFn m_on_vertex_edit; + CommandFn m_on_command; + StatusFn m_on_status; + PaneState m_pane_state; + PaneStateFn m_on_pane_state; +}; + +class UVToolButton; // a drawn icon button, defined in UVEditorCanvas.cpp + +// The UV editor pane that goes into Plater's "uv_editor" AUI pane: a header with the active layer, the +// canvas background and Unwrap; a settings row with the unwrap's seam angle and island connection; a +// narrow tool strip down the left (selection mode, seams, island actions, snap and framing); the canvas; +// and a status line naming the current gesture, with the unwrap summary on its right. +// +// It holds no editing state of its own. Every control sends a Command through the canvas to the gizmo, +// and the gizmo pushes PaneState back, from which the controls are redrawn (see apply_state()). The gizmo +// still talks to the inner canvas, reached via canvas(). +class UVEditorPanel : public wxPanel +{ +public: + explicit UVEditorPanel(wxWindow *parent); + UVEditorCanvas *canvas() { return m_canvas; } + +private: + void on_tool(wxCommandEvent &evt); + void apply_state(const UVEditorCanvas::PaneState &state); + // Selection-mode toggles, and the tools that need a selected island, follow the canvas. + void refresh_selection_tools(); + + UVEditorCanvas *m_canvas = nullptr; + + UVToolButton *m_thumb = nullptr; // the layer's texture; a click opens the texture library + wxStaticText *m_layer_name = nullptr; + wxStaticText *m_tile = nullptr; + UVToolButton *m_background[3]{}; + UVToolButton *m_unwrap = nullptr; + + ::SpinInput *m_seam_angle = nullptr; + ::CheckBox *m_connect = nullptr; + + UVToolButton *m_select[3]{}; + UVToolButton *m_mark_seams = nullptr; + UVToolButton *m_seam_path = nullptr; + UVToolButton *m_clear_seams = nullptr; + UVToolButton *m_avg_scale = nullptr; + UVToolButton *m_cut = nullptr; + UVToolButton *m_join = nullptr; + UVToolButton *m_unjoin = nullptr; + UVToolButton *m_clear_edits = nullptr; + UVToolButton *m_snap = nullptr; + UVToolButton *m_frame = nullptr; + + wxStaticText *m_status = nullptr; + wxStaticText *m_stats = nullptr; +}; + +} // namespace Slic3r::GUI + +#endif // slic3r_UVEditorCanvas_hpp_ diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp index bf9dc7b7f6..cb353761dd 100644 --- a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp +++ b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp @@ -830,7 +830,9 @@ int OrcaCloudServiceAgent::user_logout(bool request) } } - clear_session(); + // An explicit logout also wipes the backend the token storage option is not using, so a token + // stranded by switching that option cannot sign the account back in later. + clear_session(/*all_backends=*/request); return BAMBU_NETWORK_SUCCESS; } @@ -1863,7 +1865,9 @@ void OrcaCloudServiceAgent::persist_user_secret(const std::string& secret) } } - (void) stored; + if (stored) { + secret_stored = true; + } } bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret) @@ -1903,6 +1907,7 @@ bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret) } if (integrity_ok && aes256gcm_decrypt(encoded_payload, key, plain) && !plain.empty()) { + secret_stored = true; out_secret = plain; // Upgrade legacy payloads to signed format if (payload.rfind("v2:", 0) != 0) { @@ -1920,6 +1925,7 @@ bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret) if (store.Load(SECRET_STORE_SERVICE, username, secret) && secret.IsOk()) { out_secret.assign(static_cast(secret.GetData()), secret.GetSize()); if (!out_secret.empty()) { + secret_stored = true; return true; } } @@ -1929,11 +1935,20 @@ bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret) return false; } -void OrcaCloudServiceAgent::clear_user_secret() +void OrcaCloudServiceAgent::clear_user_secret(bool all_backends) { - wxSecretStore store = wxSecretStore::GetDefault(); - if (store.IsOk()) { - store.Delete(SECRET_STORE_SERVICE); + // Nothing this process loaded or saved: leave the store alone. Deleting would only cost a + // keychain round trip (or a hang while the keychain is unresponsive) and could remove a + // login another instance just saved. + if (!secret_stored.exchange(false) && !all_backends) { + return; + } + + if (all_backends || !m_use_encrypted_token_file) { + wxSecretStore store = wxSecretStore::GetDefault(); + if (store.IsOk()) { + store.Delete(SECRET_STORE_SERVICE); + } } compute_fallback_path(); @@ -2282,7 +2297,7 @@ bool OrcaCloudServiceAgent::set_user_session(const json& session_json, bool noti return success; } -void OrcaCloudServiceAgent::clear_session() +void OrcaCloudServiceAgent::clear_session(bool all_backends) { if (mqtt_connection) { mqtt_connection->stop(); @@ -2292,7 +2307,7 @@ void OrcaCloudServiceAgent::clear_session() std::lock_guard lock(session_mutex); session = SessionInfo{}; } - clear_user_secret(); + clear_user_secret(all_backends); } // ============================================================================ diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.hpp b/src/slic3r/Utils/OrcaCloudServiceAgent.hpp index c999dbeea2..6701b397c1 100644 --- a/src/slic3r/Utils/OrcaCloudServiceAgent.hpp +++ b/src/slic3r/Utils/OrcaCloudServiceAgent.hpp @@ -352,7 +352,7 @@ public: void persist_user_secret(const std::string& secret); bool load_user_secret(std::string& out_secret); - void clear_user_secret(); + void clear_user_secret(bool all_backends = false); // Token refresh helpers bool refresh_if_expiring(std::chrono::seconds skew, const std::string& reason); @@ -370,7 +370,7 @@ public: bool persist = true); // Accepts either nested Orca cloud / GoTrue session JSON or flat WebView token JSON. bool set_user_session(const nlohmann::json& session_json, bool notify_login = true); - void clear_session(); + void clear_session(bool all_backends = false); static std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = ""); @@ -465,6 +465,11 @@ private: // Member variables - auth state PkceBundle pkce_bundle; std::string secret_fallback_path; + // Set once this process has read a secret from the store or written one. Unless the user logs + // out explicitly, clear_user_secret() only touches the store while it is set, so a logged-out + // instance (the GUI polls the login status every 2 s) makes no keychain calls and cannot wipe + // a login another instance saved. + std::atomic_bool secret_stored{false}; SessionHandler session_handler; OnLoginCompleteHandler on_login_complete_handler; SessionInfo session; diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt index a061c5be76..822844f50b 100644 --- a/tests/fff_print/CMakeLists.txt +++ b/tests/fff_print/CMakeLists.txt @@ -23,6 +23,7 @@ add_executable(${_TEST_NAME}_tests test_skirt_brim.cpp test_slicing_pipeline_hook.cpp test_support_material.cpp + test_texture_displacement.cpp test_tree_support.cpp test_trianglemesh.cpp test_wipe.cpp diff --git a/tests/fff_print/test_multifilament.cpp b/tests/fff_print/test_multifilament.cpp index c39efaccf5..445b1c0694 100644 --- a/tests/fff_print/test_multifilament.cpp +++ b/tests/fff_print/test_multifilament.cpp @@ -893,3 +893,65 @@ TEST_CASE("Each filament sets the pressure advance of its extruder variant on a CHECK(gcode.find("; start pressure advance " + pressure_advance + "\n") != std::string::npos); } } + +// The speeds, in percent, a G-code turns a fan on at: the part cooling fan for `M106 S`, the auxiliary +// fan for `M106 P2 S`. +static std::set fan_speeds(const std::string &gcode, const std::string &command) +{ + std::set speeds; + std::istringstream stream(gcode); + for (std::string line; std::getline(stream, line);) + if (line.rfind(command, 0) == 0) + if (const int pwm = std::stoi(line.substr(command.size())); pwm > 0) + speeds.insert(int(std::lround(pwm * 100. / 255.))); + return speeds; +} + +// The fan speeds and the recommended nozzle temperature range are tuned per extruder variant like the +// other filament variant settings. +TEST_CASE("Each filament cools with the fan speeds of its extruder variant", "[MultiFilament]") +{ + auto [nozzle_volume_type, filament, fan_min_speed, fan_max_speed, additional_fan_speed, range_high] = GENERATE(table({ + { nvtStandard, 1, 15, 25, 10, 240 }, + { nvtHighFlow, 1, 35, 45, 20, 260 }, + { nvtHighFlow, 2, 55, 65, 40, 280 }, // filament 2 defines no High Flow variant + })); + // Layers printed faster than slow_down_layer_time run the fan at its maximum speed, layers slower than + // fan_cooling_layer_time at its minimum. + const bool fast_layers = GENERATE(false, true); + DYNAMIC_SECTION(get_nozzle_volume_type_string(nozzle_volume_type) << " nozzle, filament " << filament << (fast_layers ? ", fast layers" : ", slow layers")) { + DynamicPrintConfig config = multifilament_config(2, { + { "extruder_variant_list", "Direct Drive Standard,Direct Drive High Flow" }, + // filament 1 defines Standard and High Flow, filament 2 Standard + { "filament_extruder_variant", "Direct Drive Standard;Direct Drive High Flow;Direct Drive Standard" }, + { "filament_self_index", "1,1,2" }, + { "fan_min_speed", "15,35,55" }, + { "fan_max_speed", "25,45,65" }, + { "additional_cooling_fan_speed", "10,20,40" }, + { "nozzle_temperature_range_high", "240,260,280" }, + { "auxiliary_fan", 1 }, + { "reduce_fan_stop_start_freq", "1,1" }, + { "slow_down_layer_time", fast_layers ? "1000,1000" : "0,0" }, + { "fan_cooling_layer_time", fast_layers ? "1000,1000" : "0,0" }, + { "slow_down_for_layer_cooling", "0,0" }, + { "enable_overhang_bridge_fan", "0,0" }, + { "sparse_infill_filament_id", filament }, + { "internal_solid_filament_id", filament }, + { "top_surface_filament_id", filament }, + { "bottom_surface_filament_id", filament }, + { "outer_wall_filament_id", filament }, + { "inner_wall_filament_id", filament }, + { "enable_prime_tower", 0 }, + { "skirt_loops", 0 }, + { "brim_type", "no_brim" }, + // custom G-code indexes the per-filament arrays by filament + { "machine_start_gcode", "; start range high {nozzle_temperature_range_high[initial_extruder]}" }, + }); + config.option("nozzle_volume_type", true)->values = { nozzle_volume_type }; + const std::string gcode = slice({ cube(20) }, config); + + CHECK(fan_speeds(gcode, "M106 S") == std::set{ fast_layers ? fan_max_speed : fan_min_speed }); + CHECK(fan_speeds(gcode, "M106 P2 S") == std::set{ additional_fan_speed }); + CHECK(gcode.find("; start range high " + std::to_string(range_high) + "\n") != std::string::npos); + } +} diff --git a/tests/fff_print/test_texture_displacement.cpp b/tests/fff_print/test_texture_displacement.cpp new file mode 100644 index 0000000000..459a42d185 --- /dev/null +++ b/tests/fff_print/test_texture_displacement.cpp @@ -0,0 +1,149 @@ +#include + +#include +#include + +#include + +#include "libslic3r/Model.hpp" +#include "libslic3r/PNGReadWrite.hpp" +#include "libslic3r/Print.hpp" +#include "libslic3r/TextureDisplacement.hpp" +#include "libslic3r/TriangleSelector.hpp" + +#include "test_helpers.hpp" + +using namespace Slic3r; +using namespace Slic3r::Test; + +// What a bake leaves behind has to survive the slicer. The bake itself is covered in the libslic3r +// suite; these tests carry its result through Print::process and the G-code, which is where a mesh +// the slicer cannot use shows up - as a crash, or as a print with nothing in it. +namespace { + +// A striped height map, so the relief has real steps in it rather than a flat offset. +std::shared_ptr> stripes_png(size_t w = 32, size_t h = 32) +{ + std::vector pixels(w * h); + for (size_t y = 0; y < h; ++y) + for (size_t x = 0; x < w; ++x) + pixels[y * w + x] = ((x / 4) % 2 == 0) ? 20 : 235; + + const boost::filesystem::path tmp_path = boost::filesystem::temp_directory_path() / + boost::filesystem::unique_path("texdisp_slice_%%%%%%%%.png"); + REQUIRE(Slic3r::png::write_gray_to_file(tmp_path.string(), w, h, pixels)); + std::vector bytes; + { + std::ifstream ifs(tmp_path.string(), std::ios::binary); + bytes.assign(std::istreambuf_iterator(ifs), std::istreambuf_iterator()); + } + boost::system::error_code ec; + boost::filesystem::remove(tmp_path, ec); + REQUIRE_FALSE(bytes.empty()); + return std::make_shared>(std::move(bytes)); +} + +// Paints every facet of `volume` with the texture layer in slot 0, as painting over the whole model +// does, and gives the layer a relief to bake. +void paint_whole_volume(ModelVolume &volume, float depth_mm, float tile_mm) +{ + TextureDisplacementLayer layer; + layer.slot = 0; + layer.image_data = stripes_png(); + layer.depth_mm = depth_mm; + layer.tiling_scale = tile_mm; + volume.texture_displacement_layers = { layer }; + + TriangleSelector selector(volume.mesh()); + for (int i = 0; i < int(volume.mesh().its.indices.size()); ++i) + selector.set_facet(i, EnforcerBlockerType::ENFORCER); + volume.texture_displacement_facet(0).set(selector); +} + +// The bake as the gizmo's job commits it: the mesh is replaced, the hull recomputed and the paint of +// the baked layer cleared. `budget_k` and `resolution_mm` stand in for the two panel controls. +size_t bake_into_volume(ModelVolume &volume, int budget_k, float resolution_mm) +{ + TextureDisplacementOptions options; + options.pipeline_v2 = true; + options.v2_refine_mm = resolution_mm; + options.v2_max_triangles_k = budget_k; + + TextureDisplacementFacetsData facets; + for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) + facets[size_t(i)] = volume.texture_displacement_facet(i).get_data(); + + TriangleMesh baked(build_texture_displacement(volume.mesh().its, volume.texture_displacement_layers, facets, options)); + REQUIRE_FALSE(baked.empty()); + + volume.set_mesh(std::move(baked)); + volume.set_new_unique_id(); + volume.calculate_convex_hull(); + for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) + volume.texture_displacement_facet(i).reset(); + return volume.mesh().its.indices.size(); +} + +// Extruding moves in the G-code: what tells a print with toolpaths from one without. +size_t count_extrusions(const std::string &gcode_text) +{ + size_t count = 0; + for (size_t pos = 0; (pos = gcode_text.find("\nG1 ", pos)) != std::string::npos; ++pos) { + const size_t eol = gcode_text.find('\n', pos + 1); + if (gcode_text.find(" E", pos) < eol) + ++count; + } + return count; +} + +} // namespace + +TEST_CASE("A baked texture slices into a print with toolpaths", "[TextureDisplacement]") +{ + Model model = Slic3r::Test::model("cube", Test::cube(20.)); + ModelVolume &volume = *model.objects.front()->volumes.front(); + paint_whole_volume(volume, /* depth */ 0.4f, /* tile */ 8.f); + + // Coarse on purpose: this test is about the result reaching the slicer, not about how fine it is. + const size_t baked_triangles = bake_into_volume(volume, /* budget */ 60, /* resolution */ 0.8f); + CHECK(baked_triangles > Test::cube(20.).its.indices.size()); // the relief added geometry + + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ { "layer_height", "0.2" }, { "initial_layer_print_height", "0.2" } }); + + Print print; + Model sliced_model; + init_print({ volume.mesh() }, print, sliced_model, config); + REQUIRE(print.objects().size() == 1); + + print.process(); + const std::string gcode_text = Test::gcode(print); + + REQUIRE_FALSE(gcode_text.empty()); + CHECK(count_extrusions(gcode_text) > 1000); + CHECK(print.objects().front()->layer_count() > 10); +} + +TEST_CASE("A baked texture slices the same whether or not the budget capped it", "[TextureDisplacement]") +{ + // The budget decides how much of the relief survives, and a capped bake goes through the + // simplification and the repair that follow it. Both have to leave a mesh the slicer can print. + const int budget_k = GENERATE(10, 400); + + Model model = Slic3r::Test::model("cube", Test::cube(20.)); + ModelVolume &volume = *model.objects.front()->volumes.front(); + paint_whole_volume(volume, /* depth */ 0.4f, /* tile */ 8.f); + bake_into_volume(volume, budget_k, /* resolution */ 0.5f); + + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ { "layer_height", "0.2" }, { "initial_layer_print_height", "0.2" } }); + + Print print; + Model sliced_model; + init_print({ volume.mesh() }, print, sliced_model, config); + print.process(); + + const std::string gcode_text = Test::gcode(print); + REQUIRE_FALSE(gcode_text.empty()); + CHECK(count_extrusions(gcode_text) > 1000); +} diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 911e23d5c7..3f2938d568 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -56,6 +56,7 @@ add_executable(${_TEST_NAME}_tests test_ordering_strategies.cpp # test_png_io.cpp test_indexed_triangle_set.cpp + test_texture_displacement.cpp ../libnest2d/printer_parts.cpp ) diff --git a/tests/libslic3r/test_config.cpp b/tests/libslic3r/test_config.cpp index 0da7b7bafe..00c4d6d170 100644 --- a/tests/libslic3r/test_config.cpp +++ b/tests/libslic3r/test_config.cpp @@ -1289,3 +1289,73 @@ TEST_CASE("Static print configs compare, order and hash by their option values", REQUIRE(c.optptr("gcode_flavor") == &c.gcode_flavor); } } + +namespace { + +// Keys whose values differ between two full configs, compared as text so enum names count too. +std::vector differing_keys(const FullPrintConfig &a, const FullPrintConfig &b) +{ + std::vector keys; + for (const std::string &key : a.keys()) + if (a.opt_serialize(key) != b.opt_serialize(key)) + keys.push_back(key); + return keys; +} + +// Applies source to one full config member by member and to another key by key, as apply() did before +// static configs could apply themselves. +template void check_member_apply_matches_key_apply(const Source &source) +{ + FullPrintConfig by_member; + FullPrintConfig by_key; + by_member.apply(source); + by_key.apply_only(source, source.keys()); + CHECK(differing_keys(by_member, by_key).empty()); + CHECK_FALSE(differing_keys(by_member, FullPrintConfig()).empty()); +} + +} // namespace + +TEST_CASE("A static config applies itself onto a config of its type as a lookup by name would", "[Config]") +{ + SECTION("region config") + { + PrintRegionConfig region; + region.sparse_infill_pattern.value = ipGyroid; + region.outer_wall_speed.values = {37.}; + region.sparse_infill_density.value = 35.; + FullPrintConfig full; + REQUIRE(region.apply_to(full)); + check_member_apply_matches_key_apply(region); + } + SECTION("object config") + { + PrintObjectConfig object; + object.seam_position.value = spRear; + object.wall_generator.value = PerimeterGeneratorType::Arachne; + object.support_speed.values = {33.}; + object.enable_support.value = true; + FullPrintConfig full; + REQUIRE(object.apply_to(full)); + check_member_apply_matches_key_apply(object); + } + SECTION("G-code config, whose enum lists carry their names through a keys map") + { + GCodeConfig gcode; + gcode.z_hop_types.values = {int(zhtSpiral)}; + gcode.retraction_length.values = {1.5}; + FullPrintConfig full; + REQUIRE(gcode.apply_to(full)); + check_member_apply_matches_key_apply(gcode); + } +} + +TEST_CASE("A static config applied onto a config of another type falls back to a lookup by name", "[Config]") +{ + PrintRegionConfig region; + region.sparse_infill_pattern.value = ipGyroid; + DynamicPrintConfig dynamic; + REQUIRE_FALSE(region.apply_to(dynamic)); + dynamic.apply(region); + CHECK(dynamic.opt_serialize("sparse_infill_pattern") == "gyroid"); +} diff --git a/tests/libslic3r/test_config_variant_expansion.cpp b/tests/libslic3r/test_config_variant_expansion.cpp index de19e8219c..f30ad985e3 100644 --- a/tests/libslic3r/test_config_variant_expansion.cpp +++ b/tests/libslic3r/test_config_variant_expansion.cpp @@ -1,6 +1,7 @@ #include #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/Preset.hpp" using namespace Slic3r; @@ -30,6 +31,61 @@ void add_print_variant_columns(DynamicPrintConfig &config) } // namespace +TEST_CASE("Filament cooling and pressure settings follow the selected nozzle variant", "[Config][FilamentVariants]") +{ + struct Setting { + const char *key; + const char *standard; + const char *high_flow; + }; + const Setting settings[] = { + {"enable_pressure_advance", "0", "1"}, + {"pressure_advance", "0.04", "0.02"}, + {"fan_min_speed", "15", "10"}, + {"fan_max_speed", "15", "60"}, + {"additional_cooling_fan_speed", "70", "0"}, + {"filament_minimal_purge_on_wipe_tower", "0", "15"}, + {"filament_multitool_ramming", "0", "1"}, + {"filament_multitool_ramming_volume", "0", "5"}, + {"filament_multitool_ramming_flow", "10", "30"}, + {"nozzle_temperature_range_low", "190", "210"}, + {"nozzle_temperature_range_high", "255", "280"}, + }; + const int variant_index = GENERATE(0, 1, 2, 3); + const bool load_preset = GENERATE(false, true); + const bool high_flow = variant_index % 2 != 0; + for (const Setting &setting : settings) { + DYNAMIC_SECTION(setting.key << " variant=" << variant_index << " loaded=" << load_preset) { + DynamicPrintConfig printer = make_hybrid_printer_config(); + if (variant_index >= 2) + printer.option("extruder_type")->values[1] = etBowden; + DynamicPrintConfig filament; + filament.option("filament_extruder_variant", true)->values = + {"Direct Drive Standard", "Direct Drive High Flow", "Bowden Standard", "Bowden High Flow"}; + // one value per variant of filament_extruder_variant + const std::string values = std::string(setting.standard) + "," + setting.high_flow + "," + setting.standard + "," + setting.high_flow; + REQUIRE(filament.option(setting.key, true)->deserialize(values)); + + if (load_preset) { + extend_default_config_length(filament, false, DynamicPrintConfig::full_print_config()); + DynamicPrintConfig expanded; + REQUIRE(expanded.option(setting.key, true)->deserialize(values)); + REQUIRE(*filament.option(setting.key) == *expanded.option(setting.key)); + } + + std::vector> nozzle_types; + const int count = printer.get_extruder_nozzle_volume_count(2, nozzle_types); + filament.update_values_to_printer_extruders(printer, 2, count, nozzle_types, + filament_options_with_variant, "", "filament_extruder_variant", 1, 2, + high_flow ? nvtHighFlow : nvtStandard); + + DynamicPrintConfig expected; + REQUIRE(expected.option(setting.key, true)->deserialize(high_flow ? setting.high_flow : setting.standard)); + REQUIRE(*filament.option(setting.key) == *expected.option(setting.key)); + } + } +} + TEST_CASE("apply_override fills nil entries from the 0-based default index", "[Config]") { ConfigOptionFloats machine({10., 20., 30.}); @@ -408,6 +464,8 @@ TEST_CASE("update_values_to_printer_extruders_for_multiple_filaments resolves pe config.option("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow", "Direct Drive Standard", "Direct Drive High Flow"}; config.option("filament_max_volumetric_speed", true)->values = {12., 20., 13., 21.}; + config.option("fan_max_speed", true)->values = {15., 60., 20., 80.}; + config.option("filament_multitool_ramming", true)->values = {false, true, false, true}; }; std::set filament_keys = filament_options_with_variant; @@ -427,6 +485,8 @@ TEST_CASE("update_values_to_printer_extruders_for_multiple_filaments resolves pe "filament_self_index", "filament_extruder_variant"); REQUIRE(config.option("filament_max_volumetric_speed")->values == std::vector({12., 21.})); + REQUIRE(config.option("fan_max_speed")->values == std::vector({15., 80.})); + REQUIRE(config.option("filament_multitool_ramming")->values == std::vector({0, 1})); REQUIRE(config.option("filament_extruder_variant")->values == std::vector({"Direct Drive Standard", "Direct Drive High Flow"})); REQUIRE(config.option("filament_self_index")->values == std::vector({1, 2})); diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index b1ec3903bb..addc33b468 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -5940,3 +5940,19 @@ TEST_CASE("A project's different settings always keep the preset bookkeeping key CHECK(keys.count(key) == 1); CHECK(PresetBundle::project_different_keys(std::string()).count("inherits") == 1); } + +// A connected machine's tray is checked against the filament variant of the nozzle it feeds. +TEST_CASE("A filament's variant index follows the extruder type and nozzle volume type", "[Preset]") +{ + DynamicPrintConfig filament; + filament.option("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow", + "Bowden Standard"}; + DynamicPrintConfig printer; + printer.option("extruder_type", true)->values = {etDirectDrive, etBowden}; + + CHECK(PresetBundle::get_filament_variant_index(filament, printer, 0, nvtStandard) == 0); + CHECK(PresetBundle::get_filament_variant_index(filament, printer, 0, nvtHighFlow) == 1); + CHECK(PresetBundle::get_filament_variant_index(filament, printer, 1, nvtStandard) == 2); + // the filament defines no Bowden High Flow variant + CHECK(PresetBundle::get_filament_variant_index(filament, printer, 1, nvtHighFlow) == 0); +} diff --git a/tests/libslic3r/test_texture_displacement.cpp b/tests/libslic3r/test_texture_displacement.cpp new file mode 100644 index 0000000000..04713d3fd9 --- /dev/null +++ b/tests/libslic3r/test_texture_displacement.cpp @@ -0,0 +1,2114 @@ +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "libslic3r/AABBMesh.hpp" +#include "libslic3r/TextureDisplacement.hpp" +#include "libslic3r/TextureBake/TextureBakeDecimate.hpp" +#include "libslic3r/TextureBake/TextureBakeFlip.hpp" +#include "libslic3r/TextureBake/TextureBakeMesh.hpp" +#include "libslic3r/TriangleMesh.hpp" +#include "libslic3r/TriangleSelector.hpp" +#include "libslic3r/PNGReadWrite.hpp" + +using namespace Slic3r; +using Catch::Matchers::WithinAbs; + +// Encodes a flat (uniform-value) grayscale image through Slic3r's own PNG writer/reader round +// trip, so decode_height_texture() (which only accepts true 8-bit grayscale PNG) is guaranteed a +// compatible file, exactly like the GUI's "Add texture" import path does. +static std::shared_ptr> make_flat_gray_png(uint8_t value, size_t w = 4, size_t h = 4) +{ + std::vector pixels(w * h, value); + const boost::filesystem::path tmp_path = boost::filesystem::temp_directory_path() + / boost::filesystem::unique_path("texdisp_test_%%%%%%%%.png"); + REQUIRE(Slic3r::png::write_gray_to_file(tmp_path.string(), w, h, pixels)); + + std::vector bytes; + { + std::ifstream ifs(tmp_path.string(), std::ios::binary); + bytes.assign(std::istreambuf_iterator(ifs), std::istreambuf_iterator()); + } + boost::system::error_code ec; + boost::filesystem::remove(tmp_path, ec); + + REQUIRE_FALSE(bytes.empty()); + return std::make_shared>(std::move(bytes)); +} + +// A hard-edged black/white checkerboard, the worst case for a height map: every texel boundary is a +// step, which is precisely the relief the post-process smoothing exists to round off. +static std::shared_ptr> make_checkerboard_png(size_t w = 16, size_t h = 16) +{ + std::vector pixels(w * h); + for (size_t y = 0; y < h; ++y) + for (size_t x = 0; x < w; ++x) + pixels[y * w + x] = ((x / 2 + y / 2) % 2) ? 255 : 0; + const boost::filesystem::path tmp_path = boost::filesystem::temp_directory_path() + / boost::filesystem::unique_path("texdisp_test_%%%%%%%%.png"); + REQUIRE(Slic3r::png::write_gray_to_file(tmp_path.string(), w, h, pixels)); + + std::vector bytes; + { + std::ifstream ifs(tmp_path.string(), std::ios::binary); + bytes.assign(std::istreambuf_iterator(ifs), std::istreambuf_iterator()); + } + boost::system::error_code ec; + boost::filesystem::remove(tmp_path, ec); + + REQUIRE_FALSE(bytes.empty()); + return std::make_shared>(std::move(bytes)); +} + +// The bake never drives relief below the model's own resting plane (see build_texture_displacement()), +// so on a fully painted closed solid the vertices already sitting on that plane - a cube's four bottom +// corners, whose normals point downwards - are clamped in Z and do not move by the full depth. The +// tests below are about the displacement maths, so they check the vertices the clamp cannot touch; +// the clamp itself has its own test. + +// The tests below check the classic, topology-preserving bake vertex by vertex, so they select it +// explicitly: the default pipeline rebuilds the topology and has no vertex correspondence to check. +static TextureDisplacementOptions classic_options() +{ + TextureDisplacementOptions o; + o.pipeline_v2 = false; + return o; +} + +static bool above_resting_plane(const indexed_triangle_set &mesh, size_t vi) +{ + float bottom = std::numeric_limits::max(); + for (const Vec3f &v : mesh.vertices) + bottom = std::min(bottom, v.z()); + return mesh.vertices[vi].z() > bottom + 1e-4f; +} + +TEST_CASE("TextureDisplacement: decode_height_texture round-trips an 8-bit grayscale PNG", "[TextureDisplacement]") +{ + TextureDisplacementLayer layer; + layer.image_data = make_flat_gray_png(128, 4, 4); + + DecodedHeightTexture tex = decode_height_texture(layer); + REQUIRE_FALSE(tex.empty()); + CHECK(tex.width == 4); + CHECK(tex.height == 4); + REQUIRE_THAT(tex.sample(Vec2f(0.5f, 0.5f)), WithinAbs(128.0 / 255.0, 1.0 / 255.0)); +} + +TEST_CASE("TextureDisplacement: an empty layer list leaves the mesh unchanged", "[TextureDisplacement]") +{ + const indexed_triangle_set cube = its_make_cube(10., 10., 10.); + const std::vector layers; // none + TextureDisplacementFacetsData facets{}; // all empty + + const indexed_triangle_set result = build_texture_displacement(cube, layers, facets, classic_options()); + + REQUIRE(result.vertices.size() == cube.vertices.size()); + REQUIRE(result.indices.size() == cube.indices.size()); + for (size_t i = 0; i < cube.vertices.size(); ++i) + for (int c = 0; c < 3; ++c) + CHECK(result.vertices[i](c) == cube.vertices[i](c)); +} + +TEST_CASE("TextureDisplacement: fully painting a mesh displaces every vertex along its own normal", "[TextureDisplacement]") +{ + const indexed_triangle_set cube = its_make_cube(10., 10., 10.); + const TriangleMesh cube_mesh(cube); + + TriangleSelector selector(cube_mesh); + for (int f = 0; f < int(cube.indices.size()); ++f) + selector.set_facet(f, EnforcerBlockerType::ENFORCER); + + TextureDisplacementFacetsData facets{}; + facets[0] = selector.serialize(); + + TextureDisplacementLayer layer; + layer.slot = 0; + layer.depth_mm = 2.0f; + layer.tiling_scale = 5.0f; + layer.image_data = make_flat_gray_png(255); // sample() == 1.0 everywhere -> full depth_mm displacement + + const indexed_triangle_set result = build_texture_displacement(cube, {layer}, facets, classic_options()); + + REQUIRE(result.vertices.size() == cube.vertices.size()); + for (size_t i = 0; i < cube.vertices.size(); ++i) { + if (!above_resting_plane(cube, i)) + continue; + const float moved = (result.vertices[i] - cube.vertices[i]).norm(); + CHECK_THAT(moved, WithinAbs(layer.depth_mm, 1e-3f)); + } +} + +// Paints every facet of `mesh` into a serialized mask, the way "Select whole model" does. +static TriangleSelector::TriangleSplittingData paint_whole_mesh(const indexed_triangle_set &mesh) +{ + const TriangleMesh tm(mesh); + TriangleSelector selector(tm); + for (int f = 0; f < int(mesh.indices.size()); ++f) + selector.set_facet(f, EnforcerBlockerType::ENFORCER); + return selector.serialize(); +} + +// Paints a disc of the top face of `mesh` (the plane z = `top`) the way the gizmo's brush does: the +// selector splits the triangles under it, so the stroke need not follow the mesh. +static TriangleSelector::TriangleSplittingData paint_top_disc(const indexed_triangle_set &mesh, float top, + const Vec2f ¢re, float radius) +{ + const TriangleMesh tm(mesh); + const Vec3f at(centre.x(), centre.y(), top); + int start = -1; + Vec3d foot; + AABBMesh(tm).squared_distance(at.cast(), start, foot); // the brush starts on the face under it + TriangleSelector selector(tm); + selector.select_patch(start, + TriangleSelector::SinglePointCursor::cursor_factory( + at, at + Vec3f(0.f, 0.f, 80.f), radius, TriangleSelector::CursorType::SPHERE, + Transform3d::Identity(), TriangleSelector::ClippingPlane()), + EnforcerBlockerType::ENFORCER, Transform3d::Identity(), /* triangle_splitting */ true); + return selector.serialize(); +} + +// Regression test for the bug this feature shipped with: with two layers painted over the same +// area, the second one was silently dropped (its paint mask was remapped onto the mesh the first +// layer had already displaced, which routinely produced an empty bitstream). Every layer is now +// evaluated against the original mesh instead, so both must show up in the total. +TEST_CASE("TextureDisplacement: a second layer over the same area is applied too", "[TextureDisplacement]") +{ + const indexed_triangle_set cube = its_make_cube(10., 10., 10.); + + TextureDisplacementFacetsData facets{}; + facets[0] = paint_whole_mesh(cube); + facets[1] = facets[0]; // both layers cover the whole cube + + TextureDisplacementLayer base; + base.slot = 0; + base.depth_mm = 1.0f; + base.tiling_scale = 5.0f; + base.image_data = make_flat_gray_png(255); // height 1.0 everywhere + + TextureDisplacementLayer second = base; + second.slot = 1; + second.depth_mm = 0.5f; + second.blend_mode = TextureBlendMode::Add; + + const indexed_triangle_set result = build_texture_displacement(cube, {base, second}, facets, classic_options()); + + // Topology is preserved exactly, so vertices can be compared 1:1 with the input. + REQUIRE(result.vertices.size() == cube.vertices.size()); + REQUIRE(result.indices.size() == cube.indices.size()); + for (size_t i = 0; i < cube.vertices.size(); ++i) + if (above_resting_plane(cube, i)) + CHECK_THAT((result.vertices[i] - cube.vertices[i]).norm(), WithinAbs(1.5f, 1e-3f)); // 1.0 + 0.5, not just 1.0 +} + +TEST_CASE("TextureDisplacement: blend modes combine a layer with the ones below it", "[TextureDisplacement]") +{ + const indexed_triangle_set cube = its_make_cube(10., 10., 10.); + + TextureDisplacementFacetsData facets{}; + facets[0] = paint_whole_mesh(cube); + facets[1] = facets[0]; + + TextureDisplacementLayer base; + base.slot = 0; + base.depth_mm = 2.0f; + base.tiling_scale = 5.0f; + base.image_data = make_flat_gray_png(255); // -> contributes exactly +2.0 mm + + TextureDisplacementLayer second = base; + second.slot = 1; + second.depth_mm = 0.5f; // -> its own value is 0.5 mm + + // Expected total displacement for each mode, given base = 2.0 mm and second = 0.5 mm. Multiply + // and Divide treat the layer's value as a factor relative to 1 mm (see TextureBlendMode). + const auto expected = GENERATE(table({ + { TextureBlendMode::Add, 2.5f }, // 2.0 + 0.5 + { TextureBlendMode::Subtract, 1.5f }, // 2.0 - 0.5 + { TextureBlendMode::Multiply, 1.0f }, // 2.0 * 0.5 + { TextureBlendMode::Divide, 4.0f }, // 2.0 / 0.5 + })); + second.blend_mode = std::get<0>(expected); + + const indexed_triangle_set result = build_texture_displacement(cube, {base, second}, facets, classic_options()); + + REQUIRE(result.vertices.size() == cube.vertices.size()); + for (size_t i = 0; i < cube.vertices.size(); ++i) + if (above_resting_plane(cube, i)) + CHECK_THAT((result.vertices[i] - cube.vertices[i]).norm(), WithinAbs(std::get<1>(expected), 1e-3f)); +} + +TEST_CASE("TextureDisplacement: the lowest layer ignores its blend mode", "[TextureDisplacement]") +{ + // Multiply against the implicit zero base would annihilate the only layer present; the first + // layer to reach a vertex always starts the total off additively instead. + const indexed_triangle_set cube = its_make_cube(10., 10., 10.); + + TextureDisplacementFacetsData facets{}; + facets[0] = paint_whole_mesh(cube); + + TextureDisplacementLayer layer; + layer.slot = 0; + layer.depth_mm = 2.0f; + layer.tiling_scale = 5.0f; + layer.blend_mode = TextureBlendMode::Multiply; + layer.image_data = make_flat_gray_png(255); + + const indexed_triangle_set result = build_texture_displacement(cube, {layer}, facets, classic_options()); + + for (size_t i = 0; i < cube.vertices.size(); ++i) + if (above_resting_plane(cube, i)) + CHECK_THAT((result.vertices[i] - cube.vertices[i]).norm(), WithinAbs(2.0f, 1e-3f)); +} + +TEST_CASE("TextureDisplacement: relief is never driven below the model's resting plane", "[TextureDisplacement]") +{ + // A fully painted cube displaces outward everywhere, which on the bottom face means straight + // down - through the build plate. That geometry cannot be printed, so it is clamped back up. + const indexed_triangle_set cube = its_make_cube(10., 10., 10.); + + TextureDisplacementFacetsData facets{}; + facets[0] = paint_whole_mesh(cube); + + TextureDisplacementLayer layer; + layer.slot = 0; + layer.depth_mm = 2.0f; + layer.tiling_scale = 5.0f; + layer.image_data = make_flat_gray_png(255); // full depth everywhere + + float bottom = std::numeric_limits::max(); + for (const Vec3f &v : cube.vertices) + bottom = std::min(bottom, v.z()); + + const indexed_triangle_set result = build_texture_displacement(cube, {layer}, facets, classic_options()); + + REQUIRE(result.vertices.size() == cube.vertices.size()); + for (const Vec3f &v : result.vertices) + CHECK(v.z() >= bottom - 1e-4f); + + // ...and the clamp is confined to Z: a bottom corner still moves outwards in X and Y by the same + // amount it would have, rather than being pinned wholesale. + bool any_bottom_moved_sideways = false; + for (size_t i = 0; i < cube.vertices.size(); ++i) + if (!above_resting_plane(cube, i) && + (result.vertices[i].head<2>() - cube.vertices[i].head<2>()).norm() > 1e-3f) + any_bottom_moved_sideways = true; + CHECK(any_bottom_moved_sideways); +} + +TEST_CASE("TextureDisplacement: depth is measured in world millimetres, not the volume's own", "[TextureDisplacement]") +{ + // The same painted patch, baked once untransformed and once through a 3x scale. "Depth (mm)" is + // a millimetre on the printed part, so the *world* relief must come out the same height either + // way - which means the vertices of the scaled volume move by a third as much in its own + // coordinates. Baking both in volume space instead gave a 3x deeper relief on the scaled one. + indexed_triangle_set fan; + fan.vertices = { {0.f, 0.f, 1.f}, {1.f, 0.f, 1.f}, {0.f, 1.f, 1.f}, {-1.f, 0.f, 1.f}, {0.f, -1.f, 1.f} }; + fan.indices = { {0, 1, 2}, {0, 2, 3}, {0, 3, 4}, {0, 4, 1} }; + + TextureDisplacementFacetsData facets{}; + facets[0] = paint_whole_mesh(fan); + + TextureDisplacementLayer layer; + layer.slot = 0; + layer.depth_mm = 1.0f; + layer.tiling_scale = 5.0f; + layer.image_data = make_flat_gray_png(255); + + const indexed_triangle_set plain = build_texture_displacement(fan, {layer}, facets, classic_options()); + Transform3d scale3 = Transform3d::Identity(); + scale3.scale(Vec3d(3.0, 3.0, 3.0)); + const indexed_triangle_set scaled = build_texture_displacement(fan, {layer}, facets, classic_options(), {}, nullptr, scale3); + + REQUIRE(plain.vertices.size() == fan.vertices.size()); + REQUIRE(scaled.vertices.size() == fan.vertices.size()); + for (size_t i = 0; i < fan.vertices.size(); ++i) { + CHECK_THAT(plain.vertices[i].z() - fan.vertices[i].z(), WithinAbs(1.0f, 1e-3f)); + // A third of the movement locally is the same movement once the 3x scale is applied. + CHECK_THAT(scaled.vertices[i].z() - fan.vertices[i].z(), WithinAbs(1.0f / 3.0f, 1e-3f)); + } +} + +TEST_CASE("TextureDisplacement: a mirrored placement still raises the relief outwards", "[TextureDisplacement]") +{ + // Mirroring reverses the winding, and every normal in the bake is derived from the winding - so + // without correcting for it the whole relief is carved into the surface instead of raised off it. + indexed_triangle_set fan; + fan.vertices = { {0.f, 0.f, 1.f}, {1.f, 0.f, 1.f}, {0.f, 1.f, 1.f}, {-1.f, 0.f, 1.f}, {0.f, -1.f, 1.f} }; + fan.indices = { {0, 1, 2}, {0, 2, 3}, {0, 3, 4}, {0, 4, 1} }; + + TextureDisplacementFacetsData facets{}; + facets[0] = paint_whole_mesh(fan); + + TextureDisplacementLayer layer; + layer.slot = 0; + layer.depth_mm = 1.0f; + layer.tiling_scale = 5.0f; + layer.image_data = make_flat_gray_png(255); + + // Mirrored in X: the patch's outward direction in world space is still +Z, so in the volume's own + // coordinates the vertices must still move +Z. + Transform3d mirror_x = Transform3d::Identity(); + mirror_x.scale(Vec3d(-1.0, 1.0, 1.0)); + const indexed_triangle_set result = build_texture_displacement(fan, {layer}, facets, classic_options(), {}, nullptr, mirror_x); + + REQUIRE(result.vertices.size() == fan.vertices.size()); + for (size_t i = 0; i < fan.vertices.size(); ++i) + CHECK_THAT(result.vertices[i].z() - fan.vertices[i].z(), WithinAbs(1.0f, 1e-3f)); +} + +TEST_CASE("TextureDisplacement: the patch border is displaced by default and pinned on request", "[TextureDisplacement]") +{ + // A small triangle fan around a central vertex O, with 4 outer points A/B/C/D forming 4 + // triangles T0..T3 in the XY plane. Only T0, T1, T2 are painted, T3 is left unpainted, so: + // O: touches all 4 triangles (incl. unpainted T3) -> patch border + // A: touches T0 (painted) and T3 (unpainted) -> patch border + // D: touches T2 (painted) and T3 (unpainted) -> patch border + // B: touches only T0 and T1 (both painted) -> interior + // C: touches only T1 and T2 (both painted) -> interior + indexed_triangle_set fan; + fan.vertices = { {0.f, 0.f, 0.f}, {1.f, 0.f, 0.f}, {0.f, 1.f, 0.f}, {-1.f, 0.f, 0.f}, {0.f, -1.f, 0.f} }; + fan.indices = { {0, 1, 2}, {0, 2, 3}, {0, 3, 4}, {0, 4, 1} }; + + const TriangleMesh fan_mesh(fan); + TriangleSelector selector(fan_mesh); + selector.set_facet(0, EnforcerBlockerType::ENFORCER); + selector.set_facet(1, EnforcerBlockerType::ENFORCER); + selector.set_facet(2, EnforcerBlockerType::ENFORCER); + // facet 3 (T3) is left at its default EnforcerBlockerType::NONE. + + TextureDisplacementFacetsData facets{}; + facets[0] = selector.serialize(); + + TextureDisplacementLayer layer; + layer.slot = 0; + layer.depth_mm = 1.0f; + layer.tiling_scale = 5.0f; + layer.image_data = make_flat_gray_png(255); + + // The bake is topology-preserving, so it only ever moves fan's own vertices, in order. + auto moved = [&](const indexed_triangle_set &result, size_t i) { + return (result.vertices[i] - fan.vertices[i]).norm() > 1e-6f; + }; + + SECTION("by default the whole painted patch moves, border included") + { + const indexed_triangle_set result = build_texture_displacement(fan, {layer}, facets, classic_options()); + REQUIRE(result.vertices.size() == fan.vertices.size()); + for (size_t i = 0; i < fan.vertices.size(); ++i) + CHECK(moved(result, i)); + // ... straight along the painted surface's own normal (+Z here), by the full depth. Nothing + // has torn: the unpainted triangle T3 simply shares the moved vertices. + for (size_t i = 0; i < fan.vertices.size(); ++i) + CHECK_THAT(result.vertices[i].z() - fan.vertices[i].z(), WithinAbs(1.0f, 1e-3f)); + CHECK(result.indices == fan.indices); + } + + SECTION("pinning the border holds exactly the vertices an unpainted triangle also uses") + { + TextureDisplacementOptions options = classic_options(); + options.displace_border = false; + const indexed_triangle_set result = build_texture_displacement(fan, {layer}, facets, options); + CHECK_FALSE(moved(result, 0)); // O: border + CHECK_FALSE(moved(result, 1)); // A: border + CHECK_FALSE(moved(result, 4)); // D: border + CHECK(moved(result, 2)); // B: interior + CHECK(moved(result, 3)); // C: interior + } + +} + +TEST_CASE("TextureDisplacement: post-process smoothing relaxes only what moved", "[TextureDisplacement]") +{ + // A checkerboard height map on a fine grid gives a relief full of hard steps - exactly what the + // smoothing pass is for. Only the central square is painted, so the patch has a real border. + indexed_triangle_set plane; + plane.vertices = { { 0.f, 0.f, 0.f }, { 20.f, 0.f, 0.f }, { 20.f, 20.f, 0.f }, { 0.f, 20.f, 0.f } }; + plane.indices = { { 0, 1, 2 }, { 0, 2, 3 } }; + const indexed_triangle_set grid = subdivide_mesh_uniform(plane, 1.f, 6); + REQUIRE(grid.indices.size() > 256); + + std::vector painted(grid.indices.size(), 0); + const TriangleMesh grid_mesh(grid); + TriangleSelector selector(grid_mesh); + for (int i = 0; i < int(grid.indices.size()); ++i) { + const auto &t = grid.indices[i]; + float cx = 0.f, cy = 0.f; + for (int k = 0; k < 3; ++k) { + cx += grid.vertices[t[k]].x() / 3.f; + cy += grid.vertices[t[k]].y() / 3.f; + } + if (cx > 5.f && cx < 15.f && cy > 5.f && cy < 15.f) { + selector.set_facet(i, EnforcerBlockerType::ENFORCER); + painted[size_t(i)] = 1; + } + } + REQUIRE(std::count(painted.begin(), painted.end(), uint8_t(1)) > 32); + TextureDisplacementFacetsData facets{}; + facets[0] = selector.serialize(); + + TextureDisplacementLayer layer; + layer.slot = 0; + layer.depth_mm = 2.0f; + layer.tiling_scale = 6.0f; + + // The patch rim: vertices shared by a painted and an unpainted triangle - exactly the set + // TextureDisplacementOptions::smooth_skip_border holds out of the relaxation. + std::vector rim(grid.vertices.size(), 0), inside(grid.vertices.size(), 0); + for (size_t i = 0; i < grid.indices.size(); ++i) + for (int k = 0; k < 3; ++k) + (painted[i] ? inside : rim)[size_t(grid.indices[i][k])] = 1; + size_t rim_count = 0; + for (size_t v = 0; v < rim.size(); ++v) { + rim[v] = (rim[v] && inside[v]) ? 1 : 0; + rim_count += rim[v]; + } + REQUIRE(rim_count > 8); + + TextureDisplacementOptions options = classic_options(); + options.smooth_enabled = true; + options.smooth_strength = 0.5f; + options.smooth_iterations = 4; + + SECTION("it rounds off the steps without touching the topology") + { + layer.image_data = make_checkerboard_png(); + + // Dirichlet energy over the mesh's edges. Laplacian relaxation is gradient descent on exactly + // this, so it is the quantity guaranteed to fall - unlike the min/max spread of z, which is + // pinned by whichever vertices are held (the unpainted surface at zero, and by default the + // patch rim as well) and so need not move at all. + auto roughness = [](const indexed_triangle_set &its) { + double e = 0.0; + for (const auto &t : its.indices) + for (int k = 0; k < 3; ++k) { + const double d = double(its.vertices[t[k]].z()) - double(its.vertices[t[(k + 1) % 3]].z()); + e += d * d; + } + return e; + }; + + const indexed_triangle_set raw = build_texture_displacement(grid, { layer }, facets, classic_options()); + REQUIRE(roughness(raw) > 0.0); // the checkerboard really did produce relief to smooth + + const indexed_triangle_set smoothed = build_texture_displacement(grid, { layer }, facets, options); + CHECK(roughness(smoothed) < roughness(raw)); + CHECK(smoothed.indices == raw.indices); // ... without touching the topology + CHECK(smoothed.vertices.size() == raw.vertices.size()); + for (size_t v = 0; v < rim.size(); ++v) // ... and the held rim is bit-identical + if (rim[v]) + CHECK_THAT(smoothed.vertices[v].z(), WithinAbs(raw.vertices[v].z(), 1e-6f)); + } + + SECTION("\"ignore outer ring\" decides whether the patch rim relaxes") + { + // A flat white texture makes this exact rather than statistical: every painted vertex is + // displaced to precisely depth_mm, so a movable interior vertex sees nothing but neighbours at + // its own height and cannot move, while every rim vertex has at least one neighbour outside the + // paint pinned at zero and so must come down the moment it is allowed to. + layer.image_data = make_flat_gray_png(255); + const indexed_triangle_set raw = build_texture_displacement(grid, { layer }, facets, classic_options()); + + options.smooth_skip_border = true; + const indexed_triangle_set kept = build_texture_displacement(grid, { layer }, facets, options); + + options.smooth_skip_border = false; + const indexed_triangle_set relaxed = build_texture_displacement(grid, { layer }, facets, options); + + // Asserted on z alone, not on the whole position: relaxation averages all three coordinates, + // and while the tangential drift cancels by symmetry on a regular grid it does so only up to + // floating-point summation order, which is not something to pin down across three platforms. + // Height is what the option is about and it is exact - every neighbour of a movable vertex sits + // at the same height, so its own height cannot move. + for (size_t v = 0; v < rim.size(); ++v) + if (rim[v]) { + CHECK_THAT(kept.vertices[v].z(), WithinAbs(raw.vertices[v].z(), 1e-6f)); // held + CHECK(relaxed.vertices[v].z() < raw.vertices[v].z()); // melted down + } + } +} + +TEST_CASE("TextureDisplacement: smooth_mesh_vertices holds everything outside its mask", "[TextureDisplacement]") +{ + // A single spike on a flat sheet: relaxing it must pull the spike down and leave every vertex + // that is not flagged movable at exactly the coordinates it started at. + indexed_triangle_set plane; + plane.vertices = { { 0.f, 0.f, 0.f }, { 8.f, 0.f, 0.f }, { 8.f, 8.f, 0.f }, { 0.f, 8.f, 0.f } }; + plane.indices = { { 0, 1, 2 }, { 0, 2, 3 } }; + indexed_triangle_set grid = subdivide_mesh_uniform(plane, 1.f, 4); + + // Raise one interior vertex, and let only it and its immediate neighbours move. + size_t spike = 0; + float best = std::numeric_limits::max(); + for (size_t i = 0; i < grid.vertices.size(); ++i) + if (const float d = (grid.vertices[i] - Vec3f(4.f, 4.f, 0.f)).norm(); d < best) { + best = d; + spike = i; + } + grid.vertices[spike].z() = 5.f; + + std::vector movable(grid.vertices.size(), 0); + movable[spike] = 1; + for (const auto &t : grid.indices) + for (int e = 0; e < 3; ++e) + if (size_t(t[e]) == spike) + for (int k = 0; k < 3; ++k) + movable[size_t(t[k])] = 1; + + const indexed_triangle_set before = grid; + smooth_mesh_vertices(grid, movable, 0.5f, 3); + + CHECK(grid.vertices[spike].z() < before.vertices[spike].z()); // the spike came down + CHECK(grid.vertices[spike].z() > 0.f); // but was not flattened outright + CHECK(grid.indices == before.indices); // topology untouched + for (size_t i = 0; i < grid.vertices.size(); ++i) + if (!movable[i]) + CHECK_THAT((grid.vertices[i] - before.vertices[i]).norm(), WithinAbs(0.f, 1e-9f)); + + // Guard rails: each of these must leave the mesh byte-identical. + for (const auto &noop : { std::make_pair(0.f, 3), std::make_pair(0.5f, 0) }) { + indexed_triangle_set copy = before; + smooth_mesh_vertices(copy, movable, noop.first, noop.second); + CHECK(copy.vertices == before.vertices); + } + indexed_triangle_set copy = before; + smooth_mesh_vertices(copy, std::vector(3, 1), 0.5f, 3); // mis-sized mask + CHECK(copy.vertices == before.vertices); +} + +// Every undirected edge of a closed manifold mesh is shared by exactly two triangles. A T-junction +// (a hanging node where a refined region meets a coarse one) breaks that: the coarse side spans an +// edge that the fine side has replaced with two half-edges, so those three edges each show up an +// odd number of times. Counting edge uses is therefore an exact crack detector for a closed mesh. +static bool every_edge_used_twice(const indexed_triangle_set &its) +{ + std::map, int> uses; + for (const auto &t : its.indices) + for (int e = 0; e < 3; ++e) { + int a = t[e], b = t[(e + 1) % 3]; + if (a > b) + std::swap(a, b); + ++uses[{ a, b }]; + } + for (const auto &[edge, n] : uses) + if (n != 2) + return false; + return true; +} + +TEST_CASE("TextureDisplacement: adaptive subdivision is conformal and region-restricted", "[TextureDisplacement]") +{ + const indexed_triangle_set cube = its_make_cube(10., 10., 10.); + REQUIRE(every_edge_used_twice(cube)); // sanity: the input really is a closed manifold + + auto longest_edge = [](const indexed_triangle_set &its, const stl_triangle_vertex_indices &t) { + float m = 0.f; + for (int e = 0; e < 3; ++e) + m = std::max(m, (its.vertices[t[e]] - its.vertices[t[(e + 1) % 3]]).norm()); + return m; + }; + + SECTION("whole-mesh region refines everywhere and stays conformal") + { + std::vector region(cube.indices.size(), 1); + std::vector source; + const indexed_triangle_set out = subdivide_mesh_adaptive(cube, region, 3.f, 100000, &source); + + CHECK(out.indices.size() > cube.indices.size()); // it actually refined + CHECK(every_edge_used_twice(out)); // ... without opening a single crack + + // Refinement runs to completion, not for a fixed number of passes: with the whole mesh in the + // region and budget to spare, *every* edge really does end up at or below the target. This is + // the regression that matters - an earlier version quietly stopped a long way short, having + // spent its pass budget grading the coarse surroundings. + float worst = 0.f; + for (const auto &t : out.indices) + worst = std::max(worst, longest_edge(out, t)); + CHECK(worst <= 3.f); + + REQUIRE(source.size() == out.indices.size()); + for (int s : source) + CHECK((s >= 0 && s < int(cube.indices.size()))); // every child names a real parent + } + + SECTION("a partial region refines only there, and the boundary is still crack-free") + { + // Refine only the triangles whose centroid is in the upper (z > 5) half of the cube. + std::vector region(cube.indices.size(), 0); + size_t region_count = 0; + for (size_t i = 0; i < cube.indices.size(); ++i) { + const auto &t = cube.indices[i]; + const float cz = (cube.vertices[t[0]].z() + cube.vertices[t[1]].z() + cube.vertices[t[2]].z()) / 3.f; + if (cz > 5.f) { + region[i] = 1; + ++region_count; + } + } + REQUIRE(region_count > 0); + + std::vector source; + const indexed_triangle_set out = subdivide_mesh_adaptive(cube, region, 2.f, 100000, &source); + + CHECK(out.indices.size() > cube.indices.size()); + CHECK(every_edge_used_twice(out)); // the refined/coarse seam has no T-junction + + // Inside the region the target is actually met - refinement is not cut short by a pass budget. + // Outside it, only the graded transition band conformality requires is touched, so plenty of + // the unpainted mesh is still coarser than the target: the region was not a suggestion. + float max_in = 0.f, max_out = 0.f; + for (size_t i = 0; i < out.indices.size(); ++i) { + float &acc = region[source[i]] ? max_in : max_out; + acc = std::max(acc, longest_edge(out, out.indices[i])); + } + CHECK(max_in <= 2.f); + CHECK(max_out > 2.f); + + std::vector all(cube.indices.size(), 1); + const indexed_triangle_set whole = subdivide_mesh_adaptive(cube, all, 2.f, 100000); + CHECK(out.indices.size() < whole.indices.size()); // ... and it cost less than doing the lot + } + + SECTION("the triangle budget caps the result and still leaves a conformal mesh") + { + std::vector region(cube.indices.size(), 1); + const indexed_triangle_set out = subdivide_mesh_adaptive(cube, region, 0.05f, /*max_triangles*/ 500); + CHECK(out.indices.size() <= 500); + CHECK(out.indices.size() > cube.indices.size()); // it spent the budget rather than giving up + CHECK(every_edge_used_twice(out)); // stopping on the budget is not a crack + } + + SECTION("an empty region is a no-op") + { + std::vector region(cube.indices.size(), 0); + const indexed_triangle_set out = subdivide_mesh_adaptive(cube, region, 1.f, 100000, nullptr); + CHECK(out.indices.size() == cube.indices.size()); + CHECK(out.vertices.size() == cube.vertices.size()); + } +} + +TEST_CASE("TextureDisplacement: feature-adaptive subdivision follows curvature, not slope", "[TextureDisplacement]") +{ + // A flat sheet, tessellated into a regular grid to give the bisector something to work with. + indexed_triangle_set plane; + plane.vertices = { { 0.f, 0.f, 0.f }, { 1.f, 0.f, 0.f }, { 1.f, 1.f, 0.f }, { 0.f, 1.f, 0.f } }; + plane.indices = { { 0, 1, 2 }, { 0, 2, 3 } }; + const indexed_triangle_set grid = subdivide_mesh_uniform(plane, 0.15f, 5); // ~uniform grid of small triangles + REQUIRE(grid.indices.size() > 32); + + const std::vector region(grid.indices.size(), 1); + + auto longest_edge = [](const indexed_triangle_set &its, const stl_triangle_vertex_indices &t) { + float m = 0.f; + for (int e = 0; e < 3; ++e) + m = std::max(m, (its.vertices[t[e]] - its.vertices[t[(e + 1) % 3]]).norm()); + return m; + }; + auto centroid_xy = [](const indexed_triangle_set &its, const stl_triangle_vertex_indices &t) { + return Vec2f((its.vertices[t[0]].x() + its.vertices[t[1]].x() + its.vertices[t[2]].x()) / 3.f, + (its.vertices[t[0]].y() + its.vertices[t[1]].y() + its.vertices[t[2]].y()) / 3.f); + }; + + SECTION("a sharp hill refines densely at its center and leaves flat corners coarse") + { + // A tight Gaussian hill at the sheet's center: strong curvature near (0.5, 0.5), flat far away. + HeightFieldSampler hill = [](const Vec3f &p, const Vec3f &) { + const float r2 = (p.x() - 0.5f) * (p.x() - 0.5f) + (p.y() - 0.5f) * (p.y() - 0.5f); + return 1.0f * std::exp(-r2 / 0.02f); + }; + + // Baseline max edge 0.3 is coarser than the grid's own edges, so the baseline adds nothing + // here - this isolates the *curvature* contribution (the grid already meets the baseline). + std::vector source; + const indexed_triangle_set out = + subdivide_mesh_adaptive(grid, region, /*max edge*/ 0.3f, 200000, &source, hill, /*tol*/ 0.02f, + /*min_edge*/ 0.01f); + + CHECK(out.indices.size() > grid.indices.size()); // the hill forced real refinement + + // The largest triangle near the hill's center must be much smaller than the largest in a flat + // corner - i.e. triangles went where the curvature is, not spread evenly. + float near_max = 0.f, far_max = 0.f; + for (const auto &t : out.indices) { + const Vec2f c = centroid_xy(out, t); + const float r = (c - Vec2f(0.5f, 0.5f)).norm(); + const float len = longest_edge(out, t); + if (r < 0.1f) + near_max = std::max(near_max, len); + else if (r > 0.45f) + far_max = std::max(far_max, len); + } + REQUIRE(near_max > 0.f); + REQUIRE(far_max > 0.f); + CHECK(near_max < far_max); // finer at the hill than on the flats + } + + SECTION("a linear ramp has zero curvature and is left untouched") + { + // Height varies, but linearly - a flat triangle represents it exactly, so the chord error is + // zero everywhere and nothing should be split. This is the case a gradient-based criterion + // would wrongly over-refine. + HeightFieldSampler ramp = [](const Vec3f &p, const Vec3f &) { return 2.0f * p.x(); }; + + // Same coarse baseline (0.3) that the grid already meets, so any split would be curvature- + // driven - and a ramp has none. + const indexed_triangle_set out = + subdivide_mesh_adaptive(grid, region, /*max edge*/ 0.3f, 200000, nullptr, ramp, /*tol*/ 0.02f, + /*min_edge*/ 0.01f); + + CHECK(out.indices.size() == grid.indices.size()); // not one extra triangle + } + + SECTION("the max-edge baseline still applies in feature mode") + { + // A height field that is flat everywhere the four sample points of a coarse triangle happen to + // land, but not in between - the aliasing case where a chord test alone reports no error and + // refinement stalls before it ever starts. The baseline is what stops that: it guarantees a + // sampling density fine enough for the curvature test to see the texture at all. + HeightFieldSampler flat = [](const Vec3f &, const Vec3f &) { return 0.f; }; + + const indexed_triangle_set out = + subdivide_mesh_adaptive(grid, region, /*max edge*/ 0.03f, 200000, nullptr, flat, /*tol*/ 0.02f, + /*min_edge*/ 0.001f); + + CHECK(out.indices.size() > grid.indices.size()); + float worst = 0.f; + for (const auto &t : out.indices) + worst = std::max(worst, longest_edge(out, t)); + CHECK(worst <= 0.03f); + } +} + +// A 2x2 truecolour PNG: red, green / blue, white. Written out as bytes rather than encoded here +// because libslic3r only *writes* grayscale PNGs (png::write_gray_to_file) - which is also exactly +// why the colour path exists: the GUI importer stores colour images through wxImage instead. +static std::shared_ptr> make_rgb_png_2x2() +{ + static const unsigned char bytes[] = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, + 0x08, 0x02, 0x00, 0x00, 0x00, 0xfd, 0xd4, 0x9a, 0x73, 0x00, 0x00, 0x00, + 0x14, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0xf8, 0xcf, 0xc0, 0xc0, + 0x00, 0xc2, 0x0c, 0xff, 0xff, 0xff, 0xff, 0x0f, 0x00, 0x1f, 0xee, 0x05, + 0xfb, 0x60, 0x6c, 0x70, 0xf2, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, + 0x44, 0xae, 0x42, 0x60, 0x82, + }; + return std::make_shared>(std::begin(bytes), std::end(bytes)); +} + +// The same image with an opaque alpha channel, so four bytes per pixel instead of three. +static std::shared_ptr> make_rgba_png_2x2() +{ + static const unsigned char bytes[] = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, + 0x08, 0x06, 0x00, 0x00, 0x00, 0x72, 0xb6, 0x0d, 0x24, 0x00, 0x00, 0x00, + 0x12, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0xf8, 0xcf, 0xc0, 0xf0, + 0x1f, 0x0c, 0x81, 0x34, 0x18, 0x00, 0x00, 0x49, 0xc8, 0x09, 0xf7, 0x03, + 0xd9, 0x64, 0xf1, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, + 0x42, 0x60, 0x82, + }; + return std::make_shared>(std::begin(bytes), std::end(bytes)); +} + +TEST_CASE("TextureDisplacement: a colour texture decodes to both colour and height", "[TextureDisplacement]") +{ + TextureDisplacementLayer layer; + layer.slot = 0; + // Row stride differs between the two, and both must come out the same way up. + layer.image_data = GENERATE(make_rgb_png_2x2(), make_rgba_png_2x2()); + + const DecodedHeightTexture tex = decode_height_texture(layer); + REQUIRE_FALSE(tex.empty()); + REQUIRE(tex.has_color()); + REQUIRE(tex.width == 2); + REQUIRE(tex.height == 2); + REQUIRE(tex.rgb.size() == 2 * 2 * 3); + + // Row-major, top-to-bottom: red, green / blue, white. + CHECK(tex.rgb[0] == 255); CHECK(tex.rgb[1] == 0); CHECK(tex.rgb[2] == 0); + CHECK(tex.rgb[3] == 0); CHECK(tex.rgb[4] == 255); CHECK(tex.rgb[5] == 0); + CHECK(tex.rgb[6] == 0); CHECK(tex.rgb[7] == 0); CHECK(tex.rgb[8] == 255); + CHECK(tex.rgb[9] == 255); CHECK(tex.rgb[10] == 255); CHECK(tex.rgb[11] == 255); + + // Height is the luminance, with wxImage::ConvertToGreyscale()'s coefficients - which is what + // makes a texture displace identically whether it was imported before or after colour was kept. + CHECK(int(tex.pixels[0]) == int(std::lround(0.299 * 255))); // red + CHECK(int(tex.pixels[1]) == int(std::lround(0.587 * 255))); // green + CHECK(int(tex.pixels[2]) == int(std::lround(0.114 * 255))); // blue + CHECK(int(tex.pixels[3]) == 255); // white +} + +TEST_CASE("A 16-bit colour texture decodes to nothing rather than noise", "[TextureDisplacement]") +{ + // The 2x2 image above at 16 bits per channel. The texture library converts such a file to 8-bit + // on load, so it can only arrive here stored as-is, from a project file. + static const unsigned char bytes[] = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, + 0x10, 0x02, 0x00, 0x00, 0x00, 0xad, 0x44, 0x46, 0x30, 0x00, 0x00, 0x00, + 0x12, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0xf8, 0xff, 0x9f, 0x01, + 0x0c, 0x60, 0x34, 0x90, 0x01, 0x01, 0x00, 0x75, 0xa4, 0x0b, 0xf5, 0x97, + 0xf4, 0x36, 0xa1, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, + 0x42, 0x60, 0x82, + }; + TextureDisplacementLayer layer; + layer.slot = 0; + layer.image_data = std::make_shared>(std::begin(bytes), std::end(bytes)); + + CHECK(decode_height_texture(layer).empty()); +} + +TEST_CASE("A truncated texture decodes to nothing instead of aborting", "[TextureDisplacement]") +{ + // A half-copied file in the texture folder, or a damaged project file. libpng reports this by + // longjmp, and aborts the process if the decoder has not set a jump buffer to land on. + const auto whole = GENERATE(make_flat_gray_png(128, 16, 16), make_rgb_png_2x2()); + TextureDisplacementLayer layer; + layer.slot = 0; + layer.image_data = std::make_shared>(whole->begin(), whole->begin() + whole->size() / 2); + + CHECK(decode_height_texture(layer).empty()); +} + +TEST_CASE("TextureDisplacement: a grayscale texture reports no colour", "[TextureDisplacement]") +{ + // The shipped library is all grayscale, and has_color() is what the whole colour feature keys + // off - a height map must never look like it has colours to apply. + TextureDisplacementLayer layer; + layer.slot = 0; + layer.image_data = make_flat_gray_png(128); + + const DecodedHeightTexture tex = decode_height_texture(layer); + REQUIRE_FALSE(tex.empty()); + CHECK_FALSE(tex.has_color()); + CHECK(tex.rgb.empty()); + + Vec3f out(9.f, 9.f, 9.f); + CHECK_FALSE(sample_layer_color(tex, layer, Vec3f::Zero(), Vec3f::UnitZ(), out)); + CHECK(out.x() == 9.f); // left untouched on a false return +} + +// Two triangles making a 10x10 quad in the z=0 plane. +static indexed_triangle_set color_test_quad() +{ + indexed_triangle_set quad; + quad.vertices = { Vec3f(0, 0, 0), Vec3f(10, 0, 0), Vec3f(10, 10, 0), Vec3f(0, 10, 0) }; + quad.indices = { { 0, 1, 2 }, { 0, 2, 3 } }; + return quad; +} + +TEST_CASE("TextureDisplacement: colour is reported per triangle and only where painted", "[TextureDisplacement]") +{ + const indexed_triangle_set quad = color_test_quad(); + + TextureDisplacementLayer layer; + layer.slot = 0; + layer.image_data = make_rgb_png_2x2(); + layer.color_enabled = true; + layer.depth_mm = 0.f; // colour only, so this isolates the colour path from the geometry + layer.tiling_scale = 100.f; + layer.projection_method = TextureProjectionMethod::Triplanar; + + TriangleMesh mesh(quad); + TriangleSelector selector(mesh); + selector.set_facet(0, EnforcerBlockerType::ENFORCER); // only the first triangle + + TextureDisplacementFacetsData facets; + facets[0] = selector.serialize(); + + // A three-entry palette matched in plain RGB: all this test needs is *an* index. The perceptual + // matching is the GUI's (make_palette_quantizer), and is deliberately not under test here. + const std::array palette = { Vec3f(1, 0, 0), Vec3f(0, 1, 0), Vec3f(0, 0, 1) }; + TextureColorRequest request; + std::vector triangle_color; + request.out_triangle = &triangle_color; + request.quantize = [&palette](const Vec3f &rgb) { + int best = 0; + float bd = std::numeric_limits::max(); + for (int i = 0; i < 3; ++i) + if (const float d = (palette[size_t(i)] - rgb).squaredNorm(); d < bd) { + bd = d; + best = i; + } + return best; + }; + + const indexed_triangle_set out = build_texture_displacement(quad, { layer }, facets, classic_options(), {}, &request); + + REQUIRE_FALSE(out.indices.empty()); + REQUIRE(triangle_color.size() == quad.indices.size()); + // The painted triangle takes a filament; the unpainted one is left at 0, which is + // EnforcerBlockerType::NONE - "use the volume's own filament". That is what confines the effect + // to the painted area without having to invent a colour for everything outside it. + CHECK(triangle_color[0] != 0); + CHECK(triangle_color[1] == 0); +} + +TEST_CASE("TextureDisplacement: a layer that is not colouring reports no colours", "[TextureDisplacement]") +{ + const indexed_triangle_set quad = color_test_quad(); + + TextureDisplacementLayer layer; + layer.slot = 0; + layer.image_data = make_rgb_png_2x2(); + layer.color_enabled = false; // the checkbox is off: colour stays off even on a colour texture + layer.depth_mm = 1.f; + + TriangleMesh mesh(quad); + TriangleSelector selector(mesh); + selector.set_facet(0, EnforcerBlockerType::ENFORCER); + selector.set_facet(1, EnforcerBlockerType::ENFORCER); + + TextureDisplacementFacetsData facets; + facets[0] = selector.serialize(); + + TextureColorRequest request; + std::vector triangle_color; + request.out_triangle = &triangle_color; + request.quantize = [](const Vec3f &) { return 0; }; + + build_texture_displacement(quad, { layer }, facets, classic_options(), {}, &request); + + REQUIRE(triangle_color.size() == quad.indices.size()); + CHECK(triangle_color[0] == 0); + CHECK(triangle_color[1] == 0); +} + +TEST_CASE("TextureDisplacement: subdivision refines a colour boundary a flat height field hides", + "[TextureDisplacement]") +{ + // A cube with a flat height field, so *nothing* in the height criteria has any reason to refine + // it - which is exactly the case the colour criterion exists for. Closed, so every_edge_used_twice() + // is an exact crack detector: the colour criterion goes through the same conformal bisection as + // everything else and must not be able to open one. + const indexed_triangle_set cube = its_make_cube(10., 10., 10.); + const std::vector region(cube.indices.size(), REFINE_PAINTED); + + auto longest_edge = [](const indexed_triangle_set &its, const stl_triangle_vertex_indices &t) { + float m = 0.f; + for (int e = 0; e < 3; ++e) + m = std::max(m, (its.vertices[t[e]] - its.vertices[t[(e + 1) % 3]]).norm()); + return m; + }; + + // One filament on each side of x = 5: a step, with no gradient anywhere for a chord test to see. + ColorFieldSampler split_at_five = [](const Vec3f &p, const Vec3f &) { return p.x() < 5.f ? 0 : 1; }; + ColorFieldSampler all_one = [](const Vec3f &, const Vec3f &) { return 0; }; + + SECTION("a colour boundary gets triangles") + { + const indexed_triangle_set out = + subdivide_mesh_adaptive(cube, region, /*max edge*/ 0.f, 200000, nullptr, nullptr, 0.f, + /*min_edge*/ 0.05f, /*border*/ 0.f, nullptr, split_at_five, + /*colour edge*/ 0.5f); + + CHECK(out.indices.size() > cube.indices.size()); + CHECK(every_edge_used_twice(out)); // still watertight + + // Every triangle still straddling the boundary must be down at the target. + for (const auto &t : out.indices) { + bool straddles = false; + for (int i = 1; i < 3; ++i) + if ((out.vertices[t[i]].x() < 5.f) != (out.vertices[t[0]].x() < 5.f)) + straddles = true; + if (straddles) + CHECK(longest_edge(out, t) <= 0.5f + 1e-4f); + } + } + + SECTION("a uniform colour adds nothing") + { + const indexed_triangle_set out = + subdivide_mesh_adaptive(cube, region, /*max edge*/ 0.f, 200000, nullptr, nullptr, 0.f, + /*min_edge*/ 0.05f, /*border*/ 0.f, nullptr, all_one, + /*colour edge*/ 0.5f); + + CHECK(out.indices.size() == cube.indices.size()); + } + + SECTION("no colour sampler leaves the mesh alone") + { + // The regression this guards: the colour criterion must be inert when nothing is colouring, + // or every bake would start refining geometry for no reason. + const indexed_triangle_set out = + subdivide_mesh_adaptive(cube, region, /*max edge*/ 0.f, 200000, nullptr, nullptr, 0.f, + /*min_edge*/ 0.05f, /*border*/ 0.f, nullptr, nullptr, + /*colour edge*/ 0.5f); + + CHECK(out.indices.size() == cube.indices.size()); + } +} + +// --------------------------------------------------------------------------------------------- +// Step cutter +// --------------------------------------------------------------------------------------------- + +// A flat square sheet in the (x, y) plane split into right triangles of the given edge, with the +// diagonal alternated so the mesh has no preferred direction. Normals +z. +static indexed_triangle_set make_flat_sheet(float size, float edge) +{ + indexed_triangle_set its; + const int n = std::max(1, int(std::lround(size / edge))); + for (int j = 0; j <= n; ++j) + for (int i = 0; i <= n; ++i) + its.vertices.emplace_back(size * float(i) / float(n), size * float(j) / float(n), 0.f); + const auto id = [n](int i, int j) { return j * (n + 1) + i; }; + for (int j = 0; j < n; ++j) + for (int i = 0; i < n; ++i) { + if ((i + j) % 2 == 0) { + its.indices.emplace_back(id(i, j), id(i + 1, j), id(i + 1, j + 1)); + its.indices.emplace_back(id(i, j), id(i + 1, j + 1), id(i, j + 1)); + } else { + its.indices.emplace_back(id(i, j), id(i + 1, j), id(i, j + 1)); + its.indices.emplace_back(id(i + 1, j), id(i + 1, j + 1), id(i, j + 1)); + } + } + return its; +} + +// Every interior edge of a sheet is shared by exactly two triangles; only the sheet's own border may +// be used once. +static bool sheet_is_manifold(const indexed_triangle_set &its, float size) +{ + std::map, int> uses; + for (const auto &t : its.indices) + for (int e = 0; e < 3; ++e) { + int a = t[e], b = t[(e + 1) % 3]; + if (a > b) + std::swap(a, b); + ++uses[{ a, b }]; + } + for (const auto &[edge, n] : uses) { + const Vec3f &pa = its.vertices[size_t(edge.first)], &pb = its.vertices[size_t(edge.second)]; + const bool border = (pa.x() == pb.x() && (pa.x() == 0.f || pa.x() == size)) || + (pa.y() == pb.y() && (pa.y() == 0.f || pa.y() == size)); + if (n > 2 || (n == 1 && !border)) + return false; + } + return true; +} + +TEST_CASE("TextureDisplacement: the step cutter turns a stepped field into walls", "[TextureDisplacement]") +{ + // Square posts 1.2 mm wide on a 2 mm pitch, as a binary field: a step everywhere along the post + // edges, flat everywhere else. 1 mm triangles, so every post edge crosses several of them. + constexpr float RELIEF = 0.4f, SIZE = 6.f, STEP_W = 0.05f, GAP = 0.075f; + const auto posts = [](float x, float y) { + const float fx = std::fmod(std::fmod(x, 2.f) + 2.f, 2.f), fy = std::fmod(std::fmod(y, 2.f) + 2.f, 2.f); + return (fx > 0.4f && fx < 1.6f && fy > 0.4f && fy < 1.6f) ? RELIEF : 0.f; + }; + const HeightFieldSampler sampler = [&](const Vec3f &p, const Vec3f &) { return posts(p.x(), p.y()); }; + const indexed_triangle_set sheet = make_flat_sheet(SIZE, 1.f); + const std::vector region(sheet.indices.size(), 1); + + std::vector source; + size_t cuts = 0; + const indexed_triangle_set cut = cut_mesh_at_steps(sheet, region, sampler, STEP_W, GAP, 0.f, &source, &cuts); + + CHECK(cuts > 0); + CHECK(cut.indices.size() > sheet.indices.size()); + REQUIRE(source.size() == cut.indices.size()); + CHECK(sheet_is_manifold(cut, SIZE)); + + // Displace along the cut mesh's own area-weighted vertex normals, as the bake does. + std::vector normal(cut.vertices.size(), Vec3f::Zero()); + for (const auto &t : cut.indices) { + const Vec3f &a = cut.vertices[size_t(t[0])], &b = cut.vertices[size_t(t[1])], &c = cut.vertices[size_t(t[2])]; + const Vec3f n = (b - a).cross(c - a); + CHECK(n.z() > 0.f); // nothing inverted, nothing degenerate + for (int k = 0; k < 3; ++k) + normal[size_t(t[k])] += n; + } + indexed_triangle_set displaced = cut; + for (size_t v = 0; v < displaced.vertices.size(); ++v) { + const Vec3f n = normal[v].normalized(); + displaced.vertices[v] += n * sampler(cut.vertices[v], n); + } + + // The defining property of the cut: a triangle that spans both heights is a wall, and a wall stands + // within the seam gap of a step. Anywhere else a mixed triangle would be a ramp - exactly what + // refinement leaves and the cutter is there to remove. + const auto near_step = [&](const Vec3f &p) { + const float h = posts(p.x(), p.y()); + for (int k = 0; k < 8; ++k) { + const float ang = float(k) * float(M_PI) / 4.f; + if (posts(p.x() + GAP * std::cos(ang), p.y() + GAP * std::sin(ang)) != h) + return true; + } + return false; + }; + size_t walls = 0, ramps = 0; + for (const auto &t : displaced.indices) { + const float z0 = displaced.vertices[size_t(t[0])].z(), z1 = displaced.vertices[size_t(t[1])].z(), + z2 = displaced.vertices[size_t(t[2])].z(); + if (std::abs(z0 - z1) < 1e-4f && std::abs(z1 - z2) < 1e-4f) + continue; // flat: on one level + bool wall = true; + for (int k = 0; k < 3; ++k) + wall = wall && near_step(cut.vertices[size_t(t[k])]); + (wall ? walls : ramps)++; + } + CHECK(walls > 0); + CHECK(ramps == 0); +} + +TEST_CASE("TextureDisplacement: the step cutter passes a smooth field through untouched", "[TextureDisplacement]") +{ + // A wide hill: its mid-level contour runs through the sheet, but nowhere is it a step, so there is + // nothing to cut - refinement is the right tool for it. + const HeightFieldSampler hill = [](const Vec3f &p, const Vec3f &) { + const float r2 = (p.x() - 3.f) * (p.x() - 3.f) + (p.y() - 3.f) * (p.y() - 3.f); + return 0.4f * std::exp(-r2 / 3.f); + }; + const indexed_triangle_set sheet = make_flat_sheet(6.f, 1.f); + const std::vector region(sheet.indices.size(), 1); + + std::vector source; + size_t cuts = 0; + const indexed_triangle_set out = cut_mesh_at_steps(sheet, region, hill, 0.05f, 0.075f, 0.f, &source, &cuts); + + CHECK(cuts == 0); + CHECK(out.indices.size() == sheet.indices.size()); + CHECK(out.vertices.size() == sheet.vertices.size()); + REQUIRE(source.size() == sheet.indices.size()); + for (size_t i = 0; i < source.size(); ++i) + CHECK(source[i] == int(i)); +} + +TEST_CASE("TextureDisplacement: the step cutter leaves features at the step's own scale to refinement", + "[TextureDisplacement]") +{ + // Square posts filling the middle half of each cell, binary and sharp at every crossing - the only + // difference between the two is the pitch (offset so no post edge lies along a mesh edge). Posts a + // step width or so across have no pure interior for a seam copy to land in, and cutting them would + // double the triangles for walls no bigger than the blur. + constexpr float STEP_W = 0.05f; + const float pitch = GENERATE(0.12f, 0.6f); + const auto posts = [pitch](const Vec3f &p, const Vec3f &) { + const float fx = std::fmod(p.x() + 0.17f, pitch) / pitch, fy = std::fmod(p.y() + 0.31f, pitch) / pitch; + return (fx > 0.25f && fx < 0.75f && fy > 0.25f && fy < 0.75f) ? 0.4f : 0.f; + }; + const indexed_triangle_set sheet = make_flat_sheet(6.f, 1.f); + const std::vector region(sheet.indices.size(), 1); + + size_t cuts = 0; + const indexed_triangle_set out = cut_mesh_at_steps(sheet, region, posts, STEP_W, STEP_W, 0.f, nullptr, &cuts); + + if (0.5f * pitch < 2.25f * STEP_W) { + CHECK(cuts == 0); + CHECK(out.indices.size() == sheet.indices.size()); + } else { + CHECK(cuts > 0); + CHECK(sheet_is_manifold(out, 6.f)); + } +} + +TEST_CASE("TextureDisplacement: the step cutter only cuts inside the region", "[TextureDisplacement]") +{ + // A single step at x = 3 across the whole sheet, but only the left half is painted. + const HeightFieldSampler stripe = [](const Vec3f &p, const Vec3f &) { return p.x() > 3.f ? 0.4f : 0.f; }; + const indexed_triangle_set sheet = make_flat_sheet(6.f, 1.f); + + SECTION("an empty region is a no-op") + { + const std::vector none(sheet.indices.size(), 0); + size_t cuts = 0; + const indexed_triangle_set out = cut_mesh_at_steps(sheet, none, stripe, 0.05f, 0.075f, 0.f, nullptr, &cuts); + CHECK(cuts == 0); + CHECK(out.indices.size() == sheet.indices.size()); + } + + SECTION("unpainted triangles away from the paint are untouched") + { + std::vector region(sheet.indices.size(), 0); + for (size_t t = 0; t < sheet.indices.size(); ++t) { + const auto &f = sheet.indices[t]; + const float cy = (sheet.vertices[size_t(f[0])].y() + sheet.vertices[size_t(f[1])].y() + + sheet.vertices[size_t(f[2])].y()) / 3.f; + region[t] = cy < 3.f ? 1 : 0; // paint the lower half + } + std::vector source; + size_t cuts = 0; + const indexed_triangle_set out = cut_mesh_at_steps(sheet, region, stripe, 0.05f, 0.075f, 0.f, &source, &cuts); + CHECK(cuts > 0); + CHECK(sheet_is_manifold(out, 6.f)); + // An unpainted triangle that shares no edge with a painted one comes out exactly as it went in. + std::vector descendants(sheet.indices.size(), 0); + for (int s : source) + ++descendants[size_t(s)]; + for (size_t t = 0; t < sheet.indices.size(); ++t) { + const auto &f = sheet.indices[t]; + float ymin = 6.f; + for (int k = 0; k < 3; ++k) + ymin = std::min(ymin, sheet.vertices[size_t(f[k])].y()); + if (ymin > 3.f) // strictly above the painted half, so no shared edge with it + CHECK(descendants[t] == 1); + } + } +} + +// --------------------------------------------------------------------------------------------- +// Texture smoothing +// --------------------------------------------------------------------------------------------- + +TEST_CASE("TextureDisplacement: texture smoothing is a wrapped box blur whatever the radius", "[TextureDisplacement]") +{ + // A small pseudo-random grey image, encoded through the PNG writer so decode_height_texture() takes + // its normal path. The expected result is the plain definition of the blur - two passes of a + // (2r+1) box, horizontal then vertical, wrapping at the edges - which the sliding-window + // implementation must reproduce byte for byte. + const size_t w = 37, h = 23; + std::vector src(w * h); + uint32_t seed = 12345; + for (uint8_t &p : src) { seed = seed * 1664525u + 1013904223u; p = uint8_t(seed >> 24); } + const boost::filesystem::path tmp_path = boost::filesystem::temp_directory_path() + / boost::filesystem::unique_path("texdisp_test_%%%%%%%%.png"); + REQUIRE(Slic3r::png::write_gray_to_file(tmp_path.string(), w, h, src)); + std::vector bytes; + { + std::ifstream ifs(tmp_path.string(), std::ios::binary); + bytes.assign(std::istreambuf_iterator(ifs), std::istreambuf_iterator()); + } + boost::system::error_code ec; + boost::filesystem::remove(tmp_path, ec); + REQUIRE_FALSE(bytes.empty()); + + TextureDisplacementLayer layer; + layer.image_data = std::make_shared>(std::move(bytes)); + // radius = smoothing * 0.05 * min(w, h) = 0.05 * 23 * smoothing; 1.0 gives 1.15 -> whole radius 2, + // cross-faded 57.5 % toward the blurred image (see smooth_height_pixels()). + layer.smoothing = 1.f; + const DecodedHeightTexture tex = decode_height_texture(layer); + REQUIRE(tex.width == int(w)); + REQUIRE(tex.height == int(h)); + + const int radius = 2; + const float mixf = std::clamp((0.05f * 23.f) / 2.f, 0.f, 1.f); + auto box = [&](std::vector px) { + const int window = 2 * radius + 1; + const float inv = 1.f / float(window); + std::vector t2(px.size()); + for (int pass = 0; pass < 2; ++pass) { + for (int y = 0; y < int(h); ++y) + for (int x = 0; x < int(w); ++x) { + float s = 0.f; + for (int k = -radius; k <= radius; ++k) s += float(px[size_t(y) * w + size_t(((x + k) % int(w) + int(w)) % int(w))]); + t2[size_t(y) * w + size_t(x)] = uint8_t(std::lround(s * inv)); + } + for (int x = 0; x < int(w); ++x) + for (int y = 0; y < int(h); ++y) { + float s = 0.f; + for (int k = -radius; k <= radius; ++k) s += float(t2[size_t(((y + k) % int(h) + int(h)) % int(h)) * w + size_t(x)]); + px[size_t(y) * w + size_t(x)] = uint8_t(std::lround(s * inv)); + } + } + return px; + }; + const std::vector blurred = box(src); + size_t mismatches = 0; + for (size_t i = 0; i < w * h; ++i) { + const uint8_t expected = uint8_t(std::lround(float(src[i]) + (float(blurred[i]) - float(src[i])) * mixf)); + mismatches += tex.pixels[i] != expected; + } + CHECK(mismatches == 0); + + // Asking again with the same smoothing is served from the cache and must be identical. + const DecodedHeightTexture again = decode_height_texture(layer); + CHECK(again.pixels == tex.pixels); +} + +// --------------------------------------------------------------------------------------------- +// Edge flips along the height field (v2 pipeline) +// --------------------------------------------------------------------------------------------- + +TEST_CASE("TextureDisplacement: edge flips lay a stepped field's wall along the grid's diagonals", "[TextureDisplacement]") +{ + // A regular grid crossed by a step at 30 degrees. Before flipping, the step's wall zigzags: many + // triangles have corners on both sides of it. Flipping each quad's diagonal to follow the step must + // cut that count down, without changing the triangle count, the winding, or the manifoldness. + const indexed_triangle_set sheet = make_flat_sheet(6.f, 0.2f); + const float c = std::cos(0.5236f), s = std::sin(0.5236f); + const auto side_of = [&](const Vec3f &p) { return c * p.x() + s * p.y() > 3.5f; }; + const TextureBake::HeightSampleFn field = [&](const Vec3f &p, const Vec3f &, const Vec3f &) { + return side_of(p) ? 0.4f : 0.f; + }; + const auto mixed = [&](const TextureBake::TriSoup &g) { + size_t n = 0; + for (size_t t = 0; t < g.triangle_count(); ++t) { + const bool a = side_of(g.pos[t * 3]), b = side_of(g.pos[t * 3 + 1]), d = side_of(g.pos[t * 3 + 2]); + n += (a != b || b != d); + } + return n; + }; + + const TextureBake::TriSoup before = TextureBake::to_soup(sheet); + const TextureBake::FlipResult after = TextureBake::flip_edges_to_height(before, {}, field, TextureBake::FlipSettings{}, {}); + + CHECK(after.flipped > 0); + REQUIRE(after.geometry.triangle_count() == before.triangle_count()); + const size_t mixed_before = mixed(before), mixed_after = mixed(after.geometry); + CHECK(mixed_after < mixed_before); + // Every triangle still faces up, and none collapsed. + for (size_t t = 0; t < after.geometry.triangle_count(); ++t) { + const Vec3f n = (after.geometry.pos[t * 3 + 1] - after.geometry.pos[t * 3]).cross(after.geometry.pos[t * 3 + 2] - after.geometry.pos[t * 3]); + CHECK(n.z() > 1e-6f); + } + // Manifold: rebuild an indexed mesh from the soup and count edge uses. + indexed_triangle_set rebuilt; + std::map, int> ids; + for (const Vec3f &p : after.geometry.pos) { + const auto key = std::make_tuple(int(std::lround(p.x() * 1e4)), int(std::lround(p.y() * 1e4)), int(std::lround(p.z() * 1e4))); + auto it = ids.find(key); + if (it == ids.end()) { it = ids.emplace(key, int(rebuilt.vertices.size())).first; rebuilt.vertices.push_back(p); } + (void) it; + } + for (size_t t = 0; t < after.geometry.triangle_count(); ++t) { + int idx[3]; + for (int k = 0; k < 3; ++k) { + const Vec3f &p = after.geometry.pos[t * 3 + size_t(k)]; + idx[k] = ids.at(std::make_tuple(int(std::lround(p.x() * 1e4)), int(std::lround(p.y() * 1e4)), int(std::lround(p.z() * 1e4)))); + } + rebuilt.indices.emplace_back(idx[0], idx[1], idx[2]); + } + CHECK(sheet_is_manifold(rebuilt, 6.f)); +} + +// --------------------------------------------------------------------------------------------- +// Automatic resolution (v2 pipeline) +// --------------------------------------------------------------------------------------------- + +TEST_CASE("TextureDisplacement: automatic resolution follows the model's size", "[TextureDisplacement]") +{ + // A 20 mm cube: diagonal 34.64 mm, so diagonal / 250 = 0.1386 mm, rounded up to 0.14. + const indexed_triangle_set cube = its_make_cube(20.f, 20.f, 20.f); + TextureDisplacementLayer layer; + layer.image_data = make_checkerboard_png(16, 16); + layer.tiling_scale = 8.f; + + SECTION("edge from the diagonal, budget the standard 750 k") + { + const V2Resolution rec = recommend_v2_resolution(cube, { layer }); + CHECK_THAT(rec.edge_mm, WithinAbs(0.14f, 1e-4f)); + CHECK(rec.budget_k == 750); + CHECK_THAT(rec.texel_mm, WithinAbs(0.5f, 1e-4f)); // reported for the panel + } + + SECTION("the world transform scales the diagonal") + { + const V2Resolution rec = recommend_v2_resolution(cube, { layer }, Transform3d(Eigen::Scaling(3.0))); + CHECK_THAT(rec.edge_mm, WithinAbs(0.42f, 1e-4f)); // 103.9 / 250 = 0.4157 -> 0.42 + } + + SECTION("a tiny model stops at the 0.05 mm floor") + { + const indexed_triangle_set small = its_make_cube(2.f, 2.f, 2.f); + const V2Resolution rec = recommend_v2_resolution(small, { layer }); + CHECK_THAT(rec.edge_mm, WithinAbs(0.05f, 1e-4f)); + } + + SECTION("the texture's sharpness class is still measured") + { + const TextureDetail sharp = analyze_texture_detail(layer); + CHECK_THAT(sharp.pixels_per_edge, WithinAbs(1.f, 1e-6f)); + TextureDisplacementLayer flat; + flat.image_data = make_flat_gray_png(128, 16, 16); + CHECK_THAT(analyze_texture_detail(flat).pixels_per_edge, WithinAbs(4.f, 1e-6f)); + } +} + +TEST_CASE("TextureDisplacement: the default pipeline colours its rebuilt triangles where painted", "[TextureDisplacement]") +{ + // The same quad and 2x2 colour texture as the classic-path colour test, but baked through the + // one-run pipeline, which rebuilds the topology: every output triangle has to be coloured from the + // texture at its own position, and only those over the painted half of the quad. + const indexed_triangle_set quad = color_test_quad(); + TextureDisplacementLayer layer; + layer.slot = 0; + layer.image_data = make_rgb_png_2x2(); + layer.color_enabled = true; + layer.depth_mm = 0.f; + layer.tiling_scale = 10.f; // one tile over the whole quad, so all four texels show + layer.projection_method = TextureProjectionMethod::Triplanar; + + TriangleMesh mesh(quad); + TriangleSelector selector(mesh); + selector.set_facet(0, EnforcerBlockerType::ENFORCER); // the lower-right half only + TextureDisplacementFacetsData facets; + facets[0] = selector.serialize(); + + const std::array palette = { Vec3f(1, 0, 0), Vec3f(0, 1, 0), Vec3f(0, 0, 1) }; + TextureColorRequest request; + std::vector triangle_color; + request.out_triangle = &triangle_color; + request.quantize = [&palette](const Vec3f &rgb) { + int best = 0; + float bd = std::numeric_limits::max(); + for (int i = 0; i < 3; ++i) + if (const float d = (palette[size_t(i)] - rgb).squaredNorm(); d < bd) { bd = d; best = i; } + return best; + }; + + TextureDisplacementOptions options; + options.pipeline_v2 = true; + options.v2_refine_mm = 1.f; + options.v2_max_triangles_k = 0; // no simplification: a plain refined quad + const indexed_triangle_set out = build_texture_displacement(quad, { layer }, facets, options, {}, &request); + + REQUIRE(out.indices.size() > 2); + REQUIRE(triangle_color.size() == out.indices.size()); + size_t coloured = 0, plain = 0; + std::set distinct; + for (size_t i = 0; i < out.indices.size(); ++i) { + const stl_triangle_vertex_indices &t = out.indices[i]; + const Vec3f c = (out.vertices[size_t(t[0])] + out.vertices[size_t(t[1])] + out.vertices[size_t(t[2])]) / 3.f; + const bool painted_side = c.y() < c.x(); // below the diagonal: triangle 0 of the quad + if (triangle_color[i] > 0) { ++coloured; distinct.insert(triangle_color[i]); CHECK(painted_side); } + else { ++plain; CHECK_FALSE(painted_side); } + } + CHECK(coloured > 0); + CHECK(plain > 0); + CHECK(distinct.size() >= 2); // the texture has four colours across the quad, so one side sees several +} + +// Per chart of an unwrap: whether it is a topological disk (V - E + F = 1, one boundary loop), the only shape a +// single island can be laid flat from. +static std::vector unwrap_charts_are_disks(const PatchUnwrap &u) +{ + std::vector> verts(size_t(u.chart_count)); + std::vector>> edges(size_t(u.chart_count)); + std::vector faces(size_t(u.chart_count), 0); + for (const stl_triangle_vertex_indices &tri : u.indices) { + const int c = u.vertex_chart[size_t(tri[0])]; + ++faces[size_t(c)]; + for (int i = 0; i < 3; ++i) { + verts[size_t(c)].insert(tri[i]); + edges[size_t(c)].insert({ std::min(tri[i], tri[(i + 1) % 3]), std::max(tri[i], tri[(i + 1) % 3]) }); + } + } + // Boundary loops per chart: flood the boundary edges' vertices. + std::vector> boundary_adj(u.uvs.size()); + for (const auto &[a, b] : u.boundary_edges) { + boundary_adj[size_t(a)].push_back(b); + boundary_adj[size_t(b)].push_back(a); + } + std::vector loops(size_t(u.chart_count), 0); + std::vector seen(u.uvs.size(), false); + for (size_t v = 0; v < u.uvs.size(); ++v) { + if (seen[v] || boundary_adj[v].empty()) + continue; + ++loops[size_t(u.vertex_chart[v])]; + std::vector stack{ int(v) }; + seen[v] = true; + while (!stack.empty()) { + const int x = stack.back(); + stack.pop_back(); + for (const int y : boundary_adj[size_t(x)]) + if (!seen[size_t(y)]) { + seen[size_t(y)] = true; + stack.push_back(y); + } + } + } + std::vector disks(size_t(u.chart_count)); + for (int c = 0; c < u.chart_count; ++c) + disks[size_t(c)] = int(verts[size_t(c)].size()) - int(edges[size_t(c)].size()) + faces[size_t(c)] == 1 && + loops[size_t(c)] == 1; + return disks; +} + +TEST_CASE("TextureDisplacement: the default pipeline bakes an unwrap (LSCM) layer", "[TextureDisplacement]") +{ + // The one-run pipeline samples per point, and an unwrap has no per-point formula, so unwrap layers + // used to be dropped from it altogether: the bake moved nothing, and the job then cleared the paint + // as if it had baked - "paint, unwrap, bake, and the painted region just disappears". + const indexed_triangle_set cube = its_make_cube(10., 10., 10.); + TextureDisplacementLayer layer; + layer.slot = 0; + layer.image_data = make_flat_gray_png(255); + layer.depth_mm = 0.5f; + layer.tiling_scale = 10.f; + layer.projection_method = TextureProjectionMethod::LSCM; + + TextureDisplacementFacetsData facets; + facets[0] = paint_whole_mesh(cube); + + TextureDisplacementOptions options; + options.pipeline_v2 = true; + options.v2_refine_mm = 2.f; + options.v2_max_triangles_k = 0; + const indexed_triangle_set out = build_texture_displacement(cube, { layer }, facets, options); + REQUIRE(!out.vertices.empty()); + + // A flat white texture at depth 0.5 pushes the faces out by 0.5, so the bounding box grows on + // every side (face interiors move the full depth; only corners, moving along their blended normal, + // move less). Before the fix it stayed exactly [0, 10]. + Vec3f lo = out.vertices.front(), hi = lo; + for (const Vec3f &v : out.vertices) { + lo = lo.cwiseMin(v); + hi = hi.cwiseMax(v); + } + CHECK(lo.x() < -0.4f); + CHECK(hi.z() > 10.4f); +} + +TEST_CASE("Per-corner LSCM UVs give each triangle its own island's placement", "[TextureDisplacement]") +{ + // compute_lscm_uvs() has to collapse a seam vertex onto one chart, because displacement is + // per vertex. Everything that samples per *triangle* must not: a cube corner belongs to three + // faces, so the collapse handed a triangle at an unjoined seam a neighbouring island's placement. + // That showed up as one visibly skewed triangle per face, and as every island's texture following + // the lowest-numbered island whenever it was dragged. + const indexed_triangle_set cube = its_make_cube(10., 10., 10.); + const PatchUnwrap unwrap = compute_patch_unwrap(cube, 30.f, 0.f); + REQUIRE(unwrap.chart_count == 6); + // The map back to the patch's own triangle order, without which there are no per-corner UVs. + REQUIRE(unwrap.source_face.size() == unwrap.indices.size()); + + TextureDisplacementLayer layer; + layer.projection_method = TextureProjectionMethod::LSCM; + layer.lscm_seam_angle_deg = 30.f; + layer.islands = compute_connected_net(unwrap); + REQUIRE(layer.islands.size() == 6); + // Move one island by hand. Its neighbours must stay exactly where they were. + layer.islands[0].offset += Vec2f(37.f, -19.f); + + const std::vector corner = compute_lscm_corner_uvs(cube, layer); + REQUIRE(corner.size() == cube.indices.size() * 3); + + for (size_t t = 0; t < unwrap.indices.size(); ++t) { + const size_t f = size_t(unwrap.source_face[t]); + REQUIRE(f < cube.indices.size()); + // A triangle lies in exactly one chart, so any of its corners names that chart. + const int c = unwrap.vertex_chart[size_t(unwrap.indices[t][0])]; + for (int k = 0; k < 3; ++k) { + const Vec2f want = apply_island_transform(unwrap.uvs[size_t(unwrap.indices[t][k])], c, unwrap, layer.islands); + CHECK_THAT(corner[f * 3 + size_t(k)].x(), WithinAbs(want.x(), 1e-4)); + CHECK_THAT(corner[f * 3 + size_t(k)].y(), WithinAbs(want.y(), 1e-4)); + } + } + + // And the per-vertex path must disagree somewhere - otherwise this test proves nothing, because + // the bug it guards against is precisely that the two were the same thing. + const std::vector per_vertex = compute_lscm_uvs(cube, layer); + REQUIRE(per_vertex.size() == cube.vertices.size()); + bool differs = false; + for (size_t f = 0; f < cube.indices.size() && !differs; ++f) + for (int k = 0; k < 3; ++k) + if ((corner[f * 3 + size_t(k)] - per_vertex[size_t(cube.indices[f][k])]).norm() > 1e-3f) + differs = true; + CHECK(differs); +} + +TEST_CASE("The Cylindrical/Spherical patch frame is the bake's own, so a preview can share it", "[TextureDisplacement]") +{ + // The fast preview reconstructs these two projections in the fragment shader and needs the very + // centroid and axis the bake wraps around - a patch-only normal average would sometimes quantize + // to a different world axis and wrap the texture the other way round. + const indexed_triangle_set cube = its_make_cube(10., 10., 10.); + const std::vector normals = texture_displacement_vertex_normals(cube); + REQUIRE(normals.size() == cube.vertices.size()); + + // Just the +X face: its average normal is +X, so the cylinder axis must be a world axis + // perpendicular to it, and the centroid must sit on that face. + indexed_triangle_set face; + face.vertices = cube.vertices; + for (const stl_triangle_vertex_indices &t : cube.indices) { + const Vec3f n = (cube.vertices[size_t(t[1])] - cube.vertices[size_t(t[0])]) + .cross(cube.vertices[size_t(t[2])] - cube.vertices[size_t(t[0])]); + if (n.normalized().x() > 0.99f) + face.indices.push_back(t); + } + REQUIRE(face.indices.size() == 2); + + Vec3f center, axis, average_normal; + texture_displacement_patch_frame(face, normals, center, axis, average_normal); + CHECK_THAT(center.x(), WithinAbs(10., 1e-4)); + CHECK_THAT(std::abs(axis.x()), WithinAbs(0., 1e-4)); // never the face's own normal direction + CHECK_THAT(axis.norm(), WithinAbs(1., 1e-4)); + + // An empty patch must not divide by zero; it falls back to +Z. + texture_displacement_patch_frame(indexed_triangle_set{}, normals, center, axis, average_normal); + CHECK_THAT(center.norm(), WithinAbs(0., 1e-6)); + CHECK_THAT(axis.z(), WithinAbs(1., 1e-6)); +} + +TEST_CASE("A cube unwraps into one island per face, laid out as a connected net", "[TextureDisplacement]") +{ + const indexed_triangle_set cube = its_make_cube(10., 10., 10.); + const PatchUnwrap unwrap = compute_patch_unwrap(cube, 30.f, 0.f); + // Two triangles per face, and every face edge but the diagonals is a 90 degree crease. + REQUIRE(unwrap.chart_count == 6); + + const std::vector islands = compute_connected_net(unwrap); + REQUIRE(islands.size() == 6); + + std::vector> placed; + std::vector placed_chart; + for (const stl_triangle_vertex_indices &tri : unwrap.indices) { + const int c = unwrap.vertex_chart[size_t(tri[0])]; + std::array t; + for (int k = 0; k < 3; ++k) + t[size_t(k)] = apply_island_transform(unwrap.uvs[size_t(tri[k])], c, unwrap, islands); + placed.push_back(t); + placed_chart.push_back(c); + } + + SECTION("no two faces overlap") + { + // A 10 mm cube: the six 100 mm2 faces laid flat without overlap cover 600 mm2, which triangles overlapping + // anywhere could not add up to within their joint bounding box... so test it directly, by sampling. + float lo_x = 1e9f, lo_y = 1e9f, hi_x = -1e9f, hi_y = -1e9f; + for (const auto &t : placed) + for (const Vec2f &p : t) { + lo_x = std::min(lo_x, p.x()); + lo_y = std::min(lo_y, p.y()); + hi_x = std::max(hi_x, p.x()); + hi_y = std::max(hi_y, p.y()); + } + const auto inside = [](const std::array &t, const Vec2f &p) { + const auto cross = [](const Vec2f &a, const Vec2f &b) { return a.x() * b.y() - a.y() * b.x(); }; + const float d0 = cross(t[1] - t[0], p - t[0]), d1 = cross(t[2] - t[1], p - t[1]), d2 = cross(t[0] - t[2], p - t[2]); + return (d0 > 1e-3f && d1 > 1e-3f && d2 > 1e-3f) || (d0 < -1e-3f && d1 < -1e-3f && d2 < -1e-3f); + }; + int overlapping = 0; + for (float x = lo_x + 0.13f; x < hi_x; x += 0.5f) + for (float y = lo_y + 0.17f; y < hi_y; y += 0.5f) { + int covering = 0; + for (const auto &t : placed) + covering += inside(t, Vec2f(x, y)) ? 1 : 0; + overlapping += covering > 1 ? 1 : 0; + } + CHECK(overlapping == 0); + } + + SECTION("every face shares an edge with another face of the net") + { + std::vector joined(6, false); + for (size_t i = 0; i < placed.size(); ++i) + for (size_t j = 0; j < placed.size(); ++j) { + if (placed_chart[i] == placed_chart[j]) + continue; + int shared_corners = 0; + for (const Vec2f &p : placed[i]) + for (const Vec2f &q : placed[j]) + shared_corners += (p - q).norm() < 1e-3f ? 1 : 0; + if (shared_corners >= 2) + joined[size_t(placed_chart[i])] = true; + } + for (int c = 0; c < 6; ++c) + CHECK(joined[size_t(c)]); + } +} + +TEST_CASE("Unwrapping a closed cylinder cuts its side until every island is a disk", "[TextureDisplacement]") +{ + // The side is smooth, so the seam angle alone leaves it as one ring-shaped chart, which cannot be laid flat. + const indexed_triangle_set cylinder = its_make_cylinder(5., 20., 2. * PI / 36.); + const PatchUnwrap unwrap = compute_patch_unwrap(cylinder, 30.f, 0.f); + REQUIRE(unwrap.chart_count >= 4); // two caps and at least two side pieces + + const std::vector disks = unwrap_charts_are_disks(unwrap); + for (int c = 0; c < unwrap.chart_count; ++c) + CHECK(disks[size_t(c)]); +} + +TEST_CASE("A UV edit on one copy of a seam vertex leaves its other copies alone", "[TextureDisplacement]") +{ + const indexed_triangle_set cube = its_make_cube(10., 10., 10.); + PatchUnwrap unwrap = compute_patch_unwrap(cube, 30.f, 0.f); + + // A cube corner has a copy in each of the three faces around it. + const int edited_copy = 0; + const int mesh_vertex = unwrap.source_vertex[size_t(edited_copy)]; + std::vector other_copies; + for (size_t i = 0; i < unwrap.uvs.size(); ++i) + if (int(i) != edited_copy && unwrap.source_vertex[i] == mesh_vertex) + other_copies.push_back(int(i)); + REQUIRE_FALSE(other_copies.empty()); + + const std::vector before = unwrap.uvs; + const Vec2f target = before[size_t(edited_copy)] + Vec2f(3.f, -2.f); + const std::vector edited = apply_lscm_uv_overrides(unwrap, { { lscm_uv_override_key(edited_copy), target } }); + + CHECK_THAT(unwrap.uvs[size_t(edited_copy)].x(), WithinAbs(target.x(), 1e-6)); + CHECK_THAT(unwrap.uvs[size_t(edited_copy)].y(), WithinAbs(target.y(), 1e-6)); + CHECK(edited[size_t(edited_copy)]); + for (const int i : other_copies) { + CHECK_THAT(unwrap.uvs[size_t(i)].x(), WithinAbs(before[size_t(i)].x(), 1e-6)); + CHECK_THAT(unwrap.uvs[size_t(i)].y(), WithinAbs(before[size_t(i)].y(), 1e-6)); + CHECK_FALSE(edited[size_t(i)]); + } +} + +TEST_CASE("TextureDisplacement: moving the model about the plate does not move the texture on it", "[TextureDisplacement]") +{ + // The bake projects in world millimetres but anchored at the volume's origin, so an instance + // translated across the plate bakes exactly the relief the same instance bakes at the origin. The + // classic path keeps the topology, so the comparison is vertex by vertex. + const indexed_triangle_set cube = subdivide_mesh_uniform(its_make_cube(10.f, 10.f, 10.f), 1.f, 6); + TextureDisplacementLayer layer; + layer.slot = 0; + layer.image_data = make_checkerboard_png(16, 16); + layer.tiling_scale = 4.f; + layer.depth_mm = 0.5f; + TextureDisplacementFacetsData facets; + facets[0] = paint_whole_mesh(cube); + + const indexed_triangle_set at_origin = build_texture_displacement(cube, { layer }, facets, classic_options()); + const indexed_triangle_set moved = build_texture_displacement(cube, { layer }, facets, classic_options(), {}, nullptr, + Transform3d(Eigen::Translation3d(123.4, -56.7, 0.0))); + REQUIRE(moved.vertices.size() == at_origin.vertices.size()); + float max_diff = 0.f; + for (size_t i = 0; i < moved.vertices.size(); ++i) + max_diff = std::max(max_diff, (moved.vertices[i] - at_origin.vertices[i]).norm()); + CHECK(max_diff < 1e-3f); +} + +// Longest edge over the shortest altitude: 1.15 for an equilateral triangle, 2 for a right isosceles +// one, unbounded for a needle. +static float triangle_aspect(const Vec3f &a, const Vec3f &b, const Vec3f &c) +{ + const float longest = std::max({ (b - a).norm(), (c - b).norm(), (a - c).norm() }); + const float twice_area = (b - a).cross(c - a).norm(); + return twice_area > 0.f ? longest * longest / twice_area : std::numeric_limits::infinity(); +} + +TEST_CASE("TextureDisplacement: baking a second face leaves the first face's relief untouched", "[TextureDisplacement]") +{ + // Paint the top of a cube and bake, then paint the front of the *result* and bake again, the way + // the gizmo does (each bake replaces the mesh and clears the baked paint). The top is unpainted the + // second time round, so it is excluded from refinement and pinned by the displacement: every + // vertex of its relief must still be there, in the same number of triangles, and no needle may + // appear on it. + const indexed_triangle_set cube = subdivide_mesh_uniform(its_make_cube(20., 20., 20.), 2.f, 6); + + TextureDisplacementLayer layer; + layer.slot = 0; + layer.image_data = make_checkerboard_png(16, 16); + layer.tiling_scale = 5.f; + layer.depth_mm = 0.5f; + + TextureDisplacementOptions options; + options.pipeline_v2 = true; + options.v2_refine_mm = 0.5f; + options.v2_max_triangles_k = 0; // no simplification + + // Paints exactly the triangles whose three corners satisfy `on_face` - no brush spill. + const auto paint_where = [](const indexed_triangle_set &mesh, auto on_face) { + const TriangleMesh tm(mesh); + TriangleSelector selector(tm); + for (size_t f = 0; f < mesh.indices.size(); ++f) { + const stl_triangle_vertex_indices &t = mesh.indices[f]; + if (on_face(mesh.vertices[size_t(t[0])]) && on_face(mesh.vertices[size_t(t[1])]) && + on_face(mesh.vertices[size_t(t[2])])) + selector.set_facet(int(f), EnforcerBlockerType::ENFORCER); + } + return selector.serialize(); + }; + const auto on_top = [](const Vec3f &v) { return v.z() > 19.9f; }; + const auto on_front = [](const Vec3f &v) { return v.y() < 0.1f; }; + + // The top region of a result: its vertices, and its triangles' count and worst aspect ratio. + struct TopRegion + { + std::vector vertices; + size_t triangles = 0; + float max_aspect = 0.f; + }; + const auto top_region = [&on_top](const indexed_triangle_set &mesh) { + TopRegion r; + for (const Vec3f &v : mesh.vertices) + if (on_top(v)) + r.vertices.push_back(v); + for (const stl_triangle_vertex_indices &t : mesh.indices) { + const Vec3f &a = mesh.vertices[size_t(t[0])], &b = mesh.vertices[size_t(t[1])], &c = mesh.vertices[size_t(t[2])]; + if (!on_top(a) || !on_top(b) || !on_top(c)) + continue; + ++r.triangles; + r.max_aspect = std::max(r.max_aspect, triangle_aspect(a, b, c)); + } + return r; + }; + + TextureDisplacementFacetsData facets{}; + facets[0] = paint_where(cube, on_top); + const indexed_triangle_set first = build_texture_displacement(cube, { layer }, facets, options); + REQUIRE(first.indices.size() > cube.indices.size()); + const TopRegion top_before = top_region(first); + REQUIRE(top_before.triangles > 0); + // The relief really is there: the checkerboard raises part of the top by the full depth. + float top_z_max = 0.f; + for (const Vec3f &v : top_before.vertices) + top_z_max = std::max(top_z_max, v.z()); + REQUIRE(top_z_max > 20.3f); + REQUIRE(top_before.max_aspect < 20.f); + + // Only the front of the baked mesh is painted for the second bake. + facets[0] = paint_where(first, on_front); + REQUIRE_FALSE(facets[0].triangles_to_split.empty()); + + const auto check_top_untouched = [&](const indexed_triangle_set &second) { + REQUIRE_FALSE(second.indices.empty()); // an aborted bake returns {} + const TopRegion top_after = top_region(second); + + // Every vertex of the first relief still exists, at the same place. O(n*m) over a few + // thousand vertices each, restricted to the top region on both sides. + size_t missing = 0; + Vec3f first_missing = Vec3f::Zero(); + for (const Vec3f &v : top_before.vertices) { + bool found = false; + for (const Vec3f &w : top_after.vertices) + if ((w - v).squaredNorm() <= 1e-6f) { // within 1e-3 mm + found = true; + break; + } + if (!found) { + if (missing == 0) + first_missing = v; + ++missing; + } + } + INFO("first vertex of the top relief missing from the second bake: " << first_missing.transpose()); + CHECK(missing == 0); + + // Nothing was added to or taken from the top either - its triangles are excluded from the + // refinement, and a rim edge it shares with the front was already at the refine length. + CHECK(top_after.triangles == top_before.triangles); + + // And no needles: the worst triangle on the top is no worse than after the first bake. + INFO("worst top-face aspect ratio after the second bake: " << top_after.max_aspect + << ", after the first: " << top_before.max_aspect); + CHECK(top_after.max_aspect < 20.f); + }; + + SECTION("bake mode: no simplification") + { + check_top_untouched(build_texture_displacement(first, { layer }, facets, options)); + } + + SECTION("export mode: simplification and T-junction repair run over the excluded region too") + { + // A budget far below the mesh forces the decimation (the locked top alone is over it, so it + // only harvests flat faces) and with it the repair pass - the two stages that walk every face, + // excluded ones included. + TextureDisplacementOptions export_options = options; + export_options.v2_max_triangles_k = 1; + check_top_untouched(build_texture_displacement(first, { layer }, facets, export_options)); + } +} + +TEST_CASE("TextureDisplacement: a brush stroke smaller than a triangle displaces only the stroke", "[TextureDisplacement]") +{ + // A plain 12-triangle cube. The selector splits the top triangle under a 3 mm spherical brush, + // so the painted pieces are far smaller than the triangle. The one-run pipeline used to include + // the whole source triangle: the entire top face rose. + const indexed_triangle_set cube = its_make_cube(20.f, 20.f, 20.f); + TextureDisplacementFacetsData facets; + facets[0] = paint_top_disc(cube, 20.f, Vec2f(10.f, 10.f), 3.f); + REQUIRE(TriangleSelector::has_facets(facets[0], EnforcerBlockerType::ENFORCER)); + + TextureDisplacementLayer layer; + layer.slot = 0; + layer.image_data = make_flat_gray_png(255, 8, 8); // uniform full height: every painted point rises + layer.tiling_scale = 4.f; + layer.depth_mm = 0.5f; + TextureDisplacementOptions options; + options.pipeline_v2 = true; + options.v2_refine_mm = 0.5f; + options.v2_max_triangles_k = 0; + const indexed_triangle_set out = build_texture_displacement(cube, { layer }, facets, options); + REQUIRE(out.indices.size() > cube.indices.size()); + + size_t raised_inside = 0, raised_outside = 0, outside = 0; + for (const Vec3f &v : out.vertices) { + if (v.z() < 19.9f) + continue; // not the top face + const float r = (Vec2f(v.x(), v.y()) - Vec2f(10.f, 10.f)).norm(); + if (r < 2.f && v.z() > 20.3f) + ++raised_inside; + if (r > 4.5f) { + ++outside; + if (v.z() > 20.01f) + ++raised_outside; + } + } + CHECK(raised_inside > 0); // the stroke itself is displaced + CHECK(outside > 0); + CHECK(raised_outside == 0); // the rest of the face, inside the same source triangle, is not +} + +TEST_CASE("TextureDisplacement: a texture of flat colours is told apart from a continuous one", "[TextureDisplacement]") +{ + // The verdict that decides whether a layer may use filament mixes: a checkerboard is two colours, + // a ramp spreads over every level. + TextureDisplacementLayer checker; + checker.image_data = make_checkerboard_png(32, 32); + CHECK(analyze_texture_detail(checker).flat_colors); + + std::vector ramp(64 * 64); + for (size_t y = 0; y < 64; ++y) + for (size_t x = 0; x < 64; ++x) + ramp[y * 64 + x] = uint8_t((x * 4 + y) & 255); + const boost::filesystem::path tmp_path = boost::filesystem::temp_directory_path() + / boost::filesystem::unique_path("texdisp_test_%%%%%%%%.png"); + REQUIRE(Slic3r::png::write_gray_to_file(tmp_path.string(), 64, 64, ramp)); + std::vector bytes; + { + std::ifstream ifs(tmp_path.string(), std::ios::binary); + bytes.assign(std::istreambuf_iterator(ifs), std::istreambuf_iterator()); + } + boost::system::error_code ec; + boost::filesystem::remove(tmp_path, ec); + TextureDisplacementLayer gradient; + gradient.image_data = std::make_shared>(std::move(bytes)); + const TextureDetail d = analyze_texture_detail(gradient); + CHECK_FALSE(d.flat_colors); + CHECK(d.flat_share < 0.85f); +} + +TEST_CASE("Each layer's texture is sampled only on its own painted area", "[TextureDisplacement]") +{ + // Two layers with different textures and depths, one painted on the cube's top, one on its -X side. + const indexed_triangle_set cube = its_make_cube(10., 10., 10.); + const TriangleMesh cube_mesh(cube); + const auto paint_facing = [&](const Vec3f &dir) { + TriangleSelector selector(cube_mesh); + for (int f = 0; f < int(cube.indices.size()); ++f) { + const stl_triangle_vertex_indices &t = cube.indices[size_t(f)]; + const Vec3f n = (cube.vertices[size_t(t[1])] - cube.vertices[size_t(t[0])]) + .cross(cube.vertices[size_t(t[2])] - cube.vertices[size_t(t[0])]) + .normalized(); + if (n.dot(dir) > 0.99f) + selector.set_facet(f, EnforcerBlockerType::ENFORCER); + } + return selector.serialize(); + }; + TextureDisplacementFacetsData facets{}; + facets[0] = paint_facing(Vec3f::UnitZ()); + facets[1] = paint_facing(-Vec3f::UnitX()); + + TextureDisplacementLayer top; + top.slot = 0; + top.depth_mm = 1.0f; + top.tiling_scale = 5.0f; + top.image_data = make_flat_gray_png(255); + TextureDisplacementLayer side = top; + side.slot = 1; + side.depth_mm = 0.5f; + side.image_data = make_flat_gray_png(64); + + const HeightFieldSampler both = make_combined_displacement_sampler(cube, { top, side }, facets); + const HeightFieldSampler top_only = make_combined_displacement_sampler(cube, { top }, facets); + const HeightFieldSampler side_only = make_combined_displacement_sampler(cube, { side }, facets); + REQUIRE(both); + REQUIRE(top_only); + REQUIRE(side_only); + + const Vec3f on_top(5.f, 5.f, 10.f), on_side(0.f, 5.f, 5.f), unpainted(5.f, 10.f, 5.f); + CHECK_THAT(both(on_top, Vec3f::UnitZ()), WithinAbs(top_only(on_top, Vec3f::UnitZ()), 1e-5f)); + CHECK_THAT(both(on_side, -Vec3f::UnitX()), WithinAbs(side_only(on_side, -Vec3f::UnitX()), 1e-5f)); + CHECK_THAT(both(unpainted, Vec3f::UnitY()), WithinAbs(0.f, 1e-6f)); +} + +TEST_CASE("A budget met by collapsing flat faces alone removes no detail", "[TextureDisplacement]") +{ + // 12 * 4^5 = 12288 triangles on six flat faces, which can go down to 12 without moving the surface. + const TextureBake::TriSoup cube = TextureBake::to_soup(subdivide_mesh_uniform(its_make_cube(20., 20., 20.), 0.f, 5)); + const TextureBake::DecimateResult res = TextureBake::decimate(cube, 1000, true, 0.005); + CHECK(res.geometry.triangle_count() <= 1000); + CHECK_FALSE(res.target_cost_detail); +} + +TEST_CASE("A budget met only by flattening curvature removes detail", "[TextureDisplacement]") +{ + // A 10 degree sphere of radius 10 bulges about 0.04 mm out of each facet, far past a 0.005 mm tolerance. + const TextureBake::TriSoup sphere = TextureBake::to_soup(its_make_sphere(10., PI / 18.)); + const TextureBake::DecimateResult res = TextureBake::decimate(sphere, 100, true, 0.005); + CHECK(res.geometry.triangle_count() <= 100); + CHECK(res.target_cost_detail); +} + +TEST_CASE("Cancelling while flat faces are harvested past the budget stops the decimation", "[TextureDisplacement]") +{ + // 12 * 4^6 = 49152 triangles: the budget is met almost at once, and the harvest after it runs to + // a dozen triangles over several times the two polling intervals a cancel takes to be seen. + const TextureBake::TriSoup cube = TextureBake::to_soup(subdivide_mesh_uniform(its_make_cube(20., 20., 20.), 0.f, 6)); + const size_t target = cube.triangle_count() - 1000; + const size_t full = TextureBake::decimate(cube, target, true, 0.005).geometry.triangle_count(); + + // The Cancel button pressed once the bar is full: every call after the first to report 1.0 cancels. + bool full_seen = false; + const auto cancel_when_full = [&full_seen](double f) { + if (full_seen) + return false; + full_seen = f >= 1.0; + return true; + }; + const size_t canceled = TextureBake::decimate(cube, target, true, 0.005, {}, cancel_when_full).geometry.triangle_count(); + CHECK(full_seen); + CHECK(canceled <= target); + CHECK(canceled > full); +} + +TEST_CASE("A second bake beside a first comes out as fine as a single bake", "[TextureDisplacement]") +{ + // A stroke in each of the cube's two top triangles, baked one after the other. The first bake used + // to leave the unpainted ground fanned into slivers from a far corner, or re-triangulated by the + // flat harvest into long triangles, and the second bake refined those shapes rather than the + // cube's own grid: its stroke came out many times as dense, with walls off the texture's lines. + const indexed_triangle_set cube = its_make_cube(20.f, 20.f, 20.f); + const Vec2f first_stroke(5.f, 15.f), second_stroke(15.f, 5.f); + constexpr float radius = 3.f; + + TextureDisplacementLayer layer; + layer.slot = 0; + layer.image_data = make_checkerboard_png(16, 16); + layer.tiling_scale = 5.f; + layer.depth_mm = 0.5f; + TextureDisplacementOptions options; + options.pipeline_v2 = true; + options.v2_refine_mm = 0.5f; + options.v2_max_triangles_k = 750; // export: the flat harvest runs + const auto bake = [&](const indexed_triangle_set &mesh, const Vec2f &stroke) { + TextureDisplacementFacetsData facets; + facets[0] = paint_top_disc(mesh, 20.f, stroke, radius); + return build_texture_displacement(mesh, { layer }, facets, options); + }; + // Aspect ratios of the top-face triangles whose centroid's distance from `centre` passes `keep`. + const auto top_aspects = [](const indexed_triangle_set &mesh, const Vec2f ¢re, auto keep) { + std::vector out; + for (const stl_triangle_vertex_indices &t : mesh.indices) { + const Vec3f &a = mesh.vertices[size_t(t[0])], &b = mesh.vertices[size_t(t[1])], &c = mesh.vertices[size_t(t[2])]; + const Vec3f g = (a + b + c) / 3.f; + if (g.z() > 19.9f && keep((Vec2f(g.x(), g.y()) - centre).norm())) + out.push_back(triangle_aspect(a, b, c)); + } + return out; + }; + const auto in_stroke = [&](float d) { return d < radius; }; + + const indexed_triangle_set first = bake(cube, first_stroke); + REQUIRE(first.indices.size() > cube.indices.size()); + // What the first bake leaves around its stroke is ground the second one refines: whole pieces of + // the cube's grid, not slivers. + const std::vector ground = top_aspects(first, first_stroke, [&](float d) { return d > radius + 1.f; }); + REQUIRE(!ground.empty()); + const float worst = *std::max_element(ground.begin(), ground.end()); + INFO("worst aspect ratio on the ground the first bake left: " << worst); + CHECK(worst < 6.f); + + const size_t single = top_aspects(bake(cube, second_stroke), second_stroke, in_stroke).size(); + const size_t second = top_aspects(bake(first, second_stroke), second_stroke, in_stroke).size(); + INFO("second stroke baked alone: " << single << " triangles, after the first: " << second); + REQUIRE(single > 0); + CHECK(second <= single * 5 / 4); +} + diff --git a/tests/libslic3r/test_toolordering_nozzle_group.cpp b/tests/libslic3r/test_toolordering_nozzle_group.cpp index d9a5d70406..4074a80995 100644 --- a/tests/libslic3r/test_toolordering_nozzle_group.cpp +++ b/tests/libslic3r/test_toolordering_nozzle_group.cpp @@ -948,6 +948,9 @@ TEST_CASE("Filaments ordered after a migrator shift columns and the resolver tra config.option("nozzle_temperature", true)->values = {200, 210, 220, 230, 240, 250}; config.option("filament_retraction_length", true)->values = {0.5, 0.5, 0.7, 0.9, 1.4, 1.4}; config.option("retraction_length", true)->values = {0.8, 0.9, 1.0, 1.1}; + config.option("fan_max_speed", true)->values = {10, 10, 20, 60, 30, 30}; + config.option("additional_cooling_fan_speed", true)->values = {1, 1, 2, 6, 3, 3}; + config.option("nozzle_temperature_range_high", true)->values = {230, 230, 240, 280, 250, 250}; Model model; model.add_object("cube", "", make_cube(20, 20, 20))->add_instance(); @@ -988,6 +991,21 @@ TEST_CASE("Filaments ordered after a migrator shift columns and the resolver tra REQUIRE(merged.size() == 4); REQUIRE_THAT(merged[3], Catch::Matchers::WithinAbs(1.4, 1e-9)); REQUIRE_THAT(merged[2], Catch::Matchers::WithinAbs(0.9, 1e-9)); + + // The cooling and temperature range options follow the variant as well. + const PrintConfig &resolved = print.config(); + REQUIRE(resolved.fan_max_speed.values == std::vector{10, 20, 60, 30}); + CHECK(resolved.fan_max_speed.get_at(print.get_filament_config_indx(1, 0)) == 20); + CHECK(resolved.fan_max_speed.get_at(print.get_filament_config_indx(1, 1)) == 60); + CHECK(resolved.additional_cooling_fan_speed.get_at(print.get_filament_config_indx(2, 1)) == 3); + CHECK(resolved.nozzle_temperature_range_high.get_at(print.get_filament_config_indx(2, 1)) == 250); + // The auxiliary fan maximum takes every variant of the filaments used, and only theirs. + ToolOrdering ordering; + ordering.layer_tools().emplace_back(0.2); + ordering.layer_tools().back().extruders = {0, 2}; + CHECK(ordering.cal_max_additional_fan(resolved) == 3); + ordering.layer_tools().back().extruders = {1}; + CHECK(ordering.cal_max_additional_fan(resolved) == 6); } TEST_CASE("Selector slicing keeps the result valid across re-apply", "[Print][H2C][Dynamic]") diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index eac5165b43..1bb0e7c5b9 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -13,6 +13,7 @@ add_executable(${_TEST_NAME}_tests test_prebuild_queue.cpp test_staged_build.cpp test_network_versions.cpp + test_orca_cloud_agent.cpp test_action_source.cpp test_plugin_host_api.cpp # Exercise seam enums and predicates through the embedded Python host API. diff --git a/tests/slic3rutils/python_test_support.hpp b/tests/slic3rutils/python_test_support.hpp index ab66f05887..29747a88c4 100644 --- a/tests/slic3rutils/python_test_support.hpp +++ b/tests/slic3rutils/python_test_support.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -25,8 +26,15 @@ void ensure_python_initialized() config.parse_argv = 0; const auto python_home = boost::dll::program_location().parent_path() / "python"; +#ifdef _WIN32 + const auto stdlib = python_home / "Lib"; +#else + const auto stdlib = python_home / "lib" / + ("python" + std::to_string(PY_MAJOR_VERSION) + "." + std::to_string(PY_MINOR_VERSION)); +#endif - if (boost::filesystem::exists(python_home)) { + // Only a real runtime: a stray python/ folder (packages a test left behind) is not a home. + if (boost::filesystem::exists(stdlib / "encodings")) { const std::string home = python_home.string(); const PyStatus status = PyConfig_SetBytesString(&config, &config.home, home.c_str()); diff --git a/tests/slic3rutils/test_orca_cloud_agent.cpp b/tests/slic3rutils/test_orca_cloud_agent.cpp new file mode 100644 index 0000000000..76f4e8df16 --- /dev/null +++ b/tests/slic3rutils/test_orca_cloud_agent.cpp @@ -0,0 +1,86 @@ +#include + +#include +#include + +#include +#include + +#include "slic3r/Utils/OrcaCloudServiceAgent.hpp" +#include "test_utils.hpp" + +using namespace Slic3r; +namespace fs = boost::filesystem; + +namespace { + +// The encrypted token file is the one secret backend a test can observe without a system +// keychain. Every agent pointed at the same directory shares it, like separate app instances +// share the keychain entry. +std::unique_ptr make_file_backed_agent(const fs::path& dir) +{ + auto agent = std::make_unique(dir.string()); + agent->set_use_encrypted_token_file(true); + agent->set_config_dir(dir.string()); + return agent; +} + +fs::path secret_file(const fs::path& dir) { return dir / secret_constants::USER_SECRET_FILENAME; } + +} // namespace + +TEST_CASE("Logging out removes the secret this instance saved", "[OrcaCloudServiceAgent]") +{ + ScopedTemporaryDir dir("orca-secret"); + auto agent = make_file_backed_agent(dir.path()); + + agent->persist_user_secret("refresh-token"); + REQUIRE(fs::exists(secret_file(dir.path()))); + + agent->user_logout(false); + CHECK_FALSE(fs::exists(secret_file(dir.path()))); +} + +TEST_CASE("Logging out removes a secret this instance loaded from the store", "[OrcaCloudServiceAgent]") +{ + ScopedTemporaryDir dir("orca-secret"); + make_file_backed_agent(dir.path())->persist_user_secret("refresh-token"); + + auto agent = make_file_backed_agent(dir.path()); + std::string secret; + REQUIRE(agent->load_user_secret(secret)); + CHECK(secret == "refresh-token"); + + agent->user_logout(false); + CHECK_FALSE(fs::exists(secret_file(dir.path()))); +} + +TEST_CASE("Logging out leaves a secret this instance never loaded or saved alone", "[OrcaCloudServiceAgent]") +{ + ScopedTemporaryDir dir("orca-secret"); + make_file_backed_agent(dir.path())->persist_user_secret("refresh-token"); + + // A logged-out instance is asked to log out on every login-status poll. + auto other = make_file_backed_agent(dir.path()); + other->user_logout(false); + other->user_logout(false); + CHECK(fs::exists(secret_file(dir.path()))); + + std::string secret; + REQUIRE(make_file_backed_agent(dir.path())->load_user_secret(secret)); + CHECK(secret == "refresh-token"); +} + +TEST_CASE("Logging out leaves a secret this instance could not read alone", "[OrcaCloudServiceAgent]") +{ + ScopedTemporaryDir dir("orca-secret"); + // Written under another encryption key, e.g. by another OS user sharing the data directory. + fs::ofstream(secret_file(dir.path())) << "v2:0000:not-a-payload-this-user-can-decrypt"; + + auto agent = make_file_backed_agent(dir.path()); + std::string secret; + REQUIRE_FALSE(agent->load_user_secret(secret)); + + agent->user_logout(false); + CHECK(fs::exists(secret_file(dir.path()))); +} diff --git a/tests/slic3rutils/test_plugin_capability_config.cpp b/tests/slic3rutils/test_plugin_capability_config.cpp index f224f92308..39ecd1002d 100644 --- a/tests/slic3rutils/test_plugin_capability_config.cpp +++ b/tests/slic3rutils/test_plugin_capability_config.cpp @@ -38,6 +38,9 @@ namespace { // before this destructor's shutdown() runs. struct ScopedPluginManager { + // Before initialize(): the interpreter creates {data_dir}/python/packages and {data_dir}/log, + // which would otherwise land in the working directory. + ScopedDataDir python_data_dir{"plugin-python"}; bool initialized = PluginManager::instance().initialize(); ~ScopedPluginManager() diff --git a/tests/slic3rutils/test_plugin_cloud_metadata.cpp b/tests/slic3rutils/test_plugin_cloud_metadata.cpp index c34b596ee0..241ef3ed90 100644 --- a/tests/slic3rutils/test_plugin_cloud_metadata.cpp +++ b/tests/slic3rutils/test_plugin_cloud_metadata.cpp @@ -31,6 +31,9 @@ namespace { // same as any other plugin. struct ScopedManagerShutdown { + // Before initialize(): the interpreter creates {data_dir}/python/packages and {data_dir}/log, + // which would otherwise land in the working directory. + ScopedDataDir python_data_dir{"plugin-python"}; bool initialized = PluginManager::instance().initialize(); ~ScopedManagerShutdown() diff --git a/tests/slic3rutils/test_plugin_lifecycle.cpp b/tests/slic3rutils/test_plugin_lifecycle.cpp index 0c96f8c0b3..d90a166835 100644 --- a/tests/slic3rutils/test_plugin_lifecycle.cpp +++ b/tests/slic3rutils/test_plugin_lifecycle.cpp @@ -42,6 +42,9 @@ namespace { // Declare this FIRST in a test so it is destroyed last. struct ScopedPluginManager { + // Before initialize(): the interpreter creates {data_dir}/python/packages and {data_dir}/log, + // which would otherwise land in the working directory. + ScopedDataDir python_data_dir{"plugin-python"}; bool initialized = false; ScopedPluginManager() { initialized = PluginManager::instance().initialize(); } diff --git a/tests/slic3rutils/test_plugin_printer_agent.cpp b/tests/slic3rutils/test_plugin_printer_agent.cpp index fc85363a39..c1bba8a88e 100644 --- a/tests/slic3rutils/test_plugin_printer_agent.cpp +++ b/tests/slic3rutils/test_plugin_printer_agent.cpp @@ -12,6 +12,8 @@ #include #include +#include "plugin_test_utils.hpp" + namespace py = pybind11; using namespace Slic3r; @@ -21,6 +23,9 @@ namespace { // into Python unless PythonInterpreter::instance() reports initialized. struct ScopedPluginManager { + // Before initialize(): the interpreter creates {data_dir}/python/packages and {data_dir}/log, + // which would otherwise land in the working directory. + ScopedDataDir python_data_dir{"plugin-python"}; bool initialized = PluginManager::instance().initialize(); ~ScopedPluginManager() diff --git a/tests/slic3rutils/test_slicing_pipeline_config.cpp b/tests/slic3rutils/test_slicing_pipeline_config.cpp index ff8cddcba6..37984cff34 100644 --- a/tests/slic3rutils/test_slicing_pipeline_config.cpp +++ b/tests/slic3rutils/test_slicing_pipeline_config.cpp @@ -35,6 +35,9 @@ namespace { struct ScopedPluginManager { + // Before initialize(): the interpreter creates {data_dir}/python/packages and {data_dir}/log, + // which would otherwise land in the working directory. + ScopedDataDir python_data_dir{"plugin-python"}; bool initialized = false; ScopedPluginManager() { initialized = PluginManager::instance().initialize(); }