Rework layout engine around exact-glyph collision, add tests and docs sync
Replace the old bbox/heuristic placement (scale search rounds, large-font capping, stratified sampling, fill-retry ladders) with an area-model font sizing pass feeding a C++ exact-glyph collision engine (centroid-biased spiral + random probing, HD clearance refinement, density/hole optimization). Simplify the frontend advanced-params panel and JobParams type to match the surviving config surface, add a layout-constraints test suite and a repeatable benchmark tool, and bring docs/*.md back in sync with current code (plus new TESTING.md and DEPLOYMENT.md). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,8 +22,9 @@
|
||||
#include <future>
|
||||
#include <atomic>
|
||||
#include <random>
|
||||
#include <functional>
|
||||
#include <numeric>
|
||||
#include <functional>
|
||||
#include <numeric>
|
||||
#include <limits>
|
||||
|
||||
// ==========================================
|
||||
// Thread Pool (avoid per-query thread creation)
|
||||
@@ -114,8 +115,13 @@ public:
|
||||
int w;
|
||||
};
|
||||
|
||||
// valid coordinates sorted by distance to center (for sorted/spiral search)
|
||||
std::vector<std::pair<int, int>> valid_coords;
|
||||
// Original mask-free coordinates. Sorting is lazy because the active
|
||||
// query_direct/query_near_center paths do not need the O(A log A) order.
|
||||
std::vector<std::pair<int, int>> valid_coords;
|
||||
bool valid_coords_center_sorted = false;
|
||||
double free_center_y = 0.0;
|
||||
double free_center_x = 0.0;
|
||||
int spiral_cursor = 1;
|
||||
|
||||
// Lazy update buffers
|
||||
std::vector<int32_t> diff;
|
||||
@@ -133,9 +139,9 @@ public:
|
||||
}
|
||||
|
||||
void init_from_buffer(unsigned char* raw_mask, int h, int w) {
|
||||
int center_y = h / 2;
|
||||
int center_x = w / 2;
|
||||
valid_coords.reserve(h * w / 2);
|
||||
valid_coords.reserve(h * w / 2);
|
||||
uint64_t free_y_sum = 0;
|
||||
uint64_t free_x_sum = 0;
|
||||
|
||||
// Initialize canvas from mask
|
||||
ensure_canvas();
|
||||
@@ -151,28 +157,48 @@ public:
|
||||
if (is_blocked) {
|
||||
canvas[i * width + j] = 1;
|
||||
}
|
||||
if (!is_blocked) {
|
||||
valid_coords.push_back({i, j});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(valid_coords.begin(), valid_coords.end(),
|
||||
[center_y, center_x](const std::pair<int, int>& a, const std::pair<int, int>& b) {
|
||||
long da = (long)(a.first - center_y)*(a.first - center_y) + (long)(a.second - center_x)*(a.second - center_x);
|
||||
long db = (long)(b.first - center_y)*(b.first - center_y) + (long)(b.second - center_x)*(b.second - center_x);
|
||||
return da < db;
|
||||
}
|
||||
);
|
||||
if (!is_blocked) {
|
||||
valid_coords.push_back({i, j});
|
||||
free_y_sum += (uint64_t)i;
|
||||
free_x_sum += (uint64_t)j;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!valid_coords.empty()) {
|
||||
free_center_y = (double)free_y_sum / (double)valid_coords.size();
|
||||
free_center_x = (double)free_x_sum / (double)valid_coords.size();
|
||||
} else {
|
||||
free_center_y = (double)h * 0.5;
|
||||
free_center_x = (double)w * 0.5;
|
||||
}
|
||||
valid_coords_center_sorted = false;
|
||||
spiral_cursor = 1;
|
||||
|
||||
std::fill(diff.begin(), diff.end(), 0);
|
||||
recent_rects.clear();
|
||||
dirty_count = 0;
|
||||
}
|
||||
|
||||
void reorder_stratified(int bands) {
|
||||
std::unique_lock<std::shared_mutex> lock(mutex_);
|
||||
const size_t len = valid_coords.size();
|
||||
dirty_count = 0;
|
||||
}
|
||||
|
||||
void ensure_valid_coords_center_sorted() {
|
||||
if (valid_coords_center_sorted) return;
|
||||
const double center_y = free_center_y;
|
||||
const double center_x = free_center_x;
|
||||
std::sort(valid_coords.begin(), valid_coords.end(),
|
||||
[center_y, center_x](const std::pair<int, int>& a, const std::pair<int, int>& b) {
|
||||
double ay = (double)a.first - center_y;
|
||||
double ax = (double)a.second - center_x;
|
||||
double by = (double)b.first - center_y;
|
||||
double bx = (double)b.second - center_x;
|
||||
return ay * ay + ax * ax < by * by + bx * bx;
|
||||
}
|
||||
);
|
||||
valid_coords_center_sorted = true;
|
||||
}
|
||||
// Legacy coordinate ordering remains available to compatibility callers.
|
||||
void reorder_stratified(int bands) {
|
||||
std::unique_lock<std::shared_mutex> lock(mutex_);
|
||||
ensure_valid_coords_center_sorted();
|
||||
const size_t len = valid_coords.size();
|
||||
if (bands <= 1 || len == 0) return;
|
||||
if ((size_t)bands > len) bands = (int)len;
|
||||
|
||||
@@ -279,8 +305,7 @@ public:
|
||||
std::fill(diff.begin(), diff.end(), 0);
|
||||
recent_rects.clear();
|
||||
dirty_count = 0;
|
||||
|
||||
for (int i = 0; i < height; ++i) {
|
||||
for (int i = 0; i < height; ++i) {
|
||||
uint32_t row_sum = 0;
|
||||
for (int j = 0; j < width; ++j) {
|
||||
uint32_t val = (raw_pixels[i * width + j] > 0) ? 1 : 0;
|
||||
@@ -318,10 +343,10 @@ public:
|
||||
}
|
||||
|
||||
// Rebuild integral from the internal canvas (partial from pos_r, pos_c)
|
||||
void rebuild_from_canvas(int pos_r, int pos_c) {
|
||||
if (canvas.empty()) return;
|
||||
rebuild_from_bitmap_partial(canvas.data(), pos_r, pos_c);
|
||||
}
|
||||
void rebuild_from_canvas(int pos_r, int pos_c) {
|
||||
if (canvas.empty()) return;
|
||||
rebuild_from_bitmap_partial(canvas.data(), pos_r, pos_c);
|
||||
}
|
||||
|
||||
// v4: Partial integral rebuild from position (pos_r, pos_c) downward
|
||||
// Optimized: use row-sum approach (like rebuild_from_bitmap) for the partial region
|
||||
@@ -425,8 +450,9 @@ public:
|
||||
return {-1, -1};
|
||||
}
|
||||
|
||||
DirectResult query_direct(int box_h, int box_w, uint32_t seed) {
|
||||
// Always flush before scanning
|
||||
DirectResult query_direct(int box_h, int box_w, uint32_t seed) {
|
||||
// Apply pending rectangle updates before reading the integral image.
|
||||
// Exact-glyph placement uses its own canvas-only path.
|
||||
flush();
|
||||
|
||||
int max_row = height - box_h;
|
||||
@@ -447,11 +473,12 @@ public:
|
||||
return {true, y, x};
|
||||
}
|
||||
|
||||
// Quick random probe: try a few random positions first
|
||||
// If the canvas is mostly empty, one of these will hit quickly
|
||||
// This avoids the full O(H*W) scan for early words
|
||||
{
|
||||
int n_probes = std::min(16, (int)total_positions);
|
||||
// Random probes avoid an O(H*W) scan during early and middle packing.
|
||||
// Increase the budget as occupancy rises.
|
||||
{
|
||||
const double occ_ratio = (double)total_occupied / (double)(width * height);
|
||||
const int probe_budget = (occ_ratio < 0.15) ? 32 : (occ_ratio < 0.40) ? 96 : 192;
|
||||
const int n_probes = (int)std::min<int64_t>(probe_budget, total_positions);
|
||||
for (int p = 0; p < n_probes; ++p) {
|
||||
int y = std::uniform_int_distribution<int>(0, max_row)(rng);
|
||||
int x = std::uniform_int_distribution<int>(0, max_col)(rng);
|
||||
@@ -469,27 +496,28 @@ public:
|
||||
}
|
||||
|
||||
if (nt <= 1) {
|
||||
// Single-thread: two-pass (count then pick) is faster than
|
||||
// reservoir sampling because it avoids per-position RNG calls
|
||||
uint64_t total_valid = 0;
|
||||
for (int i = 0; i <= max_row; ++i) {
|
||||
for (int j = 0; j <= max_col; ++j) {
|
||||
if (get_area_sum_fast(i, j, box_h, box_w) == 0)
|
||||
++total_valid;
|
||||
}
|
||||
}
|
||||
if (total_valid == 0) return {false, -1, -1};
|
||||
uint64_t target = std::uniform_int_distribution<uint64_t>(0, total_valid - 1)(rng);
|
||||
uint64_t count = 0;
|
||||
// Single-pass reservoir halves the work of count-then-pick. A
|
||||
// small LCG avoids invoking mt19937 for every valid position.
|
||||
uint64_t count = 0;
|
||||
int best_y = -1, best_x = -1;
|
||||
uint32_t state = seed ? seed : 1u;
|
||||
for (int i = 0; i <= max_row; ++i) {
|
||||
for (int j = 0; j <= max_col; ++j) {
|
||||
if (get_area_sum_fast(i, j, box_h, box_w) == 0) {
|
||||
if (count == target) return {true, i, j};
|
||||
++count;
|
||||
// Replace the current result with probability 1/count.
|
||||
state = state * 1664525u + 1013904223u;
|
||||
const bool replace = (state % count) == 0;
|
||||
if (replace) {
|
||||
best_y = i;
|
||||
best_x = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {false, -1, -1};
|
||||
return count == 0
|
||||
? DirectResult{false, -1, -1}
|
||||
: DirectResult{true, best_y, best_x};
|
||||
}
|
||||
|
||||
// Multi-thread path: parallel count then pick
|
||||
@@ -531,10 +559,193 @@ public:
|
||||
}
|
||||
cum += chunk_counts[t];
|
||||
}
|
||||
return {false, -1, -1};
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
return {false, -1, -1};
|
||||
}
|
||||
|
||||
// Sample legal positions and prefer the one whose box center is closest
|
||||
// to the free-mask centroid. This gives the visually important large
|
||||
// words a stable focal region without paying for a full spiral scan.
|
||||
DirectResult query_near_center(int box_h, int box_w, uint32_t seed, int probes) {
|
||||
flush();
|
||||
|
||||
int max_row = height - box_h;
|
||||
int max_col = width - box_w;
|
||||
if (max_row < 0 || max_col < 0) return {false, -1, -1};
|
||||
|
||||
std::mt19937 rng(seed);
|
||||
probes = std::max(16, std::min(probes, 1024));
|
||||
|
||||
int best_y = -1;
|
||||
int best_x = -1;
|
||||
double best_score = std::numeric_limits<double>::infinity();
|
||||
std::uniform_int_distribution<int> row_dist(0, max_row);
|
||||
std::uniform_int_distribution<int> col_dist(0, max_col);
|
||||
std::uniform_real_distribution<double> jitter(0.0, 1e-4);
|
||||
|
||||
// Test the centroid-aligned position first.
|
||||
int center_y = std::max(0, std::min(max_row, (int)std::lround(free_center_y - box_h * 0.5)));
|
||||
int center_x = std::max(0, std::min(max_col, (int)std::lround(free_center_x - box_w * 0.5)));
|
||||
if (get_area_sum_fast(center_y, center_x, box_h, box_w) == 0) {
|
||||
return {true, center_y, center_x};
|
||||
}
|
||||
|
||||
const double norm_y = std::max(1.0, (double)height);
|
||||
const double norm_x = std::max(1.0, (double)width);
|
||||
for (int p = 0; p < probes; ++p) {
|
||||
int y = row_dist(rng);
|
||||
int x = col_dist(rng);
|
||||
if (get_area_sum_fast(y, x, box_h, box_w) != 0) continue;
|
||||
double cy = (double)y + box_h * 0.5;
|
||||
double cx = (double)x + box_w * 0.5;
|
||||
double dy = (cy - free_center_y) / norm_y;
|
||||
double dx = (cx - free_center_x) / norm_x;
|
||||
double score = dy * dy + dx * dx + jitter(rng);
|
||||
if (score < best_score) {
|
||||
best_score = score;
|
||||
best_y = y;
|
||||
best_x = x;
|
||||
}
|
||||
}
|
||||
|
||||
if (best_y >= 0) return {true, best_y, best_x};
|
||||
return query_direct(box_h, box_w, seed ^ 0x9E3779B9u);
|
||||
}
|
||||
|
||||
inline bool glyph_fits_exact(
|
||||
const unsigned char* glyph, int glyph_h, int glyph_w, int y, int x
|
||||
) const {
|
||||
for (int row = 0; row < glyph_h; ++row) {
|
||||
const unsigned char* glyph_row = glyph + row * glyph_w;
|
||||
const uint8_t* canvas_row = canvas.data() + (y + row) * width + x;
|
||||
for (int col = 0; col < glyph_w; ++col) {
|
||||
if (glyph_row[col] > 0 && canvas_row[col] != 0) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void reserve_glyph_exact(
|
||||
const unsigned char* glyph, int glyph_h, int glyph_w, int y, int x
|
||||
) {
|
||||
stamp_glyph(glyph, glyph_h, glyph_w, y, x);
|
||||
}
|
||||
|
||||
// Find and reserve a position using the actual (optionally dilated) glyph
|
||||
// bitmap. Unlike rectangle queries, transparent corners and gaps between
|
||||
// strokes may overlap safely. Search and reservation stay in one C++ call,
|
||||
// so no integral-image rebuild is needed between words.
|
||||
DirectResult place_glyph_exact(
|
||||
const unsigned char* collision_glyph,
|
||||
const unsigned char* stamp_glyph_data,
|
||||
int glyph_h,
|
||||
int glyph_w,
|
||||
uint32_t seed,
|
||||
int probes,
|
||||
int placement_mode
|
||||
) {
|
||||
ensure_canvas();
|
||||
const int max_row = height - glyph_h;
|
||||
const int max_col = width - glyph_w;
|
||||
if (max_row < 0 || max_col < 0) return {false, -1, -1};
|
||||
|
||||
std::mt19937 rng(seed);
|
||||
probes = std::max(32, std::min(probes, 2048));
|
||||
std::uniform_int_distribution<int> row_dist(0, max_row);
|
||||
std::uniform_int_distribution<int> col_dist(0, max_col);
|
||||
|
||||
int best_y = -1;
|
||||
int best_x = -1;
|
||||
double best_score = std::numeric_limits<double>::infinity();
|
||||
|
||||
if (placement_mode != 0) {
|
||||
const int center_y = std::max(
|
||||
0, std::min(max_row, (int)std::lround(free_center_y - glyph_h * 0.5))
|
||||
);
|
||||
const int center_x = std::max(
|
||||
0, std::min(max_col, (int)std::lround(free_center_x - glyph_w * 0.5))
|
||||
);
|
||||
if (glyph_fits_exact(collision_glyph, glyph_h, glyph_w, center_y, center_x)) {
|
||||
reserve_glyph_exact(stamp_glyph_data, glyph_h, glyph_w, center_y, center_x);
|
||||
return {true, center_y, center_x};
|
||||
}
|
||||
}
|
||||
|
||||
const double norm_y = std::max(1.0, (double)height);
|
||||
const double norm_x = std::max(1.0, (double)width);
|
||||
|
||||
// Mode 1 is a persistent centre-out Fermat spiral. Mode 2 skips the
|
||||
// spiral and selects a random legal candidate biased toward the centre.
|
||||
if (placement_mode == 1) {
|
||||
constexpr double golden_angle = 2.39996322972865332;
|
||||
constexpr double sample_spacing = 1.25;
|
||||
const int start_step = spiral_cursor;
|
||||
const int end_step = std::min(200000, start_step + 60000);
|
||||
int last_y = std::numeric_limits<int>::min();
|
||||
int last_x = std::numeric_limits<int>::min();
|
||||
for (int step = start_step; step <= end_step; ++step) {
|
||||
const double radius = sample_spacing * std::sqrt((double)step);
|
||||
const double theta = golden_angle * (double)step;
|
||||
const int y = (int)std::lround(
|
||||
free_center_y + radius * std::sin(theta) - glyph_h * 0.5
|
||||
);
|
||||
const int x = (int)std::lround(
|
||||
free_center_x + radius * std::cos(theta) - glyph_w * 0.5
|
||||
);
|
||||
if (y == last_y && x == last_x) continue;
|
||||
last_y = y;
|
||||
last_x = x;
|
||||
if (y < 0 || x < 0 || y > max_row || x > max_col) continue;
|
||||
if (!glyph_fits_exact(collision_glyph, glyph_h, glyph_w, y, x)) continue;
|
||||
reserve_glyph_exact(stamp_glyph_data, glyph_h, glyph_w, y, x);
|
||||
spiral_cursor = step;
|
||||
return {true, y, x};
|
||||
}
|
||||
}
|
||||
|
||||
for (int probe = 0; probe < probes; ++probe) {
|
||||
const int y = row_dist(rng);
|
||||
const int x = col_dist(rng);
|
||||
if (!glyph_fits_exact(collision_glyph, glyph_h, glyph_w, y, x)) continue;
|
||||
if (placement_mode == 0) {
|
||||
stamp_glyph(stamp_glyph_data, glyph_h, glyph_w, y, x);
|
||||
return {true, y, x};
|
||||
}
|
||||
const double cy = (double)y + glyph_h * 0.5;
|
||||
const double cx = (double)x + glyph_w * 0.5;
|
||||
const double dy = (cy - free_center_y) / norm_y;
|
||||
const double dx = (cx - free_center_x) / norm_x;
|
||||
const double score = dy * dy + dx * dx;
|
||||
if (score < best_score) {
|
||||
best_score = score;
|
||||
best_y = y;
|
||||
best_x = x;
|
||||
}
|
||||
}
|
||||
if (best_y >= 0) {
|
||||
reserve_glyph_exact(stamp_glyph_data, glyph_h, glyph_w, best_y, best_x);
|
||||
return {true, best_y, best_x};
|
||||
}
|
||||
|
||||
// Dense late-stage fallback. Start at a seeded offset to avoid a
|
||||
// top-left bias, but visit every possible origin so completeness is
|
||||
// deterministic whenever a legal position exists.
|
||||
const int n_rows = max_row + 1;
|
||||
const int n_cols = max_col + 1;
|
||||
const int row_start = row_dist(rng);
|
||||
const int col_start = col_dist(rng);
|
||||
for (int row_offset = 0; row_offset < n_rows; ++row_offset) {
|
||||
const int y = (row_start + row_offset) % n_rows;
|
||||
for (int col_offset = 0; col_offset < n_cols; ++col_offset) {
|
||||
const int x = (col_start + col_offset) % n_cols;
|
||||
if (!glyph_fits_exact(collision_glyph, glyph_h, glyph_w, y, x)) continue;
|
||||
reserve_glyph_exact(stamp_glyph_data, glyph_h, glyph_w, y, x);
|
||||
return {true, y, x};
|
||||
}
|
||||
}
|
||||
return {false, -1, -1};
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// v3: Batch query — process multiple (box_h, box_w) in one call
|
||||
// Returns vector of {found, y, x} for each query
|
||||
// =========================================================
|
||||
@@ -575,9 +786,13 @@ public:
|
||||
return {false, -1, -1, end_idx};
|
||||
}
|
||||
|
||||
std::pair<bool, std::pair<int, int>> find_spot_parallel(int box_h, int box_w, int step) {
|
||||
{
|
||||
std::shared_lock<std::shared_mutex> lock(mutex_);
|
||||
std::pair<bool, std::pair<int, int>> find_spot_parallel(int box_h, int box_w, int step) {
|
||||
{
|
||||
std::unique_lock<std::shared_mutex> lock(mutex_);
|
||||
ensure_valid_coords_center_sorted();
|
||||
}
|
||||
{
|
||||
std::shared_lock<std::shared_mutex> lock(mutex_);
|
||||
if (dirty_count > 0 && dirty_count >= rebuild_interval) {
|
||||
lock.unlock();
|
||||
std::unique_lock<std::shared_mutex> write_lock(mutex_);
|
||||
@@ -679,19 +894,67 @@ static PyObject* Grid_query_reservoir(PyIntegralGrid* self, PyObject* args) {
|
||||
}
|
||||
|
||||
// v3: Direct pixel-grid scan with parallel counting
|
||||
static PyObject* Grid_query_direct(PyIntegralGrid* self, PyObject* args) {
|
||||
static PyObject* Grid_query_direct(PyIntegralGrid* self, PyObject* args) {
|
||||
int box_h, box_w;
|
||||
unsigned int seed = 0;
|
||||
if (!PyArg_ParseTuple(args, "ii|I", &box_h, &box_w, &seed)) return NULL;
|
||||
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
// query_direct is GIL-free safe (no Python objects touched)
|
||||
Py_END_ALLOW_THREADS
|
||||
|
||||
auto r = self->grid->query_direct(box_h, box_w, seed);
|
||||
auto r = self->grid->query_direct(box_h, box_w, seed);
|
||||
if (r.found) return Py_BuildValue("ii", r.y, r.x);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject* Grid_query_near_center(PyIntegralGrid* self, PyObject* args) {
|
||||
int box_h, box_w;
|
||||
unsigned int seed = 0;
|
||||
int probes = 160;
|
||||
if (!PyArg_ParseTuple(args, "ii|Ii", &box_h, &box_w, &seed, &probes)) return NULL;
|
||||
|
||||
auto r = self->grid->query_near_center(box_h, box_w, seed, probes);
|
||||
if (r.found) return Py_BuildValue("ii", r.y, r.x);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject* Grid_place_glyph_exact(PyIntegralGrid* self, PyObject* args) {
|
||||
PyObject* collision_obj;
|
||||
PyObject* stamp_obj;
|
||||
int glyph_h, glyph_w;
|
||||
unsigned int seed = 0;
|
||||
int probes = 256;
|
||||
int placement_mode = 0;
|
||||
if (!PyArg_ParseTuple(
|
||||
args, "OOiiIii", &collision_obj, &stamp_obj, &glyph_h, &glyph_w, &seed, &probes, &placement_mode
|
||||
)) return NULL;
|
||||
|
||||
Py_buffer collision_view;
|
||||
Py_buffer stamp_view;
|
||||
if (PyObject_GetBuffer(collision_obj, &collision_view, PyBUF_SIMPLE) < 0) return NULL;
|
||||
if (PyObject_GetBuffer(stamp_obj, &stamp_view, PyBUF_SIMPLE) < 0) {
|
||||
PyBuffer_Release(&collision_view);
|
||||
return NULL;
|
||||
}
|
||||
const Py_ssize_t required = (Py_ssize_t)glyph_h * (Py_ssize_t)glyph_w;
|
||||
if (glyph_h <= 0 || glyph_w <= 0 || collision_view.len < required || stamp_view.len < required) {
|
||||
PyBuffer_Release(&collision_view);
|
||||
PyBuffer_Release(&stamp_view);
|
||||
PyErr_SetString(PyExc_ValueError, "glyph buffer is smaller than glyph_h * glyph_w");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto result = self->grid->place_glyph_exact(
|
||||
(const unsigned char*)collision_view.buf,
|
||||
(const unsigned char*)stamp_view.buf,
|
||||
glyph_h,
|
||||
glyph_w,
|
||||
seed,
|
||||
probes,
|
||||
placement_mode
|
||||
);
|
||||
PyBuffer_Release(&collision_view);
|
||||
PyBuffer_Release(&stamp_view);
|
||||
if (result.found) return Py_BuildValue("ii", result.y, result.x);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// v3: Batch query — process multiple placements in one C++ call
|
||||
// Input: list of (box_h, box_w, seed) tuples
|
||||
@@ -768,16 +1031,16 @@ static PyObject* Grid_rebuild_from_bitmap_partial(PyIntegralGrid* self, PyObject
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// v4: Stamp glyph bitmap onto C++ canvas and rebuild integral
|
||||
static PyObject* Grid_stamp_and_rebuild(PyIntegralGrid* self, PyObject* args) {
|
||||
// Stamp glyph bitmap onto C++ canvas and rebuild the affected integral region.
|
||||
static PyObject* Grid_stamp_and_rebuild(PyIntegralGrid* self, PyObject* args) {
|
||||
PyObject* glyph_obj;
|
||||
int gh, gw, pos_r, pos_c;
|
||||
if (!PyArg_ParseTuple(args, "Oiiii", &glyph_obj, &gh, &gw, &pos_r, &pos_c)) return NULL;
|
||||
Py_buffer view;
|
||||
if (PyObject_GetBuffer(glyph_obj, &view, PyBUF_SIMPLE) < 0) return NULL;
|
||||
|
||||
self->grid->stamp_glyph((const unsigned char*)view.buf, gh, gw, pos_r, pos_c);
|
||||
self->grid->rebuild_from_canvas(pos_r, pos_c);
|
||||
self->grid->stamp_glyph((const unsigned char*)view.buf, gh, gw, pos_r, pos_c);
|
||||
self->grid->rebuild_from_canvas(pos_r, pos_c);
|
||||
|
||||
PyBuffer_Release(&view);
|
||||
Py_RETURN_NONE;
|
||||
@@ -786,8 +1049,10 @@ static PyObject* Grid_stamp_and_rebuild(PyIntegralGrid* self, PyObject* args) {
|
||||
static PyMethodDef Grid_methods[] = {
|
||||
{"reorder_stratified", (PyCFunction)Grid_reorder_stratified, METH_VARARGS, "Reorder valid coords with stratified interleaving."},
|
||||
{"query_sorted", (PyCFunction)Grid_query_sorted, METH_VARARGS, "Find position using sorted coordinate list (center-out, parallel)."},
|
||||
{"query_reservoir", (PyCFunction)Grid_query_reservoir, METH_VARARGS, "Find position using parallel direct-scan reservoir sampling."},
|
||||
{"query_direct", (PyCFunction)Grid_query_direct, METH_VARARGS, "Direct pixel-grid scan with parallel reservoir sampling."},
|
||||
{"query_reservoir", (PyCFunction)Grid_query_reservoir, METH_VARARGS, "Find position using parallel direct-scan reservoir sampling."},
|
||||
{"query_direct", (PyCFunction)Grid_query_direct, METH_VARARGS, "Direct pixel-grid scan with parallel reservoir sampling."},
|
||||
{"query_near_center", (PyCFunction)Grid_query_near_center, METH_VARARGS, "Sample legal positions and prefer the free-mask centroid."},
|
||||
{"place_glyph_exact", (PyCFunction)Grid_place_glyph_exact, METH_VARARGS, "Place and reserve an exact glyph bitmap without changing its size."},
|
||||
{"batch_query", (PyCFunction)Grid_batch_query, METH_VARARGS, "Batch placement: list of (bh,bw,seed) -> list of (y,x)|None."},
|
||||
{"update", (PyCFunction)Grid_update, METH_VARARGS, "Update grid with placed rectangle."},
|
||||
{"flush", (PyCFunction)Grid_flush, METH_NOARGS, "Force rebuild integral image."},
|
||||
|
||||
Reference in New Issue
Block a user