Skip to content

[Bug]: through points added to a bezier curve are invisible despite alwaysVisible #1624

Description

@dqnykamp

Your DoenetML

<p>Number of through points: <mathInput name="n" prefill="3" /></p>

<sequence name="s" from="-4" to="4" length="$n" />

<repeat name="r" for="$s" valueName="i">
  <point>($i, sin($i))</point>
</repeat>

<graph>
  <curve name="c" through="$r">
    <bezierControls alwaysVisible="true" />
  </curve>
</graph>

What happened?

Change the number of through points from 3 to 7. The three original through points keep their handles, but the four newly added ones are drawn with no fill and no stroke — they are invisible, even though alwaysVisible="true" is set. Moving the pointer over the curve makes them all appear, and they stay visible afterwards.

Reading the JSXGraph state after the change confirms it (fillcolor on the through points):

x −4 −2.67 −1.33 0 1.33 2.67 4
fillcolor #404040 #404040 #404040 none none none none

The first three are the pre-existing points; the last four were added by the update.

Expected: every through point of a curve with <bezierControls alwaysVisible="true" /> is drawn as a handle, whether it was there from the start or added later.

The same code path runs when the pointer is over a curve without alwaysVisible (hovering the curve calls the same "make the through points always visible" helper), so a curve that gains through points while hovered should be affected too — I only verified the alwaysVisible case.

Cause

packages/doenetml/src/Viewer/renderers/curve.tsx (~L964, in the branch that adds points when numericalThroughPoints grows):

let attributesForNewThroughPoints = Object.assign(
    {},
    throughPointAttributes.current,      // { fillColor: "none", strokeColor: "none", ... }
);
if (/* the existing points are currently always-visible */) {
    Object.assign(
        attributesForNewThroughPoints,
        throughPointAlwaysVisible.current, // { fillcolor: handleColor, strokecolor: handleColor }
    );
}

throughPointAlwaysVisible is spelled in lowercase because its other two uses (makeThroughPointsAlwaysVisible, and the comparison just above this) write straight to visProp, where JSXGraph stores keys lowercased. Merged into an attribute object it is a different key from fillColor, so the object ends up carrying both spellings.

JSXGraph then normalizes the attribute object with JXG.keysToLowerCase, which walks the keys with while (n--) — reverse insertion order — so of two spellings the first-written one wins. fillColor: "none" was written first, so the always-visible override is silently dropped and the new points come out transparent.

This is the same failure mode as #1621 (a labeled <vector>/<polyline> drawing a label on each drag handle), in the opposite direction: there a camelCase override was dropped in favor of an earlier lowercase builder key. That PR documents the rule in buildGraphicalAttributes.ts and adds a Vitest guard for the attribute builders, but this call site is hand-rolled and not covered by it.

Suggested fix

Translate to the attribute spelling at the merge site rather than reusing the visProp object, e.g.

Object.assign(attributesForNewThroughPoints, {
    fillColor: throughPointAlwaysVisible.current.fillcolor,
    strokeColor: throughPointAlwaysVisible.current.strokecolor,
});

I confirmed locally that this makes every newly added through point render as a handle.

Worth pairing with e2e coverage: packages/test-cypress/cypress/e2e/tagSpecific/curve.bezier.cy.js is entirely it.skip'd, and nothing else in the repo exercises bezierControlsAlwaysVisible, so this path currently has no test at all.

What version of DoenetML are you using?

main at fd3ee74 (viewer 0.7.21)

What browsers are you seeing the problem on?

Chrome

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    Priority

    Medium

    Effort

    Low

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions