Releases: LubrieDev/lmath
Release list
1.3.3
1.3.3
Why 1.3.2 was superseded
1.3.2 did not pass Obsidian's automated review. Everything else in that review passed: the
main.js and styles.css assets carried verified GitHub attestations, no suspicious network
patterns were found, no vulnerable dependencies were detected, and the build check reproduced
the released main.js byte for byte from the repository, which is the strongest thing the
report can say about a plugin. The only failure was in the source-code check: eight instances of
obsidianmd/no-static-styles-assignment, all of them in the new block's chrome.
A release marked Failed is not one anyone should install, so 1.3.2 is superseded by this
one. This release is 1.3.2 with that check satisfied and nothing else changed: the plugin
behaves exactly as the 1.3.2 release notes describe, and that document remains the reference
for what obs-trig does.
The fix
The eight findings were one defect repeated, not eight different problems: styling that never
changes, written as inline strings in the code instead of as rules in the stylesheet. The rule
asks for CSS classes, setCssStyles or setCssProps, and all eight cases could simply be
replaced with CSS classes. What stayed in the host is what genuinely depends on values computed
at runtime: the chip's side, the box's width and the handle's position. That is why the review
never flagged them.
-
The slider's focus ring left the TypeScript entirely. A
focuslistener and ablur
listener that painted an outline became a single:focus-visiblerule instyles.css. That
also fixes an accessibility detail: the ring used to appear on mouse clicks too, which is
precisely when nobody needs it.:focus-visiblelets the browser make that distinction. The
inlineoutline: nonehad to disappear with those listeners, otherwise it would have overridden
the stylesheet rule and the focus ring would never have appeared. -
The trig controls' two layouts became two CSS classes, toggled at the width threshold
instead of swapping style strings over one another. The height of the bottom strip now reaches
the stylesheet throughsetCssPropsas a custom property, soALTO_CONTROLES_TRIGremains the
single source of truth for that measurement. Copying the value into the stylesheet would have
created two sources of truth for the same layout, and that dimension is shared with the canvas. -
The remaining four findings were static pieces: the reading band, the component row, the
live value itself — the large number the whole panel revolves around — and one row of the ratio
table. They are now ordinary CSS classes. The row that holds that value (θ on the left, the
number on the right) was converted as well, even though the review did not flag it, because it
was the same kind of rule one line away. Converting only half of a block is how a stylesheet
becomes harder to maintain.
What did not change
No behavior, no syntax, no rendering and no settings changed. minAppVersion remains
1.13.0. If any block, including obs-trig, behaves or renders differently after updating,
that is a defect and should be reported.
Validation
- Main suite: 486 passed, 0 failed.
- Zoom suite: 12 passed, 0 failed.
- Typecheck and build: clean.
- Review audit: 0 findings, down from the eight this release exists to remove.
- CSS audit: clean.
What the tests do not cover. The block host still has no automated DOM tests, and this
release only moved presentation code, which is exactly the part no automated suite can see. Two
things were therefore verified manually: that the slider shows its focus ring when reached with
Tab but not when clicked, and that the controls transition correctly across the 520 px
breakpoint in both directions.
1.3.2
Version 1.3.2
This release adds one new block, obs-trig, and two settings that go with it. It is the first
LMath block that does not plot a curve: it draws the unit circle, and you drive an angle around it.
It also brings an angle unit selector — degrees, radians or gradians — as a global plugin
setting and as a chip on the block.
Nothing else changes. The four existing blocks — obs-graph, obs-system, obs-derivate,
obs-integral — keep the same syntax and draw the same curves, and minAppVersion stays at
1.13.0.
The new block: obs-trig
An empty block already renders a working figure at 30°: in this block the unit circle is the
content, so there is nothing missing to report.
```obs-trig
θ = 30°
```
Writing a block
One line, one angle. The = only assigns a name, and the name is optional — it is not an
equation, so 30° on its own is a complete block and gets called θ.
| you write | you get |
|---|---|
| (empty) | 30° |
30° |
one angle, named θ by default |
θ = 30° |
the same angle, named θ explicitly |
α = 30° … δ = 330° |
four angles at once |
θ = 750° |
two turns and 30°: the point sits at 30° and the turn count says so |
θ = -45° |
negative angles are ordinary input |
θ = 30 |
30 radians, not 30 degrees |
θ = \frac{\pi}{6}, pi/6, 2\pi |
π is written however you write it anywhere else |
The two unit rules, which are not the same rule. The angle a block declares is read in
radians when it is a bare number: θ = 30 is 30 radians and lands at 1718.9°, not at 30°.
Degrees need the °. But inside a trigonometric function the plugin keeps its usual
convention, which is the opposite one: a literal argument is read in degrees, so sin(30) is
0.5 — sine of 30 degrees — here exactly as in obs-graph. Written as the angle of a block,
θ = sin(30) is therefore 0.5 radians. Two rules, two places; the ° is what removes all
doubt from the first.
A line that is not a readable angle is reported in the panel, just under the card — not over
the plane, where it would cover the controls. At most three are listed, and a +N counts the
rest. If no line is readable the block still draws: it falls back to 30° and reports what it
could not read.
Naming a ratio turns its trace on
If the expression is exactly a call to sin, cos or tan on a constant angle, the block
opens with that component already drawn, solid and with its construction. Writing the name of a
ratio is already saying which one you want to look at; making you press the toggle afterwards is
asking you to repeat yourself.
```obs-trig
sin(30)
```
It chooses a trace, it does not reinterpret the source. The angle is still the value the
expression evaluates to, by the same rule as everywhere else — sin(30) is 0.5, so the block
draws 0.5 radians and lights the sine of that angle. Nothing about the reading changes.
"Exactly" is meant literally, and it is checked on the normalized expression, which is the one
that actually gets evaluated:
| you write | opens with |
|---|---|
sin(30), \sin{30}, \sin 30, cos(45°), \tan{\frac{\pi}{4}} |
that component |
2sin(30), -sin(30), sin(30)*2, sin(30)+cos(30) |
nothing: the call is not the whole expression |
asin(0.5), arcsin(0.5), sinh(1), cot(30), sec(30) |
nothing: no trace on the figure |
sin(x) |
nothing — and it is not an angle either, so it is reported |
30°, \frac{\pi}{6}, 750° |
nothing, as before |
Only the angle that opens active is consulted, because the components belong to the active
angle. And it is only a seed: the moment you touch a toggle the selection is yours, and moving to
another angle with Tab does not seed it again — a choice that undid itself as you navigated would
be the same mistake as rewriting the note when you drag.
The figure
The framing is fixed — no camera, no zoom, no pan. The wheel is never captured, so the note
scrolls normally when the mouse is over the block. On touch the plane does keep the gesture, as
every LMath plane does: a swipe that starts on the circle drives the angle instead of scrolling the
note, so scroll from the panel or from the margin.
- Two grids. A faint cartesian 1:1 grid and dotted radial spokes every 15°. Each measures what
the other cannot: the cartesian grid measures the vertical and horizontal lengths that sine and
cosine are, so "this leg is a half" can be counted in quarter-unit cells — half-unit ones once
the circle gets small enough that a quarter would stop being legible — while the spokes
measure the angle and anchor the labels around the rim. The four multiples of 90° carry no spoke —
the axes are already there, and a dotted line over an axis only blurs it — so 20 are drawn. - The 24 notable angles are marked on the circumference, the sixteen classics — the multiples
of 30° and 45° — with a fatter dot than the rest. Those sixteen are also labelled outside the
rim in both units at once, degrees over the fraction of π. The labelling adapts to the size
of the plane: two lines, then one line in the active unit, then the four axes only, then none.
The label that falls under the active angle stays quiet, because the coordinates of P need that
gap — and it is matched by coterminal, soθ = 750°silences the label at 30°. - Per angle, its terminal side from the origin and its point on the circumference. The
active angle is drawn last, so it stays on top where two cross, and it is heavier and carries
a halo. It also takes the figure's own line colour; the angles that are not active are the ones
that carry a colour each, from the same palette the other blocks use for several curves. - The exact coordinates of P next to the point:
(√3/2, 1/2)where other tools print
(0.866, 0.5). Only for the active angle, and only when it has earned an exact value (see
below); otherwise nothing is written.
Sine, cosine and tangent
The three are always on the plane — for the active angle, which is the one the whole panel
talks about — dotted and each in its own colour: sine purple, cosine blue, tangent green. The
panel's three toggles promote them from dotted to solid and add their construction: the sine's
guide to the axis, and for the tangent the auxiliary line x = 1 and the point S where it meets the
terminal side. They are independent, so you can have none, one, two or all three, and a block opens
with none of them promoted unless it names one (see above).
The three tones are the plugin's own blue, green and purple — the same ones the other blocks give
to several curves, not a new family — and the suite checks that the three stay distinct in both
themes. The names sin, cos and tan in the panel carry the colour of their component, so the
table reads as the plane's legend.
The tangent is joined to the terminal side. Where the join starts depends on the sign of the
cosine, because the tangent is built on the terminal side's line, not on its ray: with cos θ > 0
it runs from P outward, and with cos θ < 0 the line meets x = 1 on the opposite side, so it runs
from the origin.
At 90° and 270° the drawing shows why the tangent does not exist. There the terminal side is
the Y axis, parallel to x = 1, so it never meets it: only that prolongation is drawn, running off
the plane. No segment over x = 1, which would be infinite, and no point S, which has nowhere to be.
Exact values
All 24 notable angles — the multiples of 15° — carry their six ratios in exact form, √3/2 and
(√6−√2)/4 included. On the four angles that land on an axis two of the six do not exist, and
those read "undefined" rather than a number.
They are written in plain unicode, on the plane and in the panel alike: the panel is rebuilt on
every frame of a drag, and putting six KaTeX formulas through that would be untenable — at this
size the unicode reads just as well. The only formula the block renders as maths is the fixed
x² + y² = 1 at the top of the panel.
The right to an exact value comes from the written text, not from the number. A block earns it
by naming the angle in degrees or in terms of π — 30°, \frac{\pi}{6}, 2\pi. 0.5236 never
claims to be sine 1/2, however close it passes to π/6, and neither does θ = 30, which is a
perfectly ordinary angle of 30 radians with no closed form.
An angle produced by the block's own controls earns the right too, because its provenance is
known: drag it, step it with the keyboard, move the slider or run the animation and that angle may
show exact values from then on. It will only actually show them when it lands on a multiple of 15°
— which the magnet, Page Up/Page Down and Home do exactly. A decimal typed by hand never
earns it.
The panel
Everything in it describes the active angle. Three bands:
x² + y² = 1, fixed, with the live point below it:P(30°) = (√3/2, 1/2). The law that
defines the figure stays put while the point that satisfies it moves. The angle is written in
the unit the chip selects, so it readsP(π/6 rad)orP(33.33 gon)just the same.- The reading. With exactly one component selected, that ratio alone and large, headed by its
name in its own colour; with none or with several, the three ratios in a table. Each one shows
its exact form with the decimal beside it, or just the decimal when there is no exact form. - The controls: the three component toggles, the live value of θ, and the angle slider.
Below 520 px of block width the panel stops being a column: it floats over a square plane and
starts closed. The controls are the one part that does not go with it — they move to a fixed strip
a...
1.3.1
Version 1.3.1
This release touches three areas: the explicit tracer's geometry budget, the algebra and
notation of the panels, and the ⓘ information panel, which now describes polar curves,
parametric curves, integrals and derivatives on their own terms instead of with categories
borrowed from y = f(x).
The common problem behind all three is the same: results that were correct but not usable.
The tracer spent a correct budget in an order that made two passes disagree; the panel printed
values that were exactly right and unreadable (7.0710678118654755 for √50, 1.5708 for
π/2); and the ⓘ panel answered cartesian questions about objects that are not cartesian curves
— it told you where the integrand of ∫₀² 2x dx crossed the y axis, which is a fact about a line,
not about the integral the block is there to compute.
What a user notices: less flicker when releasing a drag on dense curves, expressions written the
way a person writes them, and an information panel that describes the object in front of them.
Two changes affect existing notes — one alters what a block draws, the other raises the minimum
Obsidian version. See Breaking changes.
What changed?
Graphics engine and tracer
Before. 1.2.9 capped how much geometry the explicit tracer may emit, so tan(e^x) could no
longer take Obsidian down. The ceiling was right; its distribution was not. It was a single
global bag of anchoPx × 2048 vertices, spent in sweep order, left to right.
Now. Each pixel column carries its own quota. The bar per column is the same 2048, so the
total ceiling does not rise — density in one place can no longer borrow from another.
Why the old scheme was visible. The point at which the bag ran dry depended on how many base
samples a pass had, and the interactive pass and the final pass do not have the same number. They
exhausted it at different values of x and truncated different branches, which is flicker arriving
exactly when you release the mouse. Measured over the canvas:
| view | pixels changed between gesture and final image |
|---|---|
tan(x²) at ±300 |
14.2 % |
tan(x²) at ±200 |
3.5 % |
tan(e^x) default |
9.9 % |
(Those three are measurements of the old code, taken while the defect existed. They are not
reproducible from this build for the obvious reason, and are quoted as the reason the change was
made, not as evidence of its result — for that, see the pixel maps under Validation.)
In the last one the gesture pass drew more than the final one — detail appeared while dragging
and vanished on release, the opposite of what a final pass is for. Views where the budget never
bites, which is the whole ordinary repertoire, were unaffected then and are unaffected now.
A fairer split of the same ceiling also does less work, which is not obvious. The old bag
was genuinely being spent to the last vertex: in tan(e^x)'s default view, 768 px × 2048 is
1 572 864 and the tracer emitted 1 573 289. With a per-column quota only the dense columns
hit the bar and the sparse ones never spend what they do not need: 521 914 vertices, 67 %
fewer, for a nearly identical drawing. It also bites locally where a global bag noticed nothing:
tan(x²) at ±120 was nowhere near the old ceiling and still emits 9 % fewer vertices while
painting exactly the same pixels — that refinement was sub-pixel work inside crowded columns.
Two of the thirty-four bench cases draw differently, both in the pathological zone.
Everywhere else the pixel map is identical, hash for hash, for less work.
| case | pixels | branches | asymptotes |
|---|---|---|---|
tan(x²) ±300 |
129 861 → 136 300 (+5 %) | 11 914 → 10 770 | — |
tan(e^x) default |
72 313 → 91 908 (+27 %) | 23 740 → 18 228 | 23 734 → 6 385 |
The asymptote drop deserves stating plainly: agotado decides not only how much geometry is
emitted but also whether a pole is recorded as an asymptote and whether a continuous crossing is
joined instead of split, so stopping earlier in a column registers fewer. In that view they were
31 per pixel of width and are now 8 — still denser than the screen can separate, but whether
this is visible has not been verified, and the asymptote overlay draws outside the saturated
band as well as inside it. This is the one open question in the release.
Algebra and symbolic simplification
Exact constants recovered from decimals
Before. Recovering an exact constant scanned a table of √k for k = 2…40. Inside the
table it worked; one step outside, the decimal went to the screen. √50, √72, √300, √20+√5
(which is √45) and every cube root that has ever existed were outside it.
Now. Arithmetic instead of a catalogue: square the number and see whether an integer comes
back. That covers all k rather than the first forty, extends to cube roots for free, and costs
one multiplication.
Rationalisation limited to fractions people write
Before. simplify turns every decimal into an exact fraction with a factory denominator
limit of 10 000. A four-digit denominator is the decimal expansion wearing a hat.
Now. Lowered to 64. Everything genuinely written by hand survives — 0.5→1/2,
2.5→5/2, 0.125→1/8, 1.75→7/4, 0.0625→1/16 — and 0.5637 is left as typed.
| you write | before | now |
|---|---|---|
√20 |
√20 |
2√5 |
√50 |
7.0710678118654755 |
5√2 |
√20+√5 |
6.708203932499369 |
3√5 |
∛54 |
3.7797631496846193 |
3∛2 |
√(20x) |
√(20x) |
2√(5x) |
x^{0.5637} |
\sqrt[10000]{x^{5637}} |
x^{0.5637} |
(x^{0.5637})² |
\sqrt[5000]{x^{5637}} |
x^{1.1274} |
√2/2 |
1/√2 |
√2/2 |
That last row ran backwards: the reciprocal was returned unrationalised, so writing the
correct form produced the incorrect one and Simplify stopped being a no-op on its own output.
Pulling a square factor out of a radical is exact and does not move the domain (20x ≥ 0 and
5x ≥ 0 are the same condition), so no curve changes. √(2)^√2 correctly stays a decimal — it
is not a·ⁿ√b in any form, and the recogniser says so instead of guessing.
Radicals are drawn only when they read better than the power
Before. The emitter's rule was "a rational exponent becomes a radical", with no ceiling on
the index; the rationalisation limit above was doing all the work. Writing the fraction
yourself still produced x^{5/64} → \sqrt[64]{x^{5}} and x^{7/32} → \sqrt[32]{x^{7}}.
Now. Two ceilings, applied to the radical that would actually be painted:
- index ≤ 5 with a power inside;
- index ≤ 8 for a pure root, which has no exponent to read on top —
\sqrt[8]{x}is fine,
\sqrt[8]{x^{7}}is worse thanx^{7/8}and is exactly the trade being avoided.
The 5 rather than a rounder 4 is a deliberate trade-off: y^{2.5} = x ⇒ ⁵√(x²) is a case this
same release adds, and a ceiling of 4 silently removed it. Where the readability argument cannot
separate two options, not regressing an existing decision wins.
The purpose of a ceiling is stability: under the old rule an expression's appearance depended
on whether the rationaliser happened to find a fraction, which is an internal detail.
Irrational exponents are also left alone now. x^{π/2} used to be painted \sqrt{x^{π}}, which
is true and not what anyone writes: the radical is the canonical notation for a rational
exponent, where p/q means "q-th root of the p-th power", and the /2 in π/2 is ordinary
division, not an index. Same for φ/2, e/3 and τ/4. Consistency is preserved in the other
direction: x^{0.5φ} and x^{φ/2} still render identically.
Radicals finish the job
Before. Converting a rational power to a radical was the only rewrite. \sqrt[4]{x^{11}} is
correct and nobody writes it.
Now. The Euclidean division m = q·k + r pulls every perfect power out of the radicand, and
a negative rational exponent becomes the reciprocal of the radical.
| you write | before | now |
|---|---|---|
x^{3/2} |
\sqrt{x^{3}} |
x\sqrt{x} |
x^{7/2} |
\sqrt{x^{7}} |
x^{3}\sqrt{x} |
x^{11/4} |
\sqrt[4]{x^{11}} |
x^{2}\sqrt[4]{x^{3}} |
x^{5/4} |
\sqrt[4]{x^{5}} |
x\sqrt[4]{x} |
(2*x)^(5/2) † |
4\sqrt{2}x^{2}\sqrt{x} |
4x^{2}\sqrt{2x} |
(3*x)^(7/2) † |
27\sqrt{3}x^{3}\sqrt{x} |
27x^{3}\sqrt{3x} |
x^{-1/2} |
x^{\frac{-1}{2}} |
\frac{1}{\sqrt{x}} |
x^{-5/2} |
x^{\frac{-5}{2}} |
\frac{1}{x^{2}\sqrt{x}} |
† The two composite-base rows hold for that spelling only; written (2x)^{5/2} with braces the
panel shows 4\sqrt{2x^{5}}. The reason, and what it would take to close it, are at the end of
this section.
Written as a decimal the result is identical: x^{1.5} and x^{3/2} are the same function and
now look it. A negative integer exponent stays a power, because x^{-2} is already how that
is written.
Composite bases stay composite. simplify distributes a power over a product, turning
(2x)^{5/2} into 4·2^{1/2}·x^{5/2}, and the panel painted 4√2·x²√x — two loose radicals
where one is written by hand. It also contradicted the plugin's own convention, since √(20x)
is left as 2√(5x). Reaching 4x²√(2x) needed no new machinery, only the right order: the
Euclidean extraction moved from the emitted LaTeX to the tree, so splitting x^{5/2} into
x²·x^{1/2} as nodes leaves that x^{1/2} visible to a second pass, which merges radicals of
equal exponent and finds 2^{1/2} beside it. On the finished string that √x was text and
could not be touched.
Merging radicands is guarded, not free. √a·√b = √(ab) fails when both are negative
(√(−1)·√(−1) is NaN while √1 is 1). One radicand being provably non-negative is enough: then
`a...
1.3.0
1.3.0
The block on a phone: the plane gets the whole width, the formula waits behind a button
Until now the block applied a desktop layout everywhere. The formula asks for half the
width and the plane takes two thirds of what is left, which on a 412 px phone leaves a
plane 214 px wide and 261 px tall — a graph taller than it is wide, with a panel beside
it whose lower half is empty. Nothing was broken; it was a proportion designed for a
window that a phone does not have.
This release splits that decision in two, because two different things were being
confused: how much room there is, and how you are pointing. They now travel separately,
and that is what lets the phone gain a usable plane without changing anything in
landscape, where a phone has ~700 px and the desktop layout already works.
-
A narrow block is now just the plane, with the formula in a panel on top of it.
Below 520 px of container width, the formula panel leaves the flow and becomes a card
floating over the plane, opened and closed by a new f(x) button next to the ⓘ. The
plane goes from 214 × 261 to 321 × 264 on that same phone — 52 % more area, and
landscape at last.The threshold is not a round number picked by feel. In the side-by-side layout the plane
takes ⅔ of the block, so for it not to end up taller than wide you need ⅔·W ≥ 261, that
is W ≥ 392; at 520 the plane never drops below 4:3, which is the least a graph needs to
read as a graph. Being measured on the container and not on the device, a narrow side
pane on the desktop gets the same treatment — it has exactly the same problem.The panel does not change parent when the threshold is crossed: it stays a sibling of
the plane and only its box is rewritten. So rotating the phone is one style write —
KaTeX is not re-rendered, and your zoom and panning survive the turn. Rotating with the
formula open closes it, so coming back to portrait starts closed rather than with a panel
nobody asked to open.The card takes 180 of the 264 px and rests 46 px above the bottom edge instead of
sitting on it: the ⓘ and the button that closes the formula live down there, and a panel
that covers its own close button is a trap. While it is open the f(x) turns into ✕,
the same rule the ☰ menu got in 1.2.8, and the zoom column steps aside — at that height
it would be underneath the card. That is the price of a large panel, and it is the right
one: with the formula in front of you, you are not navigating the graph. Tapping the
plane also closes it, but only a clean tap — under 8 px of travel and half a second —
because a drag to move the view ends up emitting a click just like a tap, and closing the
panel every time you moved the plane would make it unusable. -
One finger moves the plane on both axes; two fingers zoom. Until now the only zoom on
a phone was the ± buttons, which are precisely the ones that step aside when the formula
is open. Panning was also fragile: a single stored position meant the second finger's
pointerdownoverwrote the first one's, and the view jumped from one finger to the
other.Pointers are now tracked per id. With one, the arithmetic is the same one as always,
so the desktop drag is unchanged. With two, the midpoint drags the view and the
separation scales it, anchored on that midpoint: pulling the fingers apart zooms in,
bringing them together zooms out, and the point of the world under your fingers stays
where it is. Lifting one finger no longer jumps, because the one that remains already has
a known position.pointercancelis finally handled — the system can take a finger away
at any moment (an incoming call, a gesture of its own), and that finger used to stay
"down" forever.The wheel and the pinch now share the same anchored-scale routine, so there is one
invariant to keep and one to test. Two guards: below 24 px of separation the gesture
only pans (the ratio explodes, and divides by zero in the limit), and the scale per event
is capped at ×4.This has a consequence worth knowing: a swipe that starts on the plane no longer
scrolls the note.touch-actionis set on the canvas only, so swipes that start on the
margins, above, below, or on the formula panel — 180 of those 264 px when it is open —
still scroll normally. -
Nothing that needs a mouse pretends to work with a finger. On touch there is no
hover, so the crosshair and the cursor cross had nothing to follow, and the rail is
driven with A/D and W/S on a keyboard that is not there. All three are now off, along
with the ⌖ button that opened a mode with no way to steer it, and the canvas no longer
hides the system cursor. Controls that stay grow from 22 to 30 px — not the 44 that
the touch guidelines ask for, because four 44 px targets over a 321 px plane would take a
third of its height and bring back the problem we came to solve. The ⓘ popover now rises
with the chip row and is capped against the plane instead of against fixed numbers, and
the popover and the formula are mutually exclusive: opening one closes the other, since
on a phone they overlap almost completely.What is lost with this: on a phone there is now no way to read the coordinates of a
point. The crosshair and the rail were the two ways to do it, and both were mouse and
keyboard. Nothing has replaced them yet; a tap that shows coordinates is the natural
candidate. A tablet with a Bluetooth keyboard also loses keyboard navigation — there is
no way to detect that keyboard until a key is pressed. -
A button to edit the block, because on mobile there is no
</>. Obsidian's button
for reaching the source of a rendered block appears on hover, so on a phone it never
does, and our canvas takes the taps. The block was left with no door to its own code.
There is now a ✎ chip in the top-left corner — away from the others on purpose: the ones
on the top right move the view, the ones at the bottom right open something inside the
block, and this one leaves the block. It takes the cursor to the end of the block's
body, not the start, because you press edit to carry on writing; if the note is in
reading mode it switches to editing first, and it scrolls the cursor into view, which on
a phone matters because the keyboard covers the lower half of the screen. It hides while
the formula panel is open, where it would only add noise. -
The block no longer flashes while it mounts. The formula panel goes through
MarkdownRenderer, and that has to be awaited before the plane can be built. In that gap
the browser painted whatever was there — the block with its formula and no graph at
all — and then jumped to the finished block. On the desktop it lasted an instant; on a
phone, leaving the editor, it was plainly visible. The block now mounts hidden and is
revealed once the layout is decided, the canvas sized, and the geometry traced and
painted. It is hidden withvisibilityand notdisplay:noneon purpose: it has to keep
occupying its place and measuring for real, because the layout is decided from
clientWidthand the canvas is sized from its real box. There is a two-second safety net
so that a failure halfway can never leave a block invisible.
The graphing engine itself is untouched: no sampler, tracer or analysis code changed, and
the geometry of every existing case is the same as in 1.2.9. Both suites pass, now with
nine new tests for the gestures — 354 tests in the main suite and 12 in the zoom
suite. Those nine check the properties that are easy to break without noticing: that the
world point under the anchor does not move when pinching (with the anchor deliberately off
centre, where a wrong anchor would go unnoticed), that one pointer still pans exactly as
before, that lifting a finger does not jump, and that a cancelled pointer stops counting.
Verified on Android. The behaviour on iOS has not been tested, and neither has a
tablet.
1.2.9
1.2.9
A block that froze Obsidian past restarting it, and a plugin that finally has a light theme
One serious bug fix, one long-standing design gap, and two smaller fixes. The
serious one is a graph that could lock the app hard enough to survive quitting
it: reopening the note re-rendered the block and froze it again, so the only way
out was to delete the block with an external editor. The design gap is the light
theme: until now the block was a dark island on a white page, with a formula that
was nearly invisible in it.
-
A block containing
tan(e^x)froze Obsidian, and reopening the app froze it
again. This bug was found during testing on a Redmagic 11S Pro and later
reproduced on Windows. The cause is arithmetic, not hardware: the plane's very
first paint runs the final, highest-quality pass, and on this function that
pass generated pathological amounts of work and memory, making the issue
algorithmic rather than hardware-dependent, regardless of the device's
performance. Two independent defects met on this one expression. First, the
explicit sampler had no bound on how much geometry it could produce:
tan(e^x)oscillates at a local frequency ofe^x/π— about 1.5·10⁷
oscillations per pixel at the right edge of the default view — and the
sampler kept subdividing, ending with 1,084,444 branches and 21,460,279
vertices after 30.3 seconds and 1.14 GB of heap. That case is deliberately
not covered by the high-frequency envelope added in 1.2.6, whose
boundedness test rejects it on purpose: an unbounded function has no band
to draw, it has asymptotes, and it was handed back to the sampler with no
limit at all. Second, the deduplication of notable points was quadratic, so on
that geometry it never finished: over 9 minutes and still running when it
was killed, all of it wasted, since a category with more than 30 points is
discarded whole.The sampler now enforces a refinement budget tied to resolution —
2048 vertices per pixel column, so a phone screen protects itself
proportionally — and when it runs out the trace does not stop: it stops
subdividing and continues at the base sampling density, which is already
bounded, so the curve is still drawn end to end without the sub-pixel detail
that resolves nothing at that scale. The deduplication now indexes points in a
grid whose cell equals the tolerance, making it linear while returning exactly
what the exhaustive scan returned. The first render oftan(e^x)goes from
30 s, 1.14 GB and a pass that never finished to 705 ms and 112 MB
(378 ms on a phone-sized canvas).Nothing else changes: the budget was calibrated against the existing
repertoire. The most expensive legitimate case,tan(x²)at ±300, uses
699 vertices per pixel column, leaving roughly a threefold safety margin,
and 240 of 240 traced cases (20 expressions × 6 zoom levels × both
passes) come out bit-identical to 1.2.8, vertex for vertex, with
identical notable points. Where the drawing does change is inside the
pathological stretch itself: past x ≈ 10.3 the old picture was a saturated
black rectangle, every pixel of every column painted, and it is now a lighter
haze over the same region, without a single pixel drawn that was not drawn
before. What is lost there is the density of an aliasing smear, not the
curve itself. -
The plugin follows your theme, and the formula is legible in a light one.
The block painted itself#1e1e1eand never set a text colour, so the formula
inherited--text-normalfrom the theme: in a dark theme that happens to be
light text on a dark panel and it worked by accident, and in a light theme it
was near-black text on a near-black panel — around 2:1 of contrast, where
4.5:1 is the minimum for legible text. That part was a defect, not a
preference.The fix splits colour in two along the line that matters. The frame —
container, formula panel, buttons, menus, borders — no longer has a colour of
its own: it derives from Obsidian's own variables (--background-primary,
--background-secondary,--text-normal,--background-modifier-border,
--shadow-s), so the block is made of the same material as the note and works
with any theme, including community ones, with nothing for us to maintain. The
plot's ink — grid, axes, labels, curves, markers — keeps a palette of its
own in two hand-tuned versions, because a graph needs guaranteed contrast
between its layers and no theme variable promises that. The only thing the
theme is asked is whether it is light or dark.The light palette is not the dark one inverted. On white the blue
#4f9eff
washes out and drops to#2f6df6; the grid goes from light grey at 12 % to
black at 10 %; the axes gain weight, because a faint grey disappears against
white; and the white halo behind each marker — which separates it from the
curve on a dark ground — turns dark, since on white a white halo separates
nothing. Every layer was measured against its own background: axis labels sit
at 5.03:1 in dark and 5.67:1 in light, and the six curve colours at 5.50–9.30
and 4.38–5.78 respectively. The six hues are the same in both themes — they are
the plugin's identity — only darkened for a light ground.Switching theme now recolours an open block in place. Curve colours are
declared by role (which equation they belong to) and resolved against the
active palette when painting rather than when the scene is built, so a theme
change is a repaint: your zoom and panning survive it. -
The same function no longer renders two different ways depending on how you
typed it. Fractional powers were already drawn as radicals —x^{1/2}as
√x,x^{2/3}as∛(x²)— but that rewrite only recognised an exponent
written as a quotient of literal integers, sox^{0.5}came out as
x^{1/2}in fraction form andx^{0.5φ}asx^{φ/2}. The decimal becomes a
fraction after the step that does the rewriting, andφis not a digit.
The decision now happens in the LaTeX writer, where every form passes through:
x^{0.5}andx^{1/2}both render√x,x^{1.5}matchesx^{3/2}, and
x^{0.5φ}renders√(x^φ). Exponents with a free variable keep their
exponential form (e^{x/2}is not a root), and so do negative ones. -
The options button now closes the menu it opens. The ☰ icon stayed the
same whether the transformations menu was open or closed, so the button gave
no sign of what pressing it would do. It now turns into ✕ while the menu is
open, with its tooltip changing to match, and back to ☰ when it closes,
including when the menu closes by clicking outside it or by applying one of
its options. The three blocks that have this menu (obs-graph/
obs-system,obs-derivateandobs-integral) all follow the same rule,
each keeping its own description of what the menu contains.
Both suites pass unchanged: 345 tests in the main suite and 12 tests in
the zoom suite.
1.2.8
1.2.8
Graph controls: an icon set, one tooltip per control, and a crosshair that survives panning sin(1/x)
Most of this release is cosmetic — a real icon set for the plane's controls, a
single tooltip per control instead of two, zoom buttons that repeat while held,
and the removal of a decorative marker — and it carries two bug fixes: curves
now close against the edge of their domain instead of stopping short of it, and
the crosshair on sin(1/x) no longer goes dark after you pan the view.
-
Curves stopped short of the edge of their domain, leaving a visible gap
there. Reported on the systemy = ±⁴√(1−x⁴)— the squirclex⁴+y⁴=1—
which at high zoom showed a hole at (±1, 0) with the stroke cut in two, so the
near-vertical tip read as a dashed asymptote rather than a curve. Where a curve
meets the end of its domain with a vertical tangent, the value falls off as a
root of the distance to the edge: for⁴√(1−x⁴)it isy ≈ (4ε)^(1/4), which
descends so slowly that after the sampler'sPROF_MAXrefinement steps y is
still ≈7·10⁻³ — tens of pixels once you zoom in — and the branch simply ended
there. It closed only by luck, when the edge happened to land exactly on the
sampling grid, which is why the gap appeared at some zoom levels and not
others, and why it was never a regression: the defect had been latent since
long before 1.2.6. The sampler already located the edge to machine precision —
it bisects 40 times to decide whether the discontinuity is a pole — but threw
that point away instead of drawing it. It is now emitted, attached to the
finite end and inside the same segment, so the polyline reaches the edge: the
gap goes from 6.6·10⁻³ (2.5·10⁻² during a gesture) to zero, both passes agree,
and it stays under a pixel at any zoom. This applies to every explicit curve
that touches a domain boundary —√x, half-parabolas, and the rest — all of
which had the same gap, just smaller than the fourth root made it. -
The crosshair on
sin(1/x)stopped appearing after a pan — a regression
from 1.2.6, unnoticed until now and fixed here. 1.2.6 began drawing
stretches that oscillate faster than one pixel as a min/max envelope band,
markedCalidadRama: "incierta"and carrying noparametro, since a band is
not a curve you can walk along. The walkability test that gates the crosshair,
curvaRecorrible, rejects any branch without aparametro— its purpose is to
catch curves that fold back in x and are therefore multivalued. A band trips
that test even though it is not a fold, so the presence of a single band
disabled the crosshair for the whole curve. It surfaced onsin(1/x)
specifically, and only after panning: the band near x=0 forms or not depending
on how the sampling grid lands on the pixels, which shifts as the view moves,
so a settled default view resolved cleanly while a panned one did not — and a
wheel zoom, which resolves the oscillation as it magnifies, never triggered it
either. The test now excludes"incierta"bands from its judgement, exactly as
the crosshair's ownyEnRamasalready skips them: the crosshair works over the
rest of the curve and is simply not drawn over the band itself, where there is
no single y to report. Before 1.2.6 the curve was traced as ordinary branches
throughout and was always walkable, so this restores the earlier behavior. -
The plane's controls now use a real icon set. Home, zoom-in, zoom-out,
trace, info and the options menu were text glyphs and emoji (🏠, +, −, ⌖, ⓘ,
☰), and the pointer over the plane was a hand-drawn cross. All are now Material
Symbols, drawn as inline SVG through Obsidian's DOM API rather than
innerHTML, and filled withcurrentColorso each icon inherits its button's
colour and follows the active/inactive highlight unchanged. The pointer icon is
drawn on the canvas as aPath2D, built lazily so it costs nothing in the Node
test bundle, which never paints. -
The zoom buttons repeat while held. Pressing + or − zoomed a single notch
and had to be clicked again for every further step. They now keep zooming for
as long as the button is held, at a steady cadence, reusing the same centred,
smoothed zoom a single press already used — so holding reads as one continuous
zoom rather than a stack of steps. A quick tap still does exactly one notch. -
One tooltip per control, above it rather than below. The options button
carried both atitleattribute and anaria-label, so hovering it produced
two tooltips at once — the browser's native one and Obsidian's. Every control
now uses Obsidian'ssetTooltipwith top placement and notitle, so there is
a single dark tooltip, positioned above the control where the pointer does not
cover it. -
The experimental-engine ⚙ marker is gone. The small gear in the top-right
corner of the plane was a decorative label; it has been removed along with its
now-unused translation strings, and the zoom buttons moved up to take its
place.
Both suites pass unchanged — 345 tests in the main suite and 12 in the zoom
suite.
1.2.7
1.2.7
No generated code: the expression compiler no longer uses new Function
A maintenance release with a single change. 1.2.6 introduced a native
expression compiler that made tracing 2.3× to 18× faster by generating
JavaScript source at runtime and compiling it with the Function
constructor. That is dynamic code execution: it requires unsafe-eval in the
Content Security Policy, and it means the plugin's behavior cannot be
determined by reading the plugin's code, since part of what runs is a string
assembled while it runs. This release removes it without giving up the speed.
-
Expressions are now compiled to a tree of closures instead of to source
code. The compiler walks the same mathjs AST as before, but each node
becomes a nested JavaScript function that has already resolved — closed over
— which operation it performs and what its operands are. Evaluating means
descending that tree calling ordinary functions; there is no typed-function
dispatch, no scope object and no name lookup left in the sampling loop, which
is where the original speedup came from. This is closure compilation: the
work that depends only on the expression is done once, and only the work
that depends on the point remains per sample. No string is ever turned into
a program, soevaland theFunctionconstructor are both gone from the
plugin, andunsafe-evalis no longer needed.Compiled closures take a fixed two-argument signature even when the
expression has one variable. That is deliberate: it avoids allocating an
argument array on every evaluation and keeps the call sites inside the tree
monomorphic, which is most of the difference between a closure tree that is
fast and one that is not.Closures are somewhat slower than generated source on large expressions, and
measuring that honestly matters more than the headline. Against the 1.2.6
implementation, over 300,000 evaluations per expression:sin x12.7 ms →
11.8,x³−3x+1/x14.0 → 15.1,(x²+y²−1)³−x²y³19.1 → 17.9,
sin(1/x)·e^(−x²)+ln(|x|+1)16.2 → 39.0,sin(xy)+cos x/(1+y²)29.3 →
63.1. So the worst case measured gives up a factor of 2.4 against generated
code — while still evaluating 8.4× to 26.8× faster than mathjs, which is the
comparison that decides frame time, since mathjs takes 222 ms to 530 ms on
those same runs. The tracing speedup of 1.2.6 therefore stands.The three safeguards are unchanged and still apply: a whitelist that refuses
anything whose semantics have not been verified against mathjs, a
differential validation of the compiled function against mathjs over ~40
probe points before it is used, and a fallback to the mathjs path whenever
either fails.
No behavior changes: both suites pass unmodified — 345 tests in the main suite
and 12 in the zoom suite — which between them cover the tracing geometry the
compiler feeds.
1.2.6
1.2.6
Graphing engine: speed, and fewer curves that change while you move the view
This release is about the graphing engine. It makes evaluation 2.3× to 18×
faster, addresses three visual defects reported while panning and zooming, and
bounds the curvature of the parametric and implicit tracers, which were drawing
smooth curves as visible polygons. It also unlocks the explicit sampler, which a
test had frozen against a dead copy of itself.
Two of those three defects are fixed outright; the third, the flicker on
sin(1/x), is greatly reduced rather than removed. A fourth — the moving
stripes on tan(x²) — is diagnosed but not fixed, and is described at the end.
-
Expressions are compiled to native JavaScript: 2.3×–18× faster tracing.
mathjs remains the parser — its AST is still the source of truth for the
syntax — but it is no longer the evaluator.mathjs.compile().evaluate(scope)
pays typed-function dispatch and scope construction on every sample, and a
trace takes between 2,500 and 220,000 evaluations, so that wrapper was the
bulk of the frame rather than the mathematics.src/compiladorNativo.tswalks
the AST and generates the equivalent JS, compiled once withnew Function.
Measured over the complete tracer:√(9−x²)12.5 ms → 1.0 ms,ln|x|
12.6 → 1.0,sin x13.2 → 1.3,tan xat zoom-out 333.7 → 52.5,
sin(1/x)deep-zoomed 585.8 → 85.6. Geometry came out bit-identical — same
branches, same vertices — in all 160 comparisons made (8 curves × 10 zoom
levels × both passes); that is a broad check, not a proof for every possible
expression, which is what the safeguards below are for. Three of them keep an
acceleration from ever becoming a change of drawing: a whitelist that refuses
to generate code for
any node whose semantics have not been verified against mathjs; a differential
validation of the generated function against mathjs over ~40 probe points
before it is used; and a fallback to the old path when either fails. Note that
mathjs 12 does not useevalornew Function, so this is a genuinely new
capability the plugin exercises where the environment allows it. Degradation
is clean by construction: the call sits inside atry, so a Content Security
Policy that blocks it returnsnull, every expression falls back to mathjs,
and the engine behaves exactly as before — verified by sabotaging the
Functionconstructor and re-rendering the curves in the test repertoire. -
tan(y) = xdrew short spurious strokes across its branches. The
separable-implicit provider rescues thin slivers next to each pole, because
regular sampling can miss a branch that only exists within a fraction of a
pixel. That rescue ran unconditionally, so where the ordinary sampling had
already covered a pole it added a second, much coarser trace of the same
place: 8 extra branches of 6 or 7 points each, which is what showed up as
marks over the curve. The rescue now consults what was already traced and
skips poles that are covered, using a sorted index of the visible x-values and
a binary search rather than a scan per pole. Real slivers are still rescued —
there is a regression test for a pole where sampling genuinely misses the
branch. -
sin(1/x)flickers far less when zooming — reduced, not eliminated.
Where a curve oscillates faster than one pixel, no sampling density resolves
it: several whole cycles
fall between consecutive samples, so what gets drawn is an arbitrary subset
of a dense band — and which subset depends on the number of samples. Since the
interactive pass and the final pass use different densities, each picked
different threads and the curve changed when the gesture ended: measured, 122
branches interactive against 38 final, with 31% of pixels differing. Those
stretches are now detected and drawn as the min/max envelope per pixel column,
on a grid pinned to the pixels and with a fixed number of samples per column,
so both passes compute exactly the same thing. Flicker went from 30.9% to 0.5%
on average, with a 6.6% peak — a large reduction, but not zero: some change
between passes remains, and the surrounding curve is still sampled the usual
way. For scale,x²measures 14.3% on the same metric purely from rasterising
a curve that legitimately moves, so the residue sits below that floor. The
deep-zoom case also got 6.8× faster (586 ms → 86 ms) because the sampler stops
trying to resolve the unresolvable. These bands are marked
CalidadRama: "incierta"— the contract value that had never been emitted —
and carry noparametro, since a band is not a curve you can walk along.
Telling a pole from an oscillation is what made this hard: inside the
column holding a pole, the function rises, jumps and rises again, which reads
as the same signature as a cycle. Neither a threshold on turning points nor
the shape of the sign change is reliable; what works is the combination of
four or more turning points with the local and global max/median ratio,
since unboundedness is a property of the function rather than of the sampling. -
Smooth parametric and polar curves were drawn as polygons. Reported on
r = sin(θ/10): faceted while zooming, smooth once stopped. Refinement used
only a deviation test — the distance from the midpoint to the chord — and
deviation is a sagitta, which scales with the square of the chord: on an arc
of screen radius R a turn of θ leaves a sagitta of only R(1−cos(θ/2)), so with
the curve small on screen a vertex can turn 36° and still pass a 1-pixel
threshold. Measured, the turn angles came out quantised at 36/18/9/4.5° —
exact powers of two, that is, the uniform-sampling polyline never refined once
— with a mean turn per vertex of 19.7° interactive and 9.9° final. A curvature
criterion now bounds the turn itself, and the worst facet measured is 3.93°
across the curves tested — a polar rose, a cardioid, a circle, a Lissajous
figure, a spiral and a parabola, each at six zoom levels in both passes — at a
cost of at most 2.5 ms. Two details matter: the measured angle is doubled
before comparison, because an accepted segment
discards its midpoint and emits the whole chord, so the angle actually drawn
at the joint is about twice the one measured; and the criterion switches off
below a 1.5-pixel chord, where a kink fits inside the stroke width and where
chasing curvature would never terminate at a genuine cusp such as the r=0 of a
rose. -
The same blind spot in implicit curves. The continuation tracer advanced
with a fixed arc-length step — 4.5 px interactive, 2.5 px final — and its
existing turn test is a validity check at ~45°, which bounds nothing visible.
Measured turn per vertex during a gesture: 17.7° on the foliumx³+y³=3xy,
7.8° on an ellipse, 6.5° on a circle. A smoothness criterion now reuses the
step-halving loop that was already there, bringing those to 3.9°, 3.7° and
3.8°, with no vertex above 5° on those three. It does not bound every curve:
x²y²=1zoomed out still leaves 6.9° at two vertices during a gesture, where
the shorter steps are rejected by the chord test and the fallback below is
what gets used. It cannot make things worse: the first valid step is kept as
a fallback and returned when no scale manages to be smooth, so
the function never returnsnullwhere it previously returned a step, which
would have triggered spurious straight-line crossings or cut branches short.
Explicit curves were checked too and needed nothing — they sample per pixel
column, so their turns were already under 2°. -
Parametric curves were drawn as a single straight line where they left the
view. On crossing the visibility boundary the tracer bisected to find the
edge and emitted only that point, so the whole visible arc leading up to it
became one chord. It bites as soon as zoom leaves the visible portion inside a
single step of the initial sampling int. Measured onr = sin(θ/10)at
semiY=0.005: 7 points and 37.6 px of deviation interactive, against 519 points
and 0.07 px final. The visible arc is now refined with the normal logic:
515 points and 0.07 px in both passes, and the deeper zooms where both
passes were broken are fixed as well. -
The explicit sampler was frozen by a parity test against a dead copy of
itself.tests/modulos/trazado.test.tsrequired vertex-by-vertex equality
withsrc/render/muestreoExplicito.ts— the legacy GraphEngine sampler, which
no longer draws anything and which the two copies had already outgrown. That
made the legacy code the definition of correct, and broke any improvement to
sampling or refinement by construction, even one that did not move a single
pixel: capping refinement below the pixel failed with "expected 4146 points,
got 4090", a 1.4% difference in vertices all of them sub-pixel. The test now
compares what is observable — same branch count, same vertical asymptotes
measured in pixels, and the two curves within 0.25 px of each other on
screen, as a two-way Hausdorff distance against the segments rather than the
vertices. It still catches genuine regressions, verified by sabotage: nearly
disabling refinement produces 6 failures and cutting the sample count by 8×
produces 22.
Existing behavior is unchanged elsewhere: the full suite is now 345 tests, the
zoom suite 12, and both pass. Every performance figure above was measured with
medians over repeated runs on jittered viewports, so caching is not being timed.
Known and not fixed
Functions whose local frequency grows, such as tan(x²), still show regularly
spaced dark stripes when zoomed out, and the pattern changes between the
interactive and the final pass....
1.2.5
1.2.5
Maintenance
Maintenance release with no user-facing changes. It reorganises the test
suite, which had grown into a single 4838-line file, and restores the type
check, which had been silently disabled by an invalid compiler option — closing
the two type holes it had been hiding. No graphing, equation-solving,
derivative, integral, or parsing behavior was changed: the shipped main.js is
byte-for-byte identical to the one built before these changes, verified with
a full rebuild and a binary comparison.
-
The test suite is split by topic.
tests/motor.test.tsheld 60 blocks in
one 4838-line file, which made a failing block hard to locate and every edit a
conflict magnet. It is now an index that loads eight modules from
tests/modulos/—trazado,carril,implicitas,despeje,
parser-latex,calculo,parametricas,escena— plus acomun.tswith
the shared viewport, tolerances and helpers. The runner keeps a module-level
pass/fail count, so the modules are imported for their side effects and the
summary is still printed exactly once; the suite remains a single bundle and a
singlenpm run test. Purely a move: the body of every block is unchanged,
and the 334 test names and results are identical to before the split, verified
by diffing the runner output of both versions. -
The type check was not running at all.
tsconfig.jsoncarried
"ignoreDeprecations": "6.0", a value TypeScript 5.9 rejects outright
(error TS5103), sotsc --noEmitaborted on the config before checking a
single file — and since the build is esbuild, which does not type-check, no
type error had been visible for some time. That option was silencing two
options deprecated for removal in TypeScript 7.0, so rather than silence them
the options themselves were retired:baseUrlwas dead weight (there are no
paths, and the only non-relative imports are theobsidianandmathjs
packages), andmoduleResolutionmoved from the deprecatednode(node10)
tobundler, which is what esbuild actually does. With nothing deprecated
left,ignoreDeprecationswas dropped.skipLibCheckwas enabled so that
three pre-existing errors insideobsidian.d.tsitself do not keep the
command red.npx tsc --noEmitnow reports zero errors and is usable as a
gate again, in the editor as well as on the command line. -
FontFaceSet.addwas untyped:"DOM.Iterable"added tolib. The
set-like methods ofFontFaceSetare declared inlib.dom.iterable.d.ts
(interface FontFaceSet extends Set<FontFace>), not inlib.dom.d.ts, so
document.fonts.add(cara)in the font loader resolved to nothing —
error TS2339: Property 'add' does not exist on type 'FontFaceSet', which is
also what ESLint reported asno-unsafe-call. The method exists at runtime, so
font loading always worked; only the types were blind to it. Fixed in the
compiler options rather than at the call site, so the whole DOM surface is
typed consistently. -
explicita()returned a type wider than what it builds. It is annotated
as returning the unionObjetoMatematico, so{ ...f, salida: "x" }— the
tumbled reading of anx(t)parametric component — was checked against every
member of the union and rejected for excess property, even thoughsalidais
declared onObjetoExplicito. The return type is narrowed to
ObjetoExplicito, which is what the function actually constructs. Type-only:
annotations are erased on emit and the bundle is unchanged. -
Known and left alone: the
display()deprecation. Obsidian deprecated
PluginSettingTab.display()in 1.13.0 in favour ofgetSettingDefinitions(),
and the settings tab still calls it. This is deliberate — the manifest declares
minAppVersion: 1.12.7, and the API documentsdisplay()as the fallback for
plugins supporting earlier versions. It will be revisited when the minimum
version moves to 1.13.
Existing behavior is unchanged: the full suite (334 tests), the zoom suite (12)
and the graduated battery (220 generated towers, 0 failures) pass exactly as
before.
1.2.4
1.2.4
Features
Minor release that widens solving for y past the "one occurrence of y"
frontier of 1.2.3, and gives obs-integral the quotient rule it was missing.
The new solver paths are general rewrites over the AST — clear the denominators,
collect the linear coefficient, square away the radicals — not per-equation
cases. Every transformation that is not an equivalence now states its condition
or is rejected: a solved form still never claims a point the curve doesn't have.
And the conditions that survive are now solved as a system instead of listed,
so a domain reads x ≥ \sqrt3 rather than as two raw inequalities. The full test
suite (334 tests), the zoom suite (12) and the graduated battery (220 generated
towers) pass.
-
yno longer has to occur once — it has to be grouped. The structural
inverter peels the outermost layer and recurses into the child holdingy,
which never neededyto be unique: the whole layer is inverted at once, so it
is enough that a single child contains it. When the tower gets stuck —y
split across both branches of an operator — what has been peeled so far is an
equivalent, simpler equation, and it is re-solved from scratch:
\ln\frac{y-1}{y+2}=x → y=\frac{2e^x+1}{1-e^x},
e^{\frac{y-1}{y+2}}=x → y=\frac{2\ln x+1}{1-\ln x},
\ln(y^2+y)=x → y=\frac{-1±\sqrt{4e^x+1}}{2}. -
yin a denominator is cleared and the equation re-solved. No additive
strategy could reach aysitting in a factor of exponent −1 unless the
numerator was free ofy. Multiplying through by the denominators — the first
step of any textbook solution — leaves a polynomial equation the existing
machinery finishes:\frac{y-1}{y+2}=x → y=\frac{2x+1}{-x+1}. -
Equations affine in
ywhose coefficient needs expanding. A sum of terms
each carrying a barey(y\ln x+y=3) already worked; what fell through was
yinside a group that has to be distributed first, when the coefficient is
not polynomial — the linear strategy reads the coefficient off the structure
of the term, soyhas to be a bare factor, and the quadratic path needs
rationalizeto expand, which only works on polynomials. Since an affine
function is fixed by two values,AandBare now recovered by
substituting two values ofy— substitution works with any coefficient, no
expansion needed — and the affinity itself is what gets checked numerically:
y-(y+2)e^x-1=0 → y=\frac{2e^x+1}{1-e^x}(this is the shape the cleared
Möbius above lands on, and it is what makes that case finish). -
Scattered square roots are rationalized by successive squaring, carrying the
domain condition of every step. One isolated root was already invertible, but
two roots — or a root plusyoutside it — leaveyungrouped and no layer
peelable. Isolating one root and squaring, repeated, produces a polynomial the
solver finishes. Squaring is not an equivalence (A=B ⟺ A²=B²and
B≥0), so each step records its guard and all of them travel to the result as
domain conditions, written inxby substituting back what the later steps
determined:\sqrt{y+1}+\sqrt{y-2}=x → y=\frac{x^4+2x^2+9}{4x^2}with
x ≥ \sqrt3(the two raw guards, resolved into one by the condition
simplifier below). Verified point by point: the solved form draws nothing for
x<\sqrt3and has no gap above it. -
Domain conditions are now solved as a system, not listed one by one. Each
restricted-range layer and each squaring step contributes its own guard, and
the panel used to print all of them — correct but unreadable, because they are
inequalities over the samexand nobody was looking at the system as a whole.
A new module resolves each condition by its sign table (the zeros of
numerator and denominator split the line into constant-sign runs) and
intersects the results, which is where redundant conditions vanish on their
own, adjacent runs merge, and contradictions surface as an empty intersection.
So\frac{x^2+3}{2x}≥0,\ \frac{x^2-3}{2x}≥0is displayed as what it says:
x ≥ \sqrt3. Critical points are computed symbolically precisely because
they are what gets displayed — the root ofx^2-3has to read\sqrt3, not
1.7320508— so-x^2+2 ≥ 0becomes-\sqrt2 ≤ x ≤ \sqrt2andx-27 ≥ 0
becomesx ≥ 27. Its declared reach is closed-form roots: degree 1, degree 2
by the general formula (with the square factor pulled out of the radical,
\sqrt{12}=2\sqrt3), and higher degrees only where integer roots deflate them.
Anything else — a guard with\tan x,|x|,\sqrt x, or a solution set made
of disjoint pieces — leaves the conditions displayed exactly as before: the
module's failure mode is "I don't simplify", never "I simplify wrongly". It is
presentation only; the engine keeps evaluating the original guards, so what is
drawn does not change. -
Contradictory guards are detected across the whole system. Each guard can
be satisfiable on its own while the system is not, and the per-guard check
could not see that. An empty intersection now means there is no real curve and
the equation is left in its reduced form instead of carrying a formula that
never applies. -
obs-integral: the logarithmic derivative
∫\frac{c·q'}{q}=c\ln|q|. A
quotient withxin both numerator and denominator always returned "no
antiderivative". The ratiop/q'is measured by finite differences rather than
by symbolic differentiation, so it does not depend on mathjs being able to
differentiate\cscor\abs. This covers∫\frac{2x}{x^2+1},
∫\frac{3x^2+2}{x^3+2x-5},∫\cot,∫\frac{f'}{f}in general. -
obs-integral: trigonometric canonicalisation as a retry. Expressions
written "by identity" reduce to something the structural rules recognise once
\csc/\sec/\cot/\tanare rewritten in\sin/\cosand the double angle is
opened:∫\frac{1}{\csc 2x-\cot 2x}collapses to∫\cot x = \ln|\sin x|. The
original form is always integrated first, so∫\sin 2xstill gives
-\frac{\cos 2x}{2}and not the expanded version. Also adds the missing
\cot,\secand\csctable entries (with linear substitution:
∫\cot 3x = \frac{\ln|\sin 3x|}{3}).
Bug fixes
-
Clearing a denominator no longer invents branches. Multiplying by
qonly
preserves the curve whereq≠0, and the cleared equation is defined there —
so it can carry solutions that do not exist.\frac{y^2-1}{y-1}=xclears to
y^2-1=x(y-1), whose roots arey=x-1andy=1, and the second is not
curve at all (the original is0/0aty=1). Since a≠condition cannot be
written with the existing≥0domain sentinel, the candidate is instead
validated branch by branch against the equation before multiplying — the only
one that knows about its own holes — and the whole solution is dropped if any
branch contradicts it. -
Removable holes are no longer silently filled.
\frac{y^2-4}{y+2}=xis
affine inythroughout its domain and solves toy=x+2, but the curve does
not containy=-2, i.e. the solved form is missing its hole atx=-4and is
laxer than the curve. Same validation, same outcome: the equation is left in
its reduced form rather than stated more loosely than it is true. -
A singular sampling point no longer leaks into the formula. Recovering the
affine coefficients by substitutingy=0,1hits0/0on an equation like
\frac{y^2-1}{y-1}, and the resultingInfinitytravelled all the way into
the displayed solution (y = ∞x + ∞). Several sampling pairs are tried until
one is clean, and a non-finite coefficient rejects the pair. -
Both branches of a
±are now validated, not just the principal one. The
numeric check evaluated the±sentinel at its principal value, so the second
branch entered the result unchecked. Validation now runs over the expanded
branches — exactly what the engine will draw. A branch that its own domain
guard empties is not a failure (that is precisely the fate of the extraneous
branch introduced by squaring); a branch that contradicts the equation is.