diff --git a/LocalPlayer/StrokeThrottle.h b/LocalPlayer/StrokeThrottle.h index 3d8e1c9..0214062 100644 --- a/LocalPlayer/StrokeThrottle.h +++ b/LocalPlayer/StrokeThrottle.h @@ -12,6 +12,7 @@ class StrokeThrottle { void Push(const SegmentData& seg); int DrawPending(AppState* state); + int GetBudget() const { return m_dynamicBudget; } private: SegmentData m_segQ[SEG_CAP]; diff --git a/include/brush_preset.h b/include/brush_preset.h index e12211f..7b51f10 100644 --- a/include/brush_preset.h +++ b/include/brush_preset.h @@ -8,7 +8,7 @@ #define BRUSH_PRESET_NAME_MAX 64 #define BRUSH_PRESET_MAX 256 #define PRESET_FILE_MAGIC "REPRESET" -#define PRESET_FILE_VER 2 +#define PRESET_FILE_VER 5 typedef struct { char name[BRUSH_PRESET_NAME_MAX]; @@ -16,11 +16,11 @@ typedef struct { /* Tool */ int mode, eraseMode; - /* BParam slider values (clipmaxF) and pen modes */ - struct { float val; int penMode; } bp[19]; + /* BParam slider values (clipmaxF, clipminF, jitter) and pen modes */ + struct { float val, clipmin, jitter; int penMode; } bp[20]; /* Texture options */ - int texBlendMode, texNoisemode, texColorMode; + int texBlendMode, texNoisemode, texColorMode, texTiling; bool useTexLumAsAlpha, texUseRGB; char texName[64]; @@ -29,6 +29,9 @@ typedef struct { bool preserveop, seamlessPaint; int strokeSmoothingMode; float strokeThrottle; + + /* Initial angle (added in V5) */ + float initialAngle; } BrushPreset; /* ── File I/O ── */ diff --git a/resources/Noise/clouds.png b/resources/Noise/clouds.png deleted file mode 100644 index 4233207..0000000 Binary files a/resources/Noise/clouds.png and /dev/null differ diff --git a/resources/Noise/dots.png b/resources/Noise/dots.png deleted file mode 100644 index 3a372d9..0000000 Binary files a/resources/Noise/dots.png and /dev/null differ diff --git a/resources/Noise/noise4k.png b/resources/Noise/noise4k.png deleted file mode 100644 index b6e6506..0000000 Binary files a/resources/Noise/noise4k.png and /dev/null differ diff --git a/resources/Noise/perlin.png b/resources/Noise/perlin.png deleted file mode 100644 index 758abe5..0000000 Binary files a/resources/Noise/perlin.png and /dev/null differ diff --git a/resources/Noise/test_nrm-mir2r.png b/resources/Noise/test_nrm-mir2r.png deleted file mode 100644 index c6b6d24..0000000 Binary files a/resources/Noise/test_nrm-mir2r.png and /dev/null differ diff --git a/resources/default_presets.dat b/resources/default_presets.dat index 4e9179b..4efdf39 100644 Binary files a/resources/default_presets.dat and b/resources/default_presets.dat differ diff --git a/src/brush_preset.cpp b/src/brush_preset.cpp index d295e9b..b0e9736 100644 --- a/src/brush_preset.cpp +++ b/src/brush_preset.cpp @@ -1,5 +1,5 @@ #include "brush_preset.h" - +// TODO - can drop all backwards compat. // ── BParam index mapping ────────────────────────────────────────────── enum { BI_SIZE, BI_SIZEMUL, BI_HARDNESS, BI_CURVATURE, BI_SPACING, @@ -7,6 +7,7 @@ enum { BI_POWER, BI_PERSPECTIVE, BI_QUICKHUE, BI_QUICKSAT, BI_QUICKLIT, BI_TEXSCALE, BI_TEXFEATHER, BI_TEXTHRESH, BI_TEXBLENDVAL, + BI_FOCALOFFSET, BI_COUNT }; @@ -19,7 +20,7 @@ static void _ensureBPs(void) { extern BParam bpOpacity, bpSize, bpHardness, bpSpacing, bpCurvature, bpScatter; extern BParam bpCloneOpacity, bpQuickHue, bpQuickSat, bpQuickLit; extern BParam bpTexScale, bpTexFeather, bpTexThresh, bpTexBlendVal; - extern BParam bpAngle, bpScaleRel, bpSizeMul, bpPower, bpPerspective; + extern BParam bpAngle, bpScaleRel, bpSizeMul, bpPower, bpPerspective, bpFocalOffset; _bps[BI_SIZE] = &bpSize; _bps[BI_SIZEMUL] = &bpSizeMul; _bps[BI_HARDNESS] = &bpHardness; @@ -39,6 +40,7 @@ static void _ensureBPs(void) { _bps[BI_TEXFEATHER] = &bpTexFeather; _bps[BI_TEXTHRESH] = &bpTexThresh; _bps[BI_TEXBLENDVAL] = &bpTexBlendVal; + _bps[BI_FOCALOFFSET] = &bpFocalOffset; } void Preset_CaptureFromCurrent(BrushPreset* p, AppState* state) { @@ -50,12 +52,15 @@ void Preset_CaptureFromCurrent(BrushPreset* p, AppState* state) { for (int i = 0; i < BI_COUNT; i++) { p->bp[i].val = _bps[i]->user.clipmaxF; + p->bp[i].clipmin = _bps[i]->user.clipminF; + p->bp[i].jitter = _bps[i]->user.jitter; p->bp[i].penMode = _bps[i]->penMode; } p->texBlendMode = state->currentBrush.Realb.texBlendMode; p->texNoisemode = state->currentBrush.Realb.texNoisemode; p->texColorMode = state->currentBrush.Realb.texColorMode; + p->texTiling = state->currentBrush.Realb.texTiling; p->useTexLumAsAlpha = state->currentBrush.Realb.useTexLumAsAlpha; p->texUseRGB = state->currentBrush.Realb.texUseRGB; @@ -75,6 +80,7 @@ void Preset_CaptureFromCurrent(BrushPreset* p, AppState* state) { extern float g_strokeThrottle; p->strokeSmoothingMode = g_strokeSmoothingMode; p->strokeThrottle = g_strokeThrottle; + p->initialAngle = state->initialAngle; } // ── Validation ──────────────────────────────────────────────────────── @@ -101,14 +107,16 @@ void Preset_ApplyToCurrent(const BrushPreset* p, AppState* state) { state->eraseMode = p->eraseMode; for (int i = 0; i < BI_COUNT; i++) { - if (i == BI_QUICKHUE || i == BI_QUICKSAT || i == BI_QUICKLIT) continue; _bps[i]->user.clipmaxF = p->bp[i].val; + _bps[i]->user.clipminF = p->bp[i].clipmin; + _bps[i]->user.jitter = p->bp[i].jitter; _bps[i]->penMode = p->bp[i].penMode; } state->currentBrush.Realb.texBlendMode = p->texBlendMode; state->currentBrush.Realb.texNoisemode = p->texNoisemode; state->currentBrush.Realb.texColorMode = p->texColorMode; + state->currentBrush.Realb.texTiling = p->texTiling; state->currentBrush.Realb.useTexLumAsAlpha = p->useTexLumAsAlpha; state->currentBrush.Realb.texUseRGB = p->texUseRGB; state->currentBrush.Realb.bmidx = p->bmidx; @@ -137,11 +145,12 @@ void Preset_ApplyToCurrent(const BrushPreset* p, AppState* state) { extern float g_strokeThrottle; g_strokeSmoothingMode = p->strokeSmoothingMode; g_strokeThrottle = p->strokeThrottle; + state->initialAngle = p->initialAngle; } // ── File I/O ── -// Old struct for version-1 files (exact layout as originally written) +// Version-1: splineMinDist + splineAngleThreshold, old bp[19]{float,int}, no focalOffset struct PresetV1 { char name[64]; int mode, eraseMode; @@ -155,10 +164,55 @@ struct PresetV1 { float splineMinDist; float splineAngleThreshold; }; -// Verify the old struct has the same size as originally written -// (4 bytes of padding between the bools and the final fields) -static_assert(sizeof(PresetV1) == sizeof(BrushPreset) + 4, - "PresetV1 layout mismatch — check alignment"); +static_assert(sizeof(PresetV1) == 324, "PresetV1 layout mismatch"); + +// Version-2: strokeThrottle, no splineAngle, old bp[19]{float,int}, no focalOffset +struct PresetV2 { + char name[64]; + int mode, eraseMode; + struct { float val; int penMode; } bp[19]; + int texBlendMode, texNoisemode, texColorMode; + bool useTexLumAsAlpha, texUseRGB; + char texName[64]; + int bmidx; + bool preserveop, seamlessPaint; + int strokeSmoothingMode; + float strokeThrottle; +}; +static_assert(sizeof(PresetV2) == 320, "PresetV2 layout mismatch"); + +// Version-3: old bp[19]{float,int} + focalOffsetVal/penMode at end +struct PresetV3 { + char name[64]; + int mode, eraseMode; + struct { float val; int penMode; } bp[19]; + int texBlendMode, texNoisemode, texColorMode; + bool useTexLumAsAlpha, texUseRGB; + char texName[64]; + int bmidx; + bool preserveop, seamlessPaint; + int strokeSmoothingMode; + float strokeThrottle; + float focalOffsetVal; + int focalOffsetPenMode; +}; +static_assert(sizeof(PresetV3) == 328, "PresetV3 layout mismatch"); + +// Version-4: new bp[19]{val,clipmin,jitter,penMode} + focalOffsetVal/penMode at end +struct PresetV4 { + char name[64]; + int mode, eraseMode; + struct { float val, clipmin, jitter; int penMode; } bp[19]; + int texBlendMode, texNoisemode, texColorMode; + bool useTexLumAsAlpha, texUseRGB; + char texName[64]; + int bmidx; + bool preserveop, seamlessPaint; + int strokeSmoothingMode; + float strokeThrottle; + float focalOffsetVal; + int focalOffsetPenMode; +}; static int _loadFile(const char* path, BrushPreset* out, int maxCount) { FILE* f = fopen(path, "rb"); @@ -169,7 +223,6 @@ static int _loadFile(const char* path, BrushPreset* out, int maxCount) { fclose(f); return 0; } - // Read version (ignored — we detect format from file size) unsigned char verBuf[4]; if (fread(verBuf, 1, 4, f) != 4) { fclose(f); return 0; } @@ -178,10 +231,9 @@ static int _loadFile(const char* path, BrushPreset* out, int maxCount) { int count = countBuf[0] | (countBuf[1] << 8) | (countBuf[2] << 16) | (countBuf[3] << 24); if (count < 0 || count > maxCount) { fclose(f); return 0; } - // Detect entry size from file size to handle format changes fseek(f, 0, SEEK_END); long fileSize = ftell(f); - fseek(f, 16, SEEK_SET); // back to start of data + fseek(f, 16, SEEK_SET); long dataSize = fileSize - 16; int entrySize = (count > 0) ? (int)(dataSize / count) : 0; @@ -189,12 +241,112 @@ static int _loadFile(const char* path, BrushPreset* out, int maxCount) { int loaded = 0; + // Seed clipmin/jitter/penMode for all bp entries from live BParams + // so old-format files that lack those fields keep the user's live values. + _ensureBPs(); + for (int i = 0; i < count && i < maxCount; i++) { + BrushPreset* p = &out[i]; + memset(p, 0, sizeof(*p)); + for (int j = 0; j < BI_COUNT; j++) { + p->bp[j].clipmin = _bps[j]->user.clipminF; + p->bp[j].jitter = _bps[j]->user.jitter; + p->bp[j].penMode = _bps[j]->penMode; + } + } + if (entrySize == sizeof(BrushPreset)) { - // Current format — read directly + // V5 format — read directly size_t readSz = fread(out, sizeof(BrushPreset), count, f); loaded = (int)readSz; + } else if (entrySize == sizeof(PresetV4)) { + // V4 format — read, collapse separate focalOffset into bp[19] + for (int i = 0; i < count && i < maxCount; i++) { + PresetV4 old; + if (fread(&old, sizeof(PresetV4), 1, f) != 1) break; + BrushPreset* p = &out[loaded]; + memcpy(p->name, old.name, sizeof(p->name)); + p->mode = old.mode; + p->eraseMode = old.eraseMode; + for (int j = 0; j < 19; j++) { + p->bp[j].val = old.bp[j].val; + p->bp[j].clipmin = old.bp[j].clipmin; + p->bp[j].jitter = old.bp[j].jitter; + p->bp[j].penMode = old.bp[j].penMode; + // (already seeded from live, overwrite val only) + } + // Focal offset pre-V5 stored as separate fields — put into bp[19] + p->bp[19].val = old.focalOffsetVal; + p->bp[19].penMode = old.focalOffsetPenMode; + p->texBlendMode = old.texBlendMode; + p->texNoisemode = old.texNoisemode; + p->texColorMode = old.texColorMode; + p->useTexLumAsAlpha = old.useTexLumAsAlpha; + p->texUseRGB = old.texUseRGB; + memcpy(p->texName, old.texName, sizeof(p->texName)); + p->bmidx = old.bmidx; + p->preserveop = old.preserveop; + p->seamlessPaint = old.seamlessPaint; + p->strokeSmoothingMode = old.strokeSmoothingMode; + p->strokeThrottle = old.strokeThrottle; + if (_presetIsValid(p)) + loaded++; + } + } else if (entrySize == sizeof(PresetV3)) { + // V3 format — old bp[19]{float,int}, focalOffset as separate fields + for (int i = 0; i < count && i < maxCount; i++) { + PresetV3 old; + if (fread(&old, sizeof(PresetV3), 1, f) != 1) break; + BrushPreset* p = &out[loaded]; + memcpy(p->name, old.name, sizeof(p->name)); + p->mode = old.mode; + p->eraseMode = old.eraseMode; + for (int j = 0; j < 19; j++) { + p->bp[j].val = old.bp[j].val; + // clipmin/jitter/penMode kept from seed + } + p->bp[19].val = old.focalOffsetVal; + p->bp[19].penMode = old.focalOffsetPenMode; + p->texBlendMode = old.texBlendMode; + p->texNoisemode = old.texNoisemode; + p->texColorMode = old.texColorMode; + p->useTexLumAsAlpha = old.useTexLumAsAlpha; + p->texUseRGB = old.texUseRGB; + memcpy(p->texName, old.texName, sizeof(p->texName)); + p->bmidx = old.bmidx; + p->preserveop = old.preserveop; + p->seamlessPaint = old.seamlessPaint; + p->strokeSmoothingMode = old.strokeSmoothingMode; + p->strokeThrottle = old.strokeThrottle; + if (_presetIsValid(p)) + loaded++; + } + } else if (entrySize == sizeof(PresetV2)) { + for (int i = 0; i < count && i < maxCount; i++) { + PresetV2 old; + if (fread(&old, sizeof(PresetV2), 1, f) != 1) break; + BrushPreset* p = &out[loaded]; + memcpy(p->name, old.name, sizeof(p->name)); + p->mode = old.mode; + p->eraseMode = old.eraseMode; + for (int j = 0; j < 19; j++) { + p->bp[j].val = old.bp[j].val; + } + p->texBlendMode = old.texBlendMode; + p->texNoisemode = old.texNoisemode; + p->texColorMode = old.texColorMode; + p->useTexLumAsAlpha = old.useTexLumAsAlpha; + p->texUseRGB = old.texUseRGB; + memcpy(p->texName, old.texName, sizeof(p->texName)); + p->bmidx = old.bmidx; + p->preserveop = old.preserveop; + p->seamlessPaint = old.seamlessPaint; + p->strokeSmoothingMode = old.strokeSmoothingMode; + p->strokeThrottle = old.strokeThrottle; + // focalOffset (bp[19]) kept from seed + if (_presetIsValid(p)) + loaded++; + } } else if (entrySize == sizeof(PresetV1)) { - // Version 1 format — read old struct and convert for (int i = 0; i < count && i < maxCount; i++) { PresetV1 old; if (fread(&old, sizeof(PresetV1), 1, f) != 1) break; @@ -202,7 +354,9 @@ static int _loadFile(const char* path, BrushPreset* out, int maxCount) { memcpy(p->name, old.name, sizeof(p->name)); p->mode = old.mode; p->eraseMode = old.eraseMode; - memcpy(p->bp, old.bp, sizeof(p->bp)); + for (int j = 0; j < 19; j++) { + p->bp[j].val = old.bp[j].val; + } p->texBlendMode = old.texBlendMode; p->texNoisemode = old.texNoisemode; p->texColorMode = old.texColorMode; @@ -213,12 +367,11 @@ static int _loadFile(const char* path, BrushPreset* out, int maxCount) { p->preserveop = old.preserveop; p->seamlessPaint = old.seamlessPaint; p->strokeSmoothingMode = old.strokeSmoothingMode; - p->strokeThrottle = old.splineMinDist; // map old min-dist to throttle + p->strokeThrottle = old.splineMinDist; if (_presetIsValid(p)) loaded++; } } else { - // Unknown format — skip (file might be corrupted) fclose(f); return 0; } @@ -238,7 +391,7 @@ int Preset_LoadUser(BrushPreset* out, int maxCount) { char path[1024]; snprintf(path, sizeof(path), "%sSaves/user_presets.dat", ad); int n = _loadFile(path, out, maxCount); - if (n == 0) return Preset_LoadDefault(out, maxCount); // fall back to defaults + if (n == 0) return Preset_LoadDefault(out, maxCount); return n; } @@ -251,7 +404,6 @@ void Preset_ApplyDefault(AppState* state) { return; } } - // Not found in user — try bundled n = Preset_LoadDefault(buf, 256); for (int i = 0; i < n; i++) { if (strcasecmp(buf[i].name, "default") == 0) { diff --git a/src/drawing/stroke_engine.cpp b/src/drawing/stroke_engine.cpp index 3800a3a..ce29ca3 100644 --- a/src/drawing/stroke_engine.cpp +++ b/src/drawing/stroke_engine.cpp @@ -18,10 +18,10 @@ static float ConfigRawVal(const BPConfig& cfg) { static float ModulateConfigVal(const BPConfig& cfg, const float modValues[csSTOP]) { float cpar = (cfg.modulatorId >= 0 && cfg.modulatorId < csSTOP) ? modValues[cfg.modulatorId] : 1.0f; - float n = (cfg.power != 1.0f) - ? powf(cfg.userMax, cfg.power) : cfg.userMax; - float rng = cfg.userMax - cfg.userMin; - float respar = cpar * rng + cfg.userMin; + float minN = powf(cfg.userMin, cfg.power); + float maxN = powf(cfg.userMax, cfg.power); + float rng = maxN - minN; + float respar = cpar * rng + minN; float randm = (((float)rand() / (float)RAND_MAX) - 0.5f) * 2.0f * cfg.jitter; float res = fminf(fmaxf(respar + randm, 0.0f), 1.0f); return res * (cfg.outMax - cfg.outMin) + cfg.outMin; diff --git a/src/drawing/viewport_hud.cpp b/src/drawing/viewport_hud.cpp index c22af48..11cf2d3 100644 --- a/src/drawing/viewport_hud.cpp +++ b/src/drawing/viewport_hud.cpp @@ -6,6 +6,7 @@ #include "external/glad.h" #include "stroke_engine.h" #include "brush_blend.h" +#include "../LocalPlayer/StrokeThrottle.h" #include #define PREVIEW_SZ 512 @@ -279,13 +280,12 @@ void ViewportHUD_Draw(AppState* state) { if (g_lastPreviewHash == 0) g_previewBaseSeed = zoomBrush.seed; zoomBrush.seed = g_previewBaseSeed; - // Build preview Quad: zoom-dependent screen corners so the world region - // stays at PREVIEW_SZ units regardless of zoom. This matches the canvas - // density — the brush dabs at canvas-pixel size (WORLD_UNIT_PX) line up - // 1:1 with the canvas background in the preview. - float pScrSz = (float)PREVIEW_SZ * state->camera.zoom; + // Build preview Quad: use full viewport height as the square side. + // The xform maps this screen area to world-space via the camera, + // giving a consistent canvas-pixel density at any zoom. + float pScrSz = vpBounds.height; float pX = vpBounds.x + vpBounds.width * 0.5f - pScrSz * 0.5f; - float pY = vpBounds.y + vpBounds.height * 0.5f - pScrSz * 0.5f; + float pY = vpBounds.y; Quad previewQuad; previewQuad.xform = GetXformFromScreenCorners(pX, pY, pX + pScrSz, pY + pScrSz, state->camera); previewQuad.rt = g_previewRT; @@ -328,7 +328,9 @@ void ViewportHUD_Draw(AppState* state) { // canvas→screen scale is zoom. Keep the dabs at canvas-pixel size so // they match the brush mark already in the composite / canvas texture. g_previewRendered = 0; - g_previewBudget = 50000; + // Seed preview budget from the stroke throttle's dynamic budget, + // so the preview starts at the same performance-adapted level. + if (g_throttle) g_previewBudget = g_throttle->GetBudget(); g_lastPreviewHash = currentHash; } @@ -368,24 +370,21 @@ void ViewportHUD_Draw(AppState* state) { EndTextureMode(); // Adapt budget - if (!renderAll) { + if (!renderAll) { // TODO: Could rely on dynamic budget double elapsed = GetTime() - tStart; if (elapsed > 0.008) g_previewBudget = (int)(g_previewBudget * 0.85f); else if (elapsed < 0.002) g_previewBudget = (int)(g_previewBudget * 1.15f); if (g_previewBudget < 10000) g_previewBudget = 10000; - if (g_previewBudget > 500000) g_previewBudget = 500000; + if (g_previewBudget > 8000000) g_previewBudget = 8000000; } } - // Display the preview quad — scale by zoom so brush size matches viewport canvas. - // The zoom cancels GetXformFromScreenCorners' zoom scaling, giving the preview - // the same world→screen pixel ratio as the canvas viewport. - float hh = (float)PREVIEW_SZ * state->camera.zoom * 0.5f; - float px = vpBounds.x + vpBounds.width * 0.5f - hh; - float py = vpBounds.y + vpBounds.height * 0.5f - hh; - float dispSz = (float)PREVIEW_SZ * state->camera.zoom; + // Display the preview — fills viewport height, centered horizontally. + float dispSz = vpBounds.height; + float px = vpBounds.x + vpBounds.width * 0.5f - dispSz * 0.5f; + float py = vpBounds.y; DrawTexturePro(g_previewRT.texture, Rectangle{0, 0, (float)PREVIEW_SZ, (float)-PREVIEW_SZ}, Rectangle{px, py, dispSz, dispSz}, diff --git a/src/ui/ui_layerpanel.cpp b/src/ui/ui_layerpanel.cpp index 7951efc..b5fa1b4 100644 --- a/src/ui/ui_layerpanel.cpp +++ b/src/ui/ui_layerpanel.cpp @@ -134,10 +134,11 @@ void LayerPanel_Draw(AppState* state) { { int blend = LayerStack_GetProps(state->activeLayer)->blendmode; if (blend < 0 || blend >= g_blendModeCount) blend = 0; - if (g_blendIconLoaded) - ImGui::Image((ImTextureID)(intptr_t)g_blendModeIcon.id, ImVec2(24, 24)); - else - ImGui::Dummy(ImVec2(24, 24)); + if (g_blendIconLoaded) { + float fh = ImGui::GetFrameHeight(); + ImGui::Image((ImTextureID)(intptr_t)g_blendModeIcon.id, ImVec2(fh, fh)); + } else + ImGui::Dummy(ImVec2(ImGui::GetFrameHeight(), ImGui::GetFrameHeight())); ImGui::SameLine(); ImGui::SetNextItemWidth(-1); if (ImGui::Combo("##blend", &blend, g_blendModeNames, g_blendModeCount, g_blendModeCount)) { diff --git a/src/ui/ui_quickpanel.cpp b/src/ui/ui_quickpanel.cpp index 744de28..84bbbfc 100644 --- a/src/ui/ui_quickpanel.cpp +++ b/src/ui/ui_quickpanel.cpp @@ -150,7 +150,7 @@ void QuickPanel_DrawUI(AppState* state) { static bool s_showTex = true; int texBtnX = sliderRightX + 3 * (dCtrl + dGap); ImGui::SetCursorScreenPos(ImVec2((float)texBtnX, (float)iconY)); - if (ImGui::Button(s_showTex ? "Brush Textures" : "Brush Textures", ImVec2((float)(dCtrl * 2 + dGap *2), (float)dCtrl))) { + if (ImGui::Button("Brush Textures", ImVec2(0, (float)dCtrl))) { s_showTex = !s_showTex; } if (ImGui::IsItemHovered()) diff --git a/src/ui/ui_sliders.cpp b/src/ui/ui_sliders.cpp index b829f85..dab362b 100644 --- a/src/ui/ui_sliders.cpp +++ b/src/ui/ui_sliders.cpp @@ -261,8 +261,6 @@ static void SliderInteraction(const ImRect& bb, int orient, BParam* bp) { if (flip) snappv = 1.0f - snappv; // Snap: compute linear value from mouse, snap, solve clipmaxF = (snapped/range)^(1/power) v = powf(snappv, 1.0f / bp->power); - float ret = powf(v,bp->power); - } else { // precise side: expand section downward float dist = mperp - bbMaxP;