Skip to content

hyperfy on crac - #2

Merged
tj (tjkilgore93-stack) merged 34 commits into
Hyperfy1:mainfrom
storytold:main
Oct 13, 2026
Merged

tj (tjkilgore93-stack) merged 34 commits into
Hyperfy1:mainfrom
storytold:main

Conversation

@tjkilgore93-stack

Copy link
Copy Markdown

What and why

How it was tested

Checklist

  • cargo xtask ci passes (fmt, clippy, tests, assets, brands, layers, wasm).
  • No panics in shipped code: no unwrap(), expect(), panic!, unreachable!, todo! or
    unimplemented! outside tests, and no unchecked indexing on data from files, parameters or
    messages. Errors are returned as Result and shown to the user
    (why and how).
  • Clean-room: nothing copied from Adobe products or GPL/AGPL code (AGENTS.md).
  • Every new asset has a row in ASSETS.md.

<flowRoot> (SVG 1.2 flowed text, which Inkscape writes for text typed
into a frame) was dropped without a word: usvg doesn't draw it. The
text pass now reads it as area type: the first <rect> of its
<flowRegion> is the frame (with the rect's transform), each <flowPara>
a paragraph (white space collapsed), styled through the same cascade
as <text>. Another kind of region leaves the text out with a warning.

Fixes #1568
The public Pathfinder documentation says Trim and Crop remove any
strokes, and Merge removes them and merges same-coloured fills. The
pieces kept every stroke, and Merge, grouping by the whole style, left
same-filled objects with different strokes apart. The three now drop
stroke layers before styles are compared.

Fixes #1570
warp_point worked on box coordinates normalised to -1..1 on both axes:
Arc's radius was in x units but its edges were offset in y units, so a
box that isn't square bent into no circular arc (skewed sides from
Bend 10 %), and at 100 % the radius (2/π) was smaller than the
half-height, folding the inner edge over. warp_point now takes the
box's aspect along the warp; Arc keeps the centre line's length on a
circular arc in real proportions, with concentric edges, and an inner
edge that would pass the centre stops there. Other styles are as they
were; a square is bent as before.

Fixes #1578
Commands whose description takes `ids` (operate on these objects;
default: the selection) failed with "nothing selected" when objects
were given but none was selected: execute checked their `enabled`
predicate against the selection, with an exception for edit.clear only.
Of the 220 commands mentioning ids, 48 were refused so; their run
already used the ids. execute now evaluates `enabled` with the given
ids selected and puts the selection back, so each predicate keeps its
meaning (Make with Top Object still needs two objects).

Fixes #1575
…document

objects_at() found Select Behind targets by running hit_test_skipping()
in a loop, skipping everything found so far. Each round is a full tree
traversal, so a stack of n overlapping paths costs O(n^2) hit tests
before the loop finds no further object.

The fast path walks each editable top-level layer once. Every selectable
object's subtree is visited only until its first hit, and a HashSet
records found ids so subtrees already covered by an earlier hit are not
searched again. Sublayers recurse the same way, so a stack of n paths
costs one traversal total.

The original traversal stays in charge where Select Behind semantics
differ from the plain front-to-back order: isolation (scope), and Edit
Contents, which exposes objects nested inside a top-level group. Non-
layer roots in malformed files also fall back. objects_at_repeated()
keeps the old loop for those cases.

Three tests: a 256-path stack asserts front-to-back order and
completeness; a mixed document (sublayer, groups, clipping group,
outline and path-only options) asserts the one-pass result equals the
repeated-traversal result; an envelope in Edit Contents asserts the
fallback preserves the original order.
… scan

Indexed export (GIF, PNG-8, BMP, TIFF) finds each pixel's nearest
palette entry with a linear scan, and the k-means refinement in median
cut does the same once per cell per pass. With the 256-entry palettes
this code produces, that is 256 distance evaluations per query.

PaletteIndex is an exact nearest-neighbour kd-tree over the palette,
balanced at the widest weighted axis, with tight channel bounds in every
subtree so branch-and-bound can skip whole boxes. Small palettes (12 or
fewer entries) keep the scan, which is cheaper than tree traversal.

Results match the linear scan exactly, including ties: an equal-distance
entry with a smaller index still wins. The bound uses the same weighted
squared f32 form as dist() rather than a transformed space, so rounding
cannot change which entry is chosen.

Three tests: randomized parity against the original scan across palette
sizes spanning the cutoff, three weight vectors, duplicate colours and
fractional query points; explicit tie-breaking cases; and an opt-in
benchmark comparing 100k queries at both, behind -- --ignored.
With paths selected but every offset empty (an inset larger than the
shape), Offset Path failed with "select paths", sending the user to
look for a selection problem. It now says the offset leaves nothing of
the selected paths; "select paths" stays for selections without paths.

Fixes #1590
… documents checked against the repeated hit test
moxcms's encode packs tagged elements back to back, so the built-in
sRGB profile embedded in JPEG, TIFF and PSD exports was 634 bytes with
`cprt` at offset 558, where ICC.1 §7.1.2 requires every tagged element
on a 4-byte boundary (stricter readers reject the files). encode now
lays the tag data out again with each element padded, updating the tag
offsets, the size and the profile ID (zeroed, "not computed"); tags
that share data keep sharing it. Reported to moxcms as
awxkee/moxcms#198.

Fixes #1584
Auto Grouping (params `autoGrouping`, a checkbox in the panel; off unless asked
for): each traced path that has others inside its outer contour comes out in a
group with them, the path first, nested as deep as the picture is. The path that
covers the whole image is nobody's group.

The grouped result paints the same picture: where paths can overlap (stacked
layers, Flat Logo, strokes) a path only joins a path below it, and only if that
moves it under nothing that shares any of its box.

The harness measures three presets with the option on ("grouped %"), painting
them in the grouped order.
Docs: ROADMAP 2026-10-13 adds #1492–#1583; Average and Join corrected to missing, Polish row count
Pathfinder: Trim, Merge and Crop remove strokes
SVG import: Inkscape flowed text (flowRoot) becomes area type
Warp › Arc: bend in the object's real proportions
Commands: objects given as ids stand in for the selection
Select Behind: test each object once instead of re-hitting the whole document
…-index

Palette nearest-colour lookup: branch-and-bound index over the linear scan
ICC profiles written here have their tags 4-byte aligned
Offset Path: an empty result says the offset leaves nothing
Type on a path is written glyph by glyph, and a stroke added to it as each
glyph's outline stroked right after the glyph. The path in between ended the
line, so each glyph came in as its own text object. Such a stroke, in the
glyph's own paint, is now held with the line and written after it, where it
becomes the type's stroke as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	crates/pdf/src/tests_import_lines.rs
Straight type takes the tracking that keeps its length as the file set
it; type on a path took none, so letter-spaced type on a curve came back
closed up to the font's advances (an "OFFICIAL ISSUED" badge sat to one
side of its band, a condensed title ran off the end of its path). Type on
a path now takes the same tracking, its length measured along the path
from the first glyph to the end of the last.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PDF/.ai import: evenly tracked type on a path keeps its length
PDF/.ai import: type stroked glyph by glyph stays one object
Gradients (params `gradients`, a checkbox in the panel for Grayscale and Color;
off unless asked for): an area whose colour changes evenly along one direction
is traced as one path filled with a linear gradient, not as bands of flat colour.

Gradients are found before the palette is made, so no colour is spent on their
bands: a connected run of smoothly changing pixels is a gradient when a plane
fits each colour channel with all three sloping the same way, the colours along
that way are at most six stops with ramps between them, and painting them back
gives the area's own pixels. Flat colour, soft edges between flat colours and
shading that goes two ways are left as they were.

The harness gains two linear-gradient inputs with known lines and stops, and
measures whether each is found, the angle of its line and the colours of its ramp.
Effects: effect.apply and effect.setParams refuse parameters the effect doesn't take (#1592)
Copilot AI balanced review requested due to automatic review settings October 13, 2026 05:27
@tjkilgore93-stack
tj (tjkilgore93-stack) merged commit 20de62a into Hyperfy1:main Oct 13, 2026
3 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Gradient tracing can exceed memory limits, grouping is unbounded, and effect/SVG parameter handling has correctness gaps.

4 open findings
What changed in this PR

This broad PR adds Image Trace grouping/gradients alongside assorted import, rendering, validation, performance, and documentation updates.

Changes:

  • Adds gradient detection and automatic grouping to Image Trace.
  • Improves SVG/PDF text import, hit testing, quantization, warping, and ICC output.
  • Expands effect validation, command targeting, tests, localization, and progress documentation.
File Description
ROADMAP.md Records recent project progress.
docs/​ui-parity.md Updates interaction parity status.
docs/​type-parity.md Documents Linux font discovery.
docs/​roadmap.md Updates milestone history.
docs/​mcp.md Documents effect parameter validation.
docs/​localization-parity.md Corrects Polish catalog counts.
docs/​gaps.md Updates ranked gaps and fixes.
docs/​file-format-parity.md Records import and native-save improvements.
docs/​effects-parity.md Records raster-effect export fixes.
crates/​ui-egui/​src/​panels/​image_trace.rs Adds grouping and gradient controls.
crates/​ui-egui/​src/​menus.rs Uses session-aware parameter checks.
crates/​ui-egui/​src/​lib.rs Passes session to validation.
crates/​ui-egui/​src/​i18n/​zh-hant.tsv Adds Traditional Chinese translation.
crates/​ui-egui/​src/​i18n/​zh-hans.tsv Adds Simplified Chinese translation.
crates/​ui-egui/​src/​i18n/​vi.tsv Adds Vietnamese strings.
crates/​ui-egui/​src/​i18n/​uk.tsv Adds Ukrainian strings.
crates/​ui-egui/​src/​i18n/​ru.tsv Adds Russian strings.
crates/​ui-egui/​src/​i18n/​pt-br.tsv Adds Brazilian Portuguese error text.
crates/​ui-egui/​src/​i18n/​pl.tsv Adds Polish strings.
crates/​ui-egui/​src/​i18n/​ko.tsv Adds Korean strings.
crates/​ui-egui/​src/​i18n/​ja.tsv Adds Japanese strings.
crates/​ui-egui/​src/​i18n/​it.tsv Adds Italian strings.
crates/​ui-egui/​src/​i18n/​hu.tsv Adds Hungarian strings.
crates/​ui-egui/​src/​i18n/​fr.tsv Adds French strings.
crates/​ui-egui/​src/​i18n/​es.tsv Adds Spanish strings.
crates/​ui-egui/​src/​i18n/​de.tsv Adds German strings.
crates/​ui-egui/​src/​i18n/​cs.tsv Adds Czech strings.
crates/​ui-egui/​src/​control.rs Applies session-aware validation.
crates/​trace/​tests/​quality/​raster.rs Renders traced gradients in tests.
crates/​trace/​tests/​quality/​measure.rs Measures grouping and gradient quality.
crates/​trace/​tests/​quality/​main.rs Expands the quality report.
crates/​trace/​tests/​quality/​inputs.rs Adds synthetic gradient inputs.
crates/​trace/​src/​logo/​pipeline.rs Initializes gradient metadata.
crates/​trace/​src/​lib.rs Integrates grouping and gradient tracing.
crates/​trace/​src/​group.rs Implements automatic nesting.
crates/​trace/​src/​gradient.rs Implements linear-gradient detection.
crates/​svg/​tests/​import_text.rs Tests flowed-text import.
crates/​svg/​src/​import/​text.rs Imports flowRoot as area text.
crates/​render/​src/​encode/​quantize.rs Adds indexed palette lookup.
crates/​pdf/​src/​tests_realtext.rs Tests tracked path text.
crates/​pdf/​src/​tests_import_lines.rs Tests glyph-outline stroke handling.
crates/​pdf/​src/​import.rs Holds glyph strokes during text import.
crates/​pdf/​src/​import_text.rs Restores path-text tracking.
crates/​mcp/​src/​headless.rs Uses session-aware validation.
crates/​engine/​tests/​param_keys.rs Tests backward-compatible effect replay.
crates/​engine/​src/​tests_rasterfilters.rs Updates raster-effect parameter tests.
crates/​engine/​src/​tests_pathops.rs Tests Pathfinder and offset behavior.
crates/​engine/​src/​tests_params.rs Tests effect parameter validation.
crates/​engine/​src/​tests_ids_without_selection.rs Tests explicit command targets.
crates/​engine/​src/​tests_build.rs Tests grouping and gradients.
crates/​engine/​src/​lib.rs Enables commands using explicit IDs.
crates/​engine/​src/​cmd/​pathops.rs Fixes Pathfinder strokes and offset errors.
crates/​engine/​src/​cmd/​params.rs Validates effect parameters.
crates/​engine/​src/​cmd/​effectcmd.rs Resolves edited effect types.
crates/​engine/​src/​cmd/​buildcmds.rs Builds grouped gradient traces.
crates/​effects/​src/​warp.rs Applies aspect-aware warping.
crates/​effects/​src/​lib.rs Clarifies Free Distort parameters.
crates/​doc/​src/​live.rs Corrects Arc warp proportions.
crates/​doc/​src/​hit.rs Optimizes Select Behind hit testing.
crates/​color/​src/​cms/​tests_icc.rs Tests ICC tag alignment.
crates/​color/​src/​cms/​icc.rs Aligns encoded ICC data.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread crates/trace/src/group.rs
Comment on lines +116 to +120
let found = holders.find(|&(p, op)| {
ox.pts.first().is_some_and(|pt| inside_or_near(*pt, op))
&& (!stacked || under().iter().all(|&y| !comes_after(&parent, &depth, y, p)))
&& ox.pts.iter().all(|pt| inside_or_near(*pt, op))
});
Comment thread crates/trace/src/lib.rs
let img = smoothed.as_ref().unwrap_or(img);
let mut q = quantize(img, params);
// Gradients are found first and kept out of the palette: no colour is spent on bands of them.
let found = (params.gradients && params.mode != Mode::BlackAndWhite).then(|| gradient::detect(img)).filter(|f| !f.1.is_empty());
Comment on lines +235 to +239
match defaults.and_then(|d| d.get(key)) {
Some(Value::Number(_)) if !v.is_number() => return Err(bad(id, format!("`{key}` must be a number"))),
Some(Value::Bool(_)) if !v.is_boolean() => return Err(bad(id, format!("`{key}` must be true or false"))),
// (A colour is also taken as `[r, g, b]` or as a colour object.)
Some(Value::String(_)) if v.is_number() || v.is_boolean() => return Err(bad(id, format!("`{key}` must be a string"))),
Comment on lines +583 to +587
let paras: Vec<XNode> = t.descendants().filter(|n| n.is_element() && n.tag_name().name() == "flowPara").collect();
let collapse = |n: XNode| {
n.descendants().filter(XNode::is_text).filter_map(|c| c.text()).collect::<String>().split_whitespace().collect::<Vec<_>>().join(" ")
};
let text = paras.iter().map(|p| collapse(*p)).collect::<Vec<_>>().join("\n");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants