diff --git a/pyproject.toml b/pyproject.toml index 16a0fae..4c3effd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build" [project] name = "openscad_cpp_evaluator" -version = "1.20.2" +version = "1.20.3" description = "C++ OpenSCAD evaluator with Python bindings" readme = "README.md" requires-python = ">=3.12" diff --git a/src/builtins/text.cpp b/src/builtins/text.cpp index 4a38e22..0f53f99 100644 --- a/src/builtins/text.cpp +++ b/src/builtins/text.cpp @@ -70,6 +70,16 @@ CSGParams resolveText(Evaluator& ev, const oscad::ModularCall& node, EvalContext } CSGParams params; + // The handle is what generateText looks the face up by, but it is an + // index into THIS evaluator's FontProvider -- the first font resolved + // is handle 0 whatever it is. Two evaluators sharing a ManifoldCache + // (two tabs in a GUI, say) therefore agreed on "font_handle=0" for + // entirely different fonts, and the second render was served the + // first one's glyphs. The spec goes in the params too, so the cache + // key tells them apart: the same spec always resolves to the same + // face, and two specs that happen to resolve alike merely miss the + // cache rather than collide. + params["font_spec"] = Value{fontSpec}; params["font_handle"] = Value{static_cast(handle)}; params["scale"] = Value{scale}; params["segs"] = Value{static_cast(segs)}; diff --git a/tests/test_manifold_cache.cpp b/tests/test_manifold_cache.cpp index fa57cbd..8d1f59d 100644 --- a/tests/test_manifold_cache.cpp +++ b/tests/test_manifold_cache.cpp @@ -36,6 +36,28 @@ TEST(ManifoldCacheKey, BoolAndNumberParamsProduceDifferentCacheKeys) { EXPECT_NE(cacheKey(a), cacheKey(b)); } +TEST(ManifoldCacheKey, TwoFontsWithTheSameHandleDoNotShareAKey) { + // A FontHandle indexes the EVALUATOR's own FontProvider, so the first + // font any evaluator resolves is handle 0 whatever it is. Two + // evaluators sharing one ManifoldCache -- two tabs in a GUI -- agreed + // on "font_handle=0" for entirely different fonts, and the second was + // served the first one's glyphs. text() puts the SPEC in the params + // too, so the key tells them apart. + CSGNode a; + a.kind = "text"; + a.isBuiltin = true; + a.params["font_handle"] = Value{0.0}; + a.params["font_spec"] = Value{std::string("Arial:style=Regular")}; + + CSGNode b; + b.kind = "text"; + b.isBuiltin = true; + b.params["font_handle"] = Value{0.0}; // same handle, different font + b.params["font_spec"] = Value{std::string("Arial:style=Bold")}; + + EXPECT_NE(cacheKey(a), cacheKey(b)); +} + TEST(ManifoldCacheKey, IdenticalStructureProducesIdenticalKeyRegardlessOfParamInsertionOrder) { CSGNode a; a.kind = "cube"; diff --git a/tests/test_text.cpp b/tests/test_text.cpp index 984f160..1f7b8ff 100644 --- a/tests/test_text.cpp +++ b/tests/test_text.cpp @@ -422,3 +422,30 @@ TEST(TextArgs, FontIsPositionalButTheRestAreNot) { // NOTHING, and halign by name does move it. EXPECT_EQ(echoes[0], "ECHO: true, false, true, false"); } + +namespace oscadeval { + +// Why text() must put the font SPEC in its CSG params and not just the +// handle: a handle numbers faces WITHIN one provider, and every Evaluator +// makes its own. Two evaluators sharing a ManifoldCache -- two GUI tabs -- +// hand the cache the same number for different fonts (BelfrySCAD#434). +TEST(FontHandles, AreOnlyMeaningfulWithinOneProvider) { + FreetypeFontProvider a, b; + const FontHandle aReg = a.resolveFont("Arial:style=Regular"); + const FontHandle aBold = a.resolveFont("Arial:style=Bold"); + if (aReg == aBold) { + GTEST_SKIP() << "this machine resolves both specs to one face"; + } + EXPECT_NE(aReg, aBold) << "within one provider, two fonts get two handles"; + // What a second provider numbers them is NOT asserted: it depends on + // which faces that machine has and the order they are opened in. On + // macOS a fresh provider resolving Bold first returns 4 -- the same + // number the first provider gave REGULAR, which is the collision this + // whole change is about. On a Linux runner with only the bundled faces + // the numbering happens to line up instead (0 and 1 either way). That + // it CAN collide is the point, and it is not a property to pin to a + // number here; the end-to-end regression test lives in BelfrySCAD, + // where the collision reproduces in both directions. +} + +} // namespace oscadeval