Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
72a6a7a
modifications to svg extraction
Krasner Jun 4, 2026
aac9407
updated method for hole-free svg
Krasner Jun 8, 2026
7e5ac60
clean up
Krasner Jun 8, 2026
37309f0
When merging contours consider thickness - set a minimum thickness to…
Krasner Jun 8, 2026
b37807e
update python arguments to include min_thickness
Krasner Jun 8, 2026
761fd24
formatting
Krasner Jun 8, 2026
7cbded3
missing bracket
Krasner Jun 8, 2026
51348fc
Merge branch 'dev' into dev/core-opt
Krasner Jun 9, 2026
b4ee348
add emsdk dev ownsership
Krasner Jun 9, 2026
9efab7c
allow dev permissions to uv
Krasner Jun 9, 2026
02cc692
formatting
Krasner Jun 9, 2026
0e973cb
hardcode userid and groupid to be 1000 to match host machine - is thi…
Krasner Jun 10, 2026
8e35991
remove dev user. everything should be root
Krasner Jun 10, 2026
680922e
Merge branch 'dev' into dev/core-opt
Krasner Jun 11, 2026
6da4d6f
Install UV in development Dockerfile
Krasner Jun 11, 2026
7aa21d7
Refactor Dockerfile.dev to match dev branch
Krasner Jun 11, 2026
b3dc95c
Merge branch 'dev' into dev/core-opt
Krasner Jun 12, 2026
0017754
Merge branch 'dev' into dev/core-opt
Krasner Jun 15, 2026
b728615
remove rw
Krasner Jun 15, 2026
8160c24
Apply suggestions from code review
Krasner Jun 15, 2026
1cb1f87
Apply suggestions from code review
Krasner Jun 15, 2026
b9864ce
docs(core): doxygen comment for coupled_smooth_junctions in contours.h
Ryan-Millard Jun 15, 2026
c2bc23b
docs(core): doxygen comment for getPixel and analyzeJunctions in graph.h
Ryan-Millard Jun 15, 2026
df07f98
chore(core): remove console prints in contours.cpp
Ryan-Millard Jun 15, 2026
b3e33ef
docs(core): add doxygen comment to min_thickness of ImageToSvgConfig …
Ryan-Millard Jun 15, 2026
d6f29a1
chore: delete accidentally-committed image
Ryan-Millard Jun 15, 2026
9360efa
clean up old code
Krasner Jun 15, 2026
b3d016e
update readme svgs
Krasner Jun 17, 2026
7dcf8aa
Merge branch 'dev' into dev/core-opt
Krasner Jun 19, 2026
b2a8c27
formatting/ apply PR suggestions
Krasner Jun 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ ENV EMSDK=$EMSDK_DIR
# Auto-load emsdk in all shells
RUN echo "source /opt/emsdk/emsdk_env.sh" >> /etc/bash.bashrc

RUN chown -R dev:dev /opt/emsdk/
Comment thread
Ryan-Millard marked this conversation as resolved.
Outdated
# --------------------------------------------------------------------------------------------------------------
# Dawn dependencies
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
Expand Down
7 changes: 3 additions & 4 deletions bindings/c/include/cimg2num.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ typedef struct img2num_ImageToSvgConfig {

/// Minimum area (in pixels) for a region to be included in the SVG.
int min_cluster_area;
int min_thickness;
Comment thread
Krasner marked this conversation as resolved.

/// Color space flag.
/// - 0 = CIE LAB (more perceptually accurate)
Expand Down Expand Up @@ -80,10 +81,8 @@ void img2num_bilateral_filter(
);

/// @copydoc ::IMG2NUM_H_LABELS_TO_SVG_DOC
char* img2num_labels_to_svg(
const uint8_t* data, const int32_t* labels, const int width, const int height,
const int min_area
);
char *img2num_labels_to_svg(const uint8_t *data, const int32_t *labels, const int width,
const int height, const int min_area, const int min_thickness);

/// @copydoc ::IMG2NUM_H_IMAGE_TO_SVG_DOC
char* img2num_image_to_svg(
Expand Down
19 changes: 9 additions & 10 deletions bindings/c/src/cimg2num.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ static img2num::ImageToSvgConfig to_cpp(const img2num_ImageToSvgConfig& c) {
},

.min_cluster_area = c.min_cluster_area,
.min_thickness = c.min_thickness,
.color_space = c.color_space
};
// clang-format on
Expand All @@ -38,6 +39,7 @@ static img2num_ImageToSvgConfig to_c(const img2num::ImageToSvgConfig& cpp) {
.max_iter = cpp.kmeans.max_iter
},
.min_cluster_area = cpp.min_cluster_area,
.min_thickness = cpp.min_thickness,
.color_space = cpp.color_space
};
// clang-format on
Expand Down Expand Up @@ -90,22 +92,19 @@ void img2num_bilateral_filter(
);
}

char* img2num_labels_to_svg(
const uint8_t* data, const int32_t* labels, const int width, const int height,
const int min_area
) {
char* result {nullptr};
char *img2num_labels_to_svg(const uint8_t *data, const int32_t *labels, const int width,
const int height, const int min_area, const int min_thickness) {
char *result{nullptr};
img2num::clear_last_error_and_catch(
[&](const uint8_t* d, const int32_t* l, const int w, const int h, const int min_a) {
std::string svg {img2num::labels_to_svg(d, l, w, h, min_a)};
result = static_cast<char*>(std::malloc(svg.size() + 1));
[&](const uint8_t *d, const int32_t *l, const int w, const int h, const int min_a, const int min_t) {
std::string svg{img2num::labels_to_svg(d, l, w, h, min_a, min_t)};
result = static_cast<char *>(std::malloc(svg.size() + 1));
if (!result) {
return; // Allocation failed
}
std::memcpy(result, svg.c_str(), svg.size() + 1);
},
data, labels, width, height, min_area
);
data, labels, width, height, min_area, min_thickness);
return result;
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are your thoughts on us removing this function entirely in the future (not now) to make it easier to maintain the library: we only expose one interface for converting to SVGs.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd still keep it... it's not difficult to maintain


Expand Down
17 changes: 8 additions & 9 deletions bindings/js/src/wasm_wrapper.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,23 @@ EMSCRIPTEN_KEEPALIVE void bilateral_filter(
img2num_bilateral_filter(image, width, height, sigma_spatial, sigma_range, color_space);
}

EMSCRIPTEN_KEEPALIVE char* labels_to_svg(
uint8_t* data, int32_t* labels, const int width, const int height, const int min_area
) {
return img2num_labels_to_svg(data, labels, width, height, min_area);
EMSCRIPTEN_KEEPALIVE char *labels_to_svg(uint8_t *data, int32_t *labels, const int width,
const int height, const int min_area, const int min_thickness) {
return img2num_labels_to_svg(data, labels, width, height, min_area, min_thickness);
}

EMSCRIPTEN_KEEPALIVE char* image_to_svg(
const uint8_t* data, const int width, const int height, double sigma_spatial,
double sigma_range, const int32_t k, const int32_t max_iter, const int min_area,
const uint8_t color_space
) {
EMSCRIPTEN_KEEPALIVE char *image_to_svg(const uint8_t *data, const int width, const int height,
double sigma_spatial, double sigma_range, const int32_t k,
const int32_t max_iter, const int min_area, const int min_thickness,
const uint8_t color_space) {
img2num_ImageToSvgConfig config = img2num_ImageToSvgConfig_default();

config.bilateral_filter.sigma_spatial = sigma_spatial;
config.bilateral_filter.sigma_range = sigma_range;
config.kmeans.k = k;
config.kmeans.max_iter = max_iter;
config.min_cluster_area = min_area;
config.min_thickness = min_thickness;
config.color_space = color_space;

return img2num_image_to_svg(data, width, height, &config);
Expand Down
69 changes: 35 additions & 34 deletions bindings/py/src/img2num_pybind.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,18 +121,17 @@ PYBIND11_MODULE(_img2num, m) {
"labels_to_svg",
[](pybind11::array_t<uint8_t, pybind11::array::c_style> data,
pybind11::array_t<int32_t, pybind11::array::c_style> labels, int width, int height,
int min_area) {
const uint8_t* data_ptr {static_cast<const uint8_t*>(data.request().ptr)};
const int32_t* labels_ptr {static_cast<const int32_t*>(labels.request().ptr)};
int min_area, int min_thickness) {
const uint8_t *data_ptr{static_cast<const uint8_t *>(data.request().ptr)};
const int32_t *labels_ptr{static_cast<const int32_t *>(labels.request().ptr)};

std::string svg {img2num::labels_to_svg(data_ptr, labels_ptr, width, height, min_area)};
std::string svg{img2num::labels_to_svg(data_ptr, labels_ptr, width, height, min_area, min_thickness)};
pybind11::str svg_py_str(std::move(svg));

return svg_py_str;
},
pybind11::arg("data"), pybind11::arg("labels"), pybind11::arg("width"),
pybind11::arg("height"), pybind11::arg("min_area"), "Convert labels to SVG string"
);
pybind11::arg("height"), pybind11::arg("min_area"), pybind11::arg("min_thickness"), "Convert labels to SVG string");

// ---------------------- Config Structs ----------------------
pybind11::class_<img2num::ImageToSvgConfig> config(m, "ImageToSvgConfig");
Expand Down Expand Up @@ -160,36 +159,37 @@ PYBIND11_MODULE(_img2num, m) {
});

config
.def(
pybind11::init([](pybind11::dict bf_dict, pybind11::dict km_dict,
pybind11::kwargs kwargs) {
// hand over ownership to python
std::unique_ptr<img2num::ImageToSvgConfig> c =
std::make_unique<img2num::ImageToSvgConfig>();
if (bf_dict.contains("sigma_spatial"))
c->bilateral_filter.sigma_spatial = bf_dict["sigma_spatial"].cast<double>();
if (bf_dict.contains("sigma_range"))
c->bilateral_filter.sigma_range = bf_dict["sigma_range"].cast<double>();

// 3. Process KMeans overrides from the 'km' dictionary
if (km_dict.contains("k"))
c->kmeans.k = km_dict["k"].cast<int>();
if (km_dict.contains("max_iter"))
c->kmeans.max_iter = km_dict["max_iter"].cast<int>();

// 4. Process remaining top-level kwargs (like color_space or min_cluster_area)
if (kwargs.contains("min_cluster_area"))
c->min_cluster_area = kwargs["min_cluster_area"].cast<int>();
if (kwargs.contains("color_space"))
c->color_space = kwargs["color_space"].cast<uint8_t>();

return c;
}),
pybind11::arg("bilateral_filter") = pybind11::dict(), // Defaults to empty dict
pybind11::arg("kmeans") = pybind11::dict() // Defaults to empty dict
)
.def(pybind11::init([](pybind11::dict bf_dict, pybind11::dict km_dict,
pybind11::kwargs kwargs) {
// hand over ownership to python
std::unique_ptr<img2num::ImageToSvgConfig> c =
std::make_unique<img2num::ImageToSvgConfig>();
if (bf_dict.contains("sigma_spatial"))
c->bilateral_filter.sigma_spatial = bf_dict["sigma_spatial"].cast<double>();
if (bf_dict.contains("sigma_range"))
c->bilateral_filter.sigma_range = bf_dict["sigma_range"].cast<double>();

// 3. Process KMeans overrides from the 'km' dictionary
if (km_dict.contains("k")) c->kmeans.k = km_dict["k"].cast<int>();
if (km_dict.contains("max_iter"))
c->kmeans.max_iter = km_dict["max_iter"].cast<int>();

// 4. Process remaining top-level kwargs (like color_space or min_cluster_area)
if (kwargs.contains("min_cluster_area"))
c->min_cluster_area = kwargs["min_cluster_area"].cast<int>();
if (kwargs.contains("min_thickness"))
c->min_thickness = kwargs["min_thickness"].cast<int>();
if (kwargs.contains("color_space"))
c->color_space = kwargs["color_space"].cast<uint8_t>();

return c;
}),
pybind11::arg("bilateral_filter") = pybind11::dict(), // Defaults to empty dict
pybind11::arg("kmeans") = pybind11::dict() // Defaults to empty dict
)
.def_readwrite("bilateral_filter", &img2num::ImageToSvgConfig::bilateral_filter)
.def_readwrite("min_cluster_area", &img2num::ImageToSvgConfig::min_cluster_area)
.def_readwrite("min_thickness", &img2num::ImageToSvgConfig::min_thickness)
.def_readwrite("color_space", &img2num::ImageToSvgConfig::color_space)
.def_readwrite("kmeans", &img2num::ImageToSvgConfig::kmeans)
.def("__repr__", [](const img2num::ImageToSvgConfig& c) {
Expand All @@ -199,6 +199,7 @@ PYBIND11_MODULE(_img2num, m) {
<< "bilateral_filter: "
<< pybind11::repr(pybind11::cast(c.bilateral_filter)).cast<std::string>() << ", "
<< "min_cluster_area: " << c.min_cluster_area << ", "
<< "min_thickness: " << c.min_thickness << ", "
<< "color_space: " << (int)c.color_space << ", "
<< "kmeans: " << pybind11::repr(pybind11::cast(c.kmeans)).cast<std::string>()
<< "}>";
Expand Down
7 changes: 3 additions & 4 deletions core/include/img2num.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ struct ImageToSvgConfig {

/// Minimum area (in pixels) for a region to be included in the SVG.
int min_cluster_area = 100;
int min_thickness = 0;
Comment thread
Ryan-Millard marked this conversation as resolved.

/// Color space flag.
/// - 0 = CIE LAB (more perceptually accurate)
Expand Down Expand Up @@ -75,10 +76,8 @@ void bilateral_filter(
);

/// @copydoc IMG2NUM_H_LABELS_TO_SVG_DOC
std::string labels_to_svg(
const uint8_t* data, const int32_t* labels, const int width, const int height,
const int min_area
);
std::string labels_to_svg(const uint8_t *data, const int32_t *labels, const int width,
const int height, const int min_area, const int min_thickness);

/// @copydoc IMG2NUM_H_IMAGE_TO_SVG_DOC
std::string image_to_svg(
Expand Down
14 changes: 10 additions & 4 deletions core/include/internal/bezier.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@

#include "internal/contours.h"

void fit_curve_reduction(
const std::vector<std::vector<Point>>& chains, std::vector<std::vector<QuadBezier>>& results,
float tolerance
);
void fit_curve_reduction(const std::vector<std::vector<Point>> &chains,
std::vector<std::vector<QuadBezier>> &results, float tolerance);

// Same, but `fixed[i][k]!=0` marks point k of chain i as a junction that must NOT
// move: the chain is split at those points so each becomes an exact (pinned)
// curve endpoint. Chains with no fixed points fit identically to the overload
// above.
void fit_curve_reduction(const std::vector<std::vector<Point>> &chains,
const std::vector<std::vector<uint8_t>> &fixed,
std::vector<std::vector<QuadBezier>> &results, float tolerance);
Comment thread
Ryan-Millard marked this conversation as resolved.
Outdated
#endif
7 changes: 4 additions & 3 deletions core/include/internal/contours.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ ContoursResult find_contours(const std::vector<uint8_t>& binary, int width, int
void stitch_smooth(std::vector<Point>& vecA, std::vector<Point>& vecB);
void coupled_smooth(std::vector<std::vector<Point>>& contours, Rect bounds);

void pack_with_boundary_constraints(
std::vector<std::vector<Point>>& contours, Rect bounds, int iterations = 15
);
void coupled_smooth_junctions(std::vector<std::vector<Point>> &contours, Rect bounds, std::vector<uint8_t> junctions, int width);
Comment thread
Ryan-Millard marked this conversation as resolved.
Outdated

void pack_with_boundary_constraints(std::vector<std::vector<Point>> &contours, Rect bounds,
int iterations = 15);

} // namespace contours

Expand Down
28 changes: 28 additions & 0 deletions core/include/internal/douglas_peucker.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#ifndef DOUGLAS_PEUCKER_H
#define DOUGLAS_PEUCKER_H

#include <cstdint>
#include <vector>

#include "internal/contours.h"

// Douglas-Peucker point reduction. Reduces each contour's point count directly
// (the bezier least-squares fit's only real purpose here was shrinking the SVG).
//
// `fixed[i][k]!=0` marks junctions/corners that must NOT be removed: the chain is
// split at them so every one survives as an exact, shared endpoint -- neighbours
// that share a junction therefore split at the SAME point. Simplification is
// bounded so it never retracts a boundary inward past the inter-region overlap
// margin, so reducing one region never eats its overlap with another and opens a
// gap. Kept points are emitted as straight-line quads, so the existing QuadBezier
// SVG path emission (and its closing 'Z') is unchanged.
//
// `eps` : overall deviation tolerance (px). Env override IMG2NUM_DP_EPS.
// retract_eps : max inward (region-shrinking) move allowed when dropping a
// vertex; defaults to min(eps, 0.5px). Env override
// IMG2NUM_DP_RETRACT.
Comment thread
Ryan-Millard marked this conversation as resolved.
Outdated
void dp_curve_reduction(const std::vector<std::vector<Point>> &chains,
const std::vector<std::vector<uint8_t>> &fixed,
std::vector<std::vector<QuadBezier>> &results, float eps);
Comment thread
Krasner marked this conversation as resolved.
Outdated

#endif
22 changes: 13 additions & 9 deletions core/include/internal/graph.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,16 @@ class Graph {
void hash_node_ids(void);
void process_overlapping_edges();

public:
inline Graph(std::unique_ptr<std::vector<Node_ptr>>& nodes, int width, int height)
: m_nodes(std::move(nodes))
, m_width(width)
, m_height(height) {
inline uint8_t getPixel(const std::vector<uint8_t>& img, int w, int h, int x, int y) {
if (x < 0 || x >= w || y < 0 || y >= h) return 0; // Boundary check
return img[y * w + x];
}
Comment on lines +42 to +56

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably schedule a refactor to make the library use the image class wherever possible because we repeat this pattern a lot.


std::vector<uint8_t> analyzeJunctions(const std::vector<uint8_t>& skel, int w, int h);
Comment thread
Ryan-Millard marked this conversation as resolved.

public:
inline Graph(std::unique_ptr<std::vector<Node_ptr>> &nodes, int width, int height)
: m_nodes(std::move(nodes)), m_width(width), m_height(height) {
hash_node_ids();
}

Expand All @@ -68,10 +73,9 @@ class Graph {
return m_nodes->size();
}

void discover_edges(
const std::vector<int32_t>& region_labels, const int32_t width, const int32_t height
);
void merge_small_area_nodes(const int32_t min_area);
void discover_edges(const std::vector<int32_t> &region_labels, const int32_t width,
const int32_t height);
void merge_small_area_nodes(const int32_t min_area, const int32_t min_thickness = 0);
void compute_contours();
};

Expand Down
30 changes: 30 additions & 0 deletions core/include/internal/shared_contours.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#ifndef SHARED_CONTOURS_H
#define SHARED_CONTOURS_H

#include <cstdint>
#include <unordered_map>
#include <vector>

#include "internal/Point.h"
#include "internal/contours.h" // QuadBezier

// Crack-based shared-edge contour builder.
//
// Builds region boundaries on the pixel-corner ("crack") grid rather than on
// pixel centres. The boundary between two regions is extracted ONCE as a
// canonical edge (a chain of cracks between two junction corners), simplified
// once, and then reused by BOTH adjacent regions. Neighbouring contours are
// therefore exactly coincident along every shared edge -- no overlap band and no
// gaps, by construction.
//
// `labels` is the per-pixel region id (w*h, row major); any value is treated as a
// distinct region, out-of-bounds is the image exterior. Returns, per region id,
// its list of closed boundary loops, each a sequence of quadratic beziers in
// corner coordinates (0..w, 0..h). Each shared edge is fitted ONCE and reused by
// both adjacent regions, so neighbouring loops are exactly coincident.
//
// `eps` is the curve-fit tolerance applied to each canonical edge.
std::unordered_map<int32_t, std::vector<std::vector<QuadBezier>>> build_shared_loops(
const std::vector<int32_t> &labels, int w, int h, float eps);
Comment thread
Krasner marked this conversation as resolved.
Outdated

#endif
34 changes: 34 additions & 0 deletions core/src/internal/bezier.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,37 @@ void fit_curve_reduction(
results.push_back(result);
}
}

// --- Junction-aware wrapper ---
// Splits each chain at its fixed (junction) points and fits the pieces
// separately. Because fitRecursive always keeps a segment's first and last point
// exactly, every junction becomes a pinned on-curve point the fit cannot move.
void fit_curve_reduction(const std::vector<std::vector<Point>> &chains,
const std::vector<std::vector<uint8_t>> &fixed,
std::vector<std::vector<QuadBezier>> &results, float tolerance) {
for (size_t i = 0; i < chains.size(); ++i) {
const std::vector<Point> &chain = chains[i];
const int n = static_cast<int>(chain.size());
std::vector<QuadBezier> result;
if (n < 2) {
results.push_back(result);
continue;
}

// Segment boundaries: chain ends plus every interior junction point.
std::vector<int> bounds;
bounds.push_back(0);
for (int k = 1; k < n - 1; ++k)
if (k < static_cast<int>(fixed[i].size()) && fixed[i][k]) bounds.push_back(k);
bounds.push_back(n - 1);

// Fit each [bounds[s], bounds[s+1]] piece; consecutive pieces share the
// junction point, so the curve stays continuous and pinned there.
for (size_t s = 0; s + 1 < bounds.size(); ++s) {
const int a = bounds[s], b = bounds[s + 1];
std::vector<Point> seg(chain.begin() + a, chain.begin() + b + 1);
fitRecursive(seg, tolerance, result);
}
results.push_back(result);
}
}

@coderabbitai coderabbitai Bot Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate that fixed dimensions match chains dimensions.

The function assumes fixed[i].size() is compatible with chains[i].size(), but only checks the size at line 174 within the loop. If a caller provides a fixed mask with mismatched dimensions (e.g., fixed[i].size() < chains[i].size()), interior junctions beyond the fixed mask length will be silently ignored. Consider adding an upfront validation that fixed.size() == chains.size() and optionally that each fixed[i].size() == chains[i].size() to catch contract violations early.

🛡️ Proposed validation
 void fit_curve_reduction(const std::vector<std::vector<Point>> &chains,
                          const std::vector<std::vector<uint8_t>> &fixed,
                          std::vector<std::vector<QuadBezier>> &results, float tolerance) {
+    if (fixed.size() != chains.size()) {
+        throw std::invalid_argument("fixed and chains must have the same outer dimension");
+    }
     for (size_t i = 0; i < chains.size(); ++i) {
         const std::vector<Point> &chain = chains[i];
         const int n = static_cast<int>(chain.size());
+        if (i < fixed.size() && fixed[i].size() != static_cast<size_t>(n)) {
+            throw std::invalid_argument("fixed[i] and chains[i] must have the same size");
+        }
         std::vector<QuadBezier> result;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/internal/bezier.cpp` around lines 158 - 186, fit_curve_reduction
currently assumes fixed has the same outer and per-chain sizes as chains; add an
upfront validation that fixed.size() == chains.size() and for each i verify
fixed[i].size() == chains[i].size() (or at least >= chains[i].size()) and handle
violations (e.g., throw std::invalid_argument or assert) before the existing
loop so mismatched masks are caught early and interior junctions aren't silently
ignored; reference function fit_curve_reduction and variables fixed, chains and
the loop that uses fixed[i].size() to locate where to add the checks.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Krasner please check this out. I'm not sure if this is even likely.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Loading