Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
d7a54d6
fix(example-app): enable ref forwarding in GlassCard React component
Ryan-Millard Mar 7, 2026
d1199d5
feat(example-app): (React) Editor page controls
Ryan-Millard Mar 7, 2026
365797b
refactor(example-app): (React) make navbar hamburger reusable
Ryan-Millard Mar 8, 2026
b2c1018
feat(example-app): (React) improve UI and make reusable HamburgerMenu
Ryan-Millard Mar 11, 2026
ccd105a
feat(example-app): (React) add print feature
Ryan-Millard Mar 11, 2026
150959e
refactor(example-app): (React) move printing to its own function
Ryan-Millard Mar 11, 2026
410abe4
perf(example-app): (React) use pointerrawupdate for faster mouse drags
Ryan-Millard Mar 11, 2026
097a64f
feat(example-app): (React) undo and redo with history tracking
Ryan-Millard Mar 11, 2026
0b436bc
chore(example-app): (React) clean up styles
Ryan-Millard Mar 11, 2026
722e688
feat(example-app): add confirmation dialog for resets
Ryan-Millard Mar 11, 2026
9120639
feat(example-app): hide save buttons behind a modal and prompt for co…
Ryan-Millard Mar 11, 2026
d6dbf96
fix(example-app): (React) Modal displayed under other elements & adde…
Ryan-Millard Mar 12, 2026
0006934
fix(example-app): (React) editor page keybindings now support Mac
Ryan-Millard Mar 12, 2026
fd580a6
feat(example-app): (React) improve Editor page styles
Ryan-Millard Mar 20, 2026
6389ea8
refactor(example-app): (React) reuse download function and clean up CSS
Ryan-Millard Mar 21, 2026
184e7ca
feat(example-app): (React) improve editor page reset confirmation mod…
Ryan-Millard Mar 21, 2026
ff4bd80
fix(example-app): (React) unrevoked Blob URL
Ryan-Millard Mar 21, 2026
a762bcb
fix(example-app): (React) fix history bug where resetting doesn't cle…
Ryan-Millard Mar 21, 2026
27d6df7
fix(example-app): (React) Skip history writes when the tap doesn't ch…
Ryan-Millard Mar 21, 2026
e98b0fa
fix(example-app): (React) Don't swallow every Ctrl/Cmd shortcut, and …
Ryan-Millard Mar 21, 2026
b604158
ci(fix-checkouts): include recursive checkouts to include submodules…
Ryan-Millard Mar 21, 2026
6b0343c
Merge branch 'main' into feat/example-app/react/download-svg
Ryan-Millard Mar 21, 2026
ec0bfb1
Merge branch 'main' into feat/example-app/react/download-svg
Ryan-Millard Mar 21, 2026
cb44b7e
fix(example-app): (React) use pointermove when pointerrawupdate is un…
Ryan-Millard Mar 25, 2026
050bdde
fix(example-app): (React) revokeObjectURL on SVG in Editor page
Ryan-Millard Mar 25, 2026
3d8ca26
Merge branch 'main' into feat/example-app/react/download-svg
Ryan-Millard Mar 31, 2026
23faea3
Merge branch 'main' into feat/example-app/react/download-svg
Ryan-Millard Apr 1, 2026
2e489c1
style(example-app): format all files
Ryan-Millard Apr 1, 2026
01cbb1e
fix(react-js): resolve ESLint warnings in Editor components
Ryan-Millard Apr 1, 2026
35c0ea1
Merge branch 'main' into feat/example-app/react/download-svg
Ryan-Millard Apr 3, 2026
1aa1674
chore(file-resets): reset gpu.h and pr-auto-label.yml back to main's …
Ryan-Millard Apr 4, 2026
801584e
feat(editor): add fullscreen and improve zoom behavior
Ryan-Millard Apr 4, 2026
623e83f
style(example-app): (React) format all files
Ryan-Millard Apr 4, 2026
f56519f
refactor(example-app): (React) fix eslint warnings & tooltip positions
Ryan-Millard Apr 4, 2026
f736879
style(format): format all files
Ryan-Millard Apr 4, 2026
b6a40d3
Merge branch 'main' into feat/example-app/react/download-svg
Ryan-Millard Apr 4, 2026
20855b7
fix(example-app): (React) used functionbefore defined
Ryan-Millard Apr 4, 2026
843af6b
fix(useFullscreen): push/pop history on fullscreenchange instead of o…
Ryan-Millard Apr 4, 2026
4931be6
feat(editor): add raster (PNG/JPG) and PDF export to EditorControls
Ryan-Millard Apr 4, 2026
7b18ad8
style(format): format all files
Ryan-Millard Apr 4, 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
5 changes: 3 additions & 2 deletions core/include/internal/gpu.h
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,9 @@ class GPU {
deviceDesc.SetDeviceLostCallback(
wgpu::CallbackMode::AllowProcessEvents,
[](const wgpu::Device&, wgpu::DeviceLostReason reason, wgpu::StringView msg) {
std::string err_msg = (msg.data && msg.length > 0) ? std::string(msg.data, msg.length)
: "Unknown device lost reason";
std::string err_msg = (msg.data && msg.length > 0)
? std::string(msg.data, msg.length)
: "Unknown device lost reason";
std::cerr << "[DEVICE LOST] Reason: " << static_cast<int>(reason)
<< " Msg: " << err_msg << std::endl;
});
Expand Down
21 changes: 12 additions & 9 deletions core/src/internal/graph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ void Graph::process_overlapping_edges() {

for (const Node_ptr &n : get_nodes()) {
if (n->area() == 0) continue;

for (auto &[_, p] : n->get_pixels()) {
label_map[p.y * m_width + p.x] = n->id();
}
Expand Down Expand Up @@ -170,7 +170,6 @@ void Graph::process_overlapping_edges() {

// Is it a neighbor? AND have we not processed this pairing yet?
if (n_val != 0 && n_val != val && val < n_val) {

bool is_too_thin = false;

// Check around the neighbor pixel for a 3rd region (pinching)
Expand All @@ -181,16 +180,18 @@ void Graph::process_overlapping_edges() {
if (mx < 0 || mx >= m_width || my < 0 || my >= m_height) continue;

int32_t m_val = label_map[my * m_width + mx];

if (m_val != 0 && m_val != val && m_val != n_val) {
is_too_thin = true;
break; // CRITICAL: Stop checking immediately once proven thin!
break; // CRITICAL: Stop checking immediately once proven thin!
}
}

if (is_too_thin) {
// Give our pixel to the neighbor
Node_ptr neighbor_node = m_nodes->at(m_node_ids[n_val]); // get_node_by_id(n_val); // Assuming you have this lookup
Node_ptr neighbor_node =
m_nodes->at(m_node_ids[n_val]); // get_node_by_id(n_val); // Assuming
// you have this lookup
if (neighbor_node) {
neighbor_node->add_edge_pixel(XY{x, y});
}
Expand All @@ -213,7 +214,7 @@ void Graph::compute_contours() {
if (n->area() == 0) continue;
n->compute_contour();
}

// smoothing
std::vector<std::vector<Point>> all_contours;
for (const Node_ptr &n : get_nodes()) {
Expand Down Expand Up @@ -256,7 +257,7 @@ void Graph::merge_small_area_nodes(const int32_t min_area) {
std::copy(n->edges().begin(), n->edges().end(), std::back_inserter(neighbors));

ImageLib::RGBPixel<uint8_t> col = n->color();

Node_ptr best_neighbor = nullptr;
float best_score = std::numeric_limits<float>::max();
for (const Node_ptr &ne : n->edges()) {
Expand All @@ -271,8 +272,10 @@ void Graph::merge_small_area_nodes(const int32_t min_area) {
}

// no valid neighbor found, skip this node
if (!best_neighbor) { continue; }

if (!best_neighbor) {
continue;
}

if (best_neighbor->area() >= n->area()) {
merge_nodes(best_neighbor, n);
} else {
Expand Down
8 changes: 4 additions & 4 deletions core/src/internal/labels_to_svg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -195,18 +195,18 @@ char *labels_to_svg(uint8_t *data, int32_t *labels, const int width, const int h
// 1. enumerate regions and convert to Nodes
std::vector<Node_ptr> nodes;
region_labeling(data, labels_vector, region_labels, width, height, nodes);

// 2. initialize Graph from all Nodes
std::unique_ptr<std::vector<Node_ptr>> node_ptr =
std::make_unique<std::vector<Node_ptr>>(std::move(nodes));
Graph G(node_ptr, width, height);

// 3. Discover node adjacencies - add edges to Graph
G.discover_edges(region_labels, width, height);

// 4. Merge small area nodes until all nodes are minArea or larger
G.merge_small_area_nodes(min_area);

// 5. recolor image on new regions
ImageLib::Image<ImageLib::RGBAPixel<uint8_t>> results{width, height};
for (auto &n : G.get_nodes()) {
Expand All @@ -221,7 +221,7 @@ char *labels_to_svg(uint8_t *data, int32_t *labels, const int width, const int h
// 6. Contours
// graph will manage computing contours
G.compute_contours();

// accumulate all contours for svg export
ColoredContours all_contours;
for (auto &n : G.get_nodes()) {
Expand Down
5 changes: 3 additions & 2 deletions example-apps/react-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,17 +107,18 @@
"lint:style": "editorconfig-checker"
},
"dependencies": {
"@tanstack/react-query": "^5.95.0",
"@testing-library/dom": "^10.4.1",
"html-react-parser": "^5.2.15",
"img2num": "workspace:*",
"jspdf": "^4.2.1",
"lucide-react": "^0.577.0",
"prop-types": "^15.8.1",
"react": "^18.2.4",
"react-dom": "^18.2.4",
"react-helmet": "^6.1.0",
"react-router-dom": "^7.13.1",
"react-tooltip": "^5.30.0",
"@tanstack/react-query": "^5.95.0"
"react-tooltip": "^5.30.0"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
Expand Down
7 changes: 4 additions & 3 deletions example-apps/react-js/src/components/GlassCard.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { forwardRef } from "react";
import PropTypes from "prop-types";
import styles from "./GlassCard.module.css";

// eslint-disable-next-line no-unused-vars
const GlassCard = ({ as: Tag = "div", children, ...rest }) => (
<Tag {...rest} className={`text-center glass ${styles.card} ${rest.className || ""}`}>
const GlassCard = forwardRef(({ as: Tag = "div", children, ...rest }, ref) => (
<Tag {...rest} className={`text-center glass ${styles.card} ${rest.className || ""}`} ref={ref}>
{children}
</Tag>
);
));

GlassCard.propTypes = {
as: PropTypes.oneOf([
Expand Down
66 changes: 66 additions & 0 deletions example-apps/react-js/src/components/GlassModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useEffect } from "react";
import { createPortal } from "react-dom";
import PropTypes from "prop-types";
import GlassCard from "@components/GlassCard";
import styles from "./GlassModal.module.css";
import { X } from "lucide-react";

export default function GlassModal({
isOpen,
onClose,
children,
size = "lg", // sm | md | lg | any CSS width
showCloseButton = true,
closeOnBackdropClick = true,
className = "",
style = {},
}) {
useEffect(() => {
const handleKey = (e) => {
if (e.key === "Escape") onClose?.();
};
if (isOpen) window.addEventListener("keydown", handleKey);
return () => window.removeEventListener("keydown", handleKey);
}, [isOpen, onClose]);

if (!isOpen) return null;

const handleBackdropClick = (e) => {
if (e.target === e.currentTarget && closeOnBackdropClick) {
onClose?.();
}
};

const width = size === "sm" ? "300px" : size === "md" ? "500px" : size === "lg" ? "800px" : size;

const modal = (
<div className={styles.backdrop} onClick={handleBackdropClick} role="dialog">
<GlassCard
as="div"
className={`${styles.modal} ${className}`}
style={{
flex: "0 0 auto",
width: width,
...style,
}}
>
{showCloseButton && (
<a
href="#"
onClick={(e) => {
e.preventDefault();
onClose();
}}
className={`${styles.closeButton}`}
aria-label="Close modal"
>
<X size={20} />
</a>
)}
{children}
</GlassCard>
</div>
);

return createPortal(modal, document.body);
}
54 changes: 54 additions & 0 deletions example-apps/react-js/src/components/GlassModal.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
.backdrop {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.2s ease forwards;
}

.modal {
position: relative;
border-radius: 1rem;
background: var(--color-bg);
padding: var(--spacing-xl) var(--spacing-lg) var(--spacing-lg);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
animation: scaleIn 0.2s ease forwards;
max-height: 90vh;
max-width: 95vw;
overflow-y: auto;
z-index: 1001;
}

.closeButton {
position: absolute;
top: 0.5rem;
right: 0.5rem;
border: none;
cursor: pointer;
padding: var(--spacing-xs);
display: flex;
align-items: center;
justify-content: center;
}

@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

@keyframes scaleIn {
from {
transform: scale(0.95);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
Comment thread
Ryan-Millard marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
border-radius: 9999px;
position: relative;
cursor: pointer;
margin-right: 15px;
}

/* WCAG 2.4.7 Compliant Focus Indicator */
Expand Down
9 changes: 8 additions & 1 deletion example-apps/react-js/src/global-styles/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
box-sizing: border-box;
}

body {
body,
:not(:root):fullscreen::backdrop {
margin: 0;
padding: var(--spacing-lg);
color: var(--color-text);
Expand Down Expand Up @@ -60,6 +61,11 @@ img {
}

/* General link style */
button.anchor-style {
padding: auto;
background: none;
border: none;
}
a,
.anchor-style {
color: var(--color-primary);
Expand All @@ -69,6 +75,7 @@ a,
text-decoration 0.3s;
cursor: pointer;
text-decoration: underline;
cursor: pointer;
}

/* Hover/focus states */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
display: flex;
justify-content: space-between;
}
.flex-wrap-wrap {
display: flex;
flex-wrap: wrap;
}

.grid {
display: grid;
Expand Down
Loading
Loading