Add 33 OOXML-aligned shapes (slides Phase 1 foundation)#202
Conversation
Phase 1 of a four-phase roadmap to grow the slides shape library from 4 (rect, ellipse, line, arrow) to OOXML's 187 prstGeom presets. This commit lands only the design + plan; subsequent commits land the data model, dispatcher refactor, 31 new path builders, and the categorised picker popover. Goal coverage after Phase 1: 35 OOXML-aligned ShapeKind values (2 lines special + 15 basic + 8 block arrows + 4 callouts + 6 equation), with adjustments storage forward-compatible with later phases' editing UIs and OOXML preset import.
Phase 1 of the four-phase shape-library expansion (see docs/design/slides/slides-shapes-p1.md). Adds 31 new ShapeKind members aligned with OOXML prstGeom preset names, and an optional `adjustments: number[]` field on ShapeElement.data that mirrors `<a:avLst>`. No renderer changes yet — those land in subsequent tasks. Existing rect/ellipse/line/arrow data round-trips unchanged.
T1's data-model expansion in @wafflebase/slides also needs to propagate to the frontend's parallel Yorkie schema, which inlines its own ShapeKind union instead of re-exporting. Without this fix, later tasks writing kind: 'roundRect' (etc.) through the Yorkie store would have the inline union narrow them to never.
Establishes the PathBuilder/AdjustmentSpec types and the two registry maps that subsequent tasks populate one shape at a time. The dispatcher in Task 4 reads from these maps; an empty registry is safe because the dispatcher falls back to a placeholder rect for unknown ShapeKind values.
Pure code move ahead of the dispatcher refactor. line/arrow do not fit the (size, adjustments) -> Path2D builder pattern (open path, two-tone arrow head), so they keep bespoke renderers in their own file. The dispatcher rewrite in the next commit switches to these.
line/arrow keep their bespoke renderers (lifted to shape-special.ts in the previous commit). All other kinds resolve through the PATH_BUILDERS registry, with a placeholder-rect fallback for kinds that have not yet registered a builder. Subsequent commits fill in the registry one shape at a time.
Establishes the per-shape file pattern: one builder + one test file per ShapeKind, plus a one-line registration in shapes/index.ts. The dispatcher now routes 'rect' through the builder; placeholder fallback only fires for kinds still pending registration. Adds a Path2D + isPointInPath shim to test-canvas-env so builder unit tests can hit-test geometry under node/jsdom (neither ships a real Path2D). Adapts the rect-related shape-/slide-/element-renderer test assertions from fillRect/strokeRect to ctx.fill(path) / ctx.stroke(path) to match the dispatcher's Path2D-aware draw calls.
T7 builders need quadraticCurveTo (roundRect, callouts), bezierCurveTo (can), and arc (cloud) plus an evenodd fill rule for donut. Extend the test-canvas-env Path2D stub to record those ops as polyline approximations and accept a fillRule argument on isPointInPath. Add a self-test so future shim regressions surface immediately.
Donut needs the inner ellipse punched out as a hole, which only works with the evenodd fill rule. Add EVENODD_KINDS to the dispatcher and forward 'evenodd' or 'nonzero' to ctx.fill(path, ...). Move the unknown-kind fallback test from 'donut' (now registered) to 'rightArrow' (still unregistered).
T8 will register rightArrow, so move the unknown-kind fallback test to use 'wedgeRectCallout' (registered in T9) as its placeholder so the dispatcher's unknown-kind path remains exercised.
Pins every registered path builder's canvas operations into a single snapshot, guarding against accidental geometry regressions when adding/refactoring builders. Updates require explicit --update intent.
InsertKind now mirrors ShapeKind | 'text' so all 35 kinds are selectable from the editor controller. buildInsertElement applies per-category defaults (filled / outlined / line) instead of a per-kind switch. Existing rect/ellipse/line/arrow defaults are preserved.
Paints a shape outline at icon size into the supplied context using the same PATH_BUILDERS the canvas uses, so the upcoming Shape ▾ picker's previews cannot drift from the actual geometry. line/arrow get a small dedicated picker glyph since their canvas-time renderers paint themed fills.
Single Shape ▾ trigger replaces the previous five inline insert buttons. Popover renders five category sections (Lines, Shapes, Block Arrows, Callouts, Equation) as 6-col grids of canvas-rendered icons sourced from the slides PATH_BUILDERS, so previews stay in sync with canvas geometry without bespoke SVG assets.
Replaces the five inline insert buttons with a single Text-box toggle plus the categorised Shape ▾ popover. Insert mode wiring is unchanged: clicks still call editor.setInsertMode(kind), and the next pointer drag on the slide creates that shape.
Records the workflow, test infrastructure, path-builder design, picker UX, dispatcher invariants, and subagent-execution lessons that came out of shipping the 35-shape catalogue. Notes things to watch in Phase 2 (regular-polygon helper, adjustments toolbar UI binding to ADJUSTMENT_SPECS).
Five new scenarios cover the Phase 1 shape library's render surface end-to-end: - Full 35-shape catalog (5x7 grid) under default-light, default-dark, and material themes — verifies role-bound fills/strokes resolve correctly across palettes for every new builder. - Single large donut — verifies the dispatcher's evenodd opt-in keeps the hole visible at scale. - wedgeRectCallout with default tail — verifies the closest-edge tail attachment and outlined default fill. New scenarios will need baseline regeneration via pnpm verify:browser:docker:update before the next visual diff run.
Filled-category shapes (basic + block arrows + equation = 28 kinds) now insert with both an accent1 fill AND a thin text-coloured stroke at width 1, matching Google Slides convention. Without the stroke, shapes whose silhouette depends on contour (can lid, cloud, donut hole) read as flat blobs against the slide background, and the picker preview (stroke-only) was inconsistent with the rendered result. Existing rect/ellipse documents are unaffected — the change only applies to newly inserted shapes.
The previous bezierCurveTo control points produced an over-domed silhouette plus the separately-drawn lid ellipse stroked twice over the bezier curve at the top. Replaced with proper half-ellipse arcs for the body outline; lid stays as a separate sub-path so it renders cleanly as the can opening when stroked. No adjustment-spec change.
At 24x24 the wedgeRect/wedgeRoundRect/wedgeEllipse/cloudCallout tails extend below the frame and the small thought-bubble circles collapse into a few pixels — unrecognizable previews. Special-case the four callouts in renderShapeIcon to use their bubble proxy (rect / roundRect / ellipse / cloud) so the picker shows the identifying body shape. The slide canvas still renders the full callout at full size unchanged.
Two problems with the previous can fix: 1. Canvas2D's path.ellipse() emits an implicit lineTo from the current pen position to the arc's start point. After the body sub-path's closePath() the pen sat at (0, ry); the lid full- ellipse started at (w, ry). The implicit lineTo painted a horizontal line across the top of the can in real browsers. The test-canvas shim doesn't replicate this implicit lineTo, so the bug only manifested in the browser. 2. The lid full-ellipse double-stroked the top arc (already drawn by the body silhouette's upper half), thickening the top. Fix: explicit moveTo(w, ry) before the lid arc, and only draw the LOWER half of the lid (the visible "can opening" line). The upper half is the body silhouette's top arc and shouldn't be drawn twice.
The previous cloud builder painted N=5 full circles plus implicit Canvas2D lineTo segments between arc start points — visually a flower of separate circle outlines connected by a pentagon, not a cloud. The new builder uses N=6 lobes and computes each adjacent pair's outer intersection point. Each lobe contributes only its OUTER arc (from previous-lobe intersection to next-lobe intersection), so the path is one continuous closed silhouette that reads as a puffy cloud. cloudCallout's bubble portion picks up the same silhouette via its existing buildCloud() composition, so its picker proxy stays correct without further changes.
User reported borders look too thick at the 24×24 picker scale — even simple polygon outlines (rect, ellipse, hexagon) read as chunky. Drop STROKE_WIDTH to 1.0 to better match other 24px line icons in the toolbar. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
After the outer ellipse traces back to (w, h/2) at 2π, drawing the inner ellipse implicitly emits a lineTo from (w, h/2) to the inner arc's start at (outerRx + innerRx, outerRy). In real browsers that extra segment is stroked, painting a horizontal line from the inner hole's right edge to the outer ring's right edge. The test-canvas Path2D shim's `ellipse()` does not replicate this implicit lineTo, so unit tests passed. Same class of bug as the recent can fix. Insert an explicit moveTo to the inner arc's start so Canvas2D doesn't fill in the gap. Snapshot updated to include the new subpath; rendered output now shows a clean ring on stroked donuts. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The plus path is a single 12-vertex outline with four concave 90° corners (the inner notches between arms). Default Canvas2D miter joins splay outward at concave corners, painting tiny spikes that point toward the cross's centre. Four spikes meeting at the middle read as a small phantom square outline when the shape is stroked. Set `ctx.lineJoin = 'round'` in the dispatcher's stroke branch. The picker icon renderer already sets this, so the fix only affects the slide-canvas dispatcher; rounded joins are also a better default for the rest of the shape catalog. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The previous implementation drew two separate diagonal rectangles as two closed sub-paths. When stroked, both rectangles' boundaries were painted — including the four short edges that sit inside the overlap region — which read as a small square outline at the centre of the X. Build the cross unrotated as a 12-vertex `+` outline (4 arm tips × 2 outer corners + 4 inner notches), then rotate every vertex by 45° about the centre. The single closed polygon contains only the outer outline of the X union, so the centre fills/strokes cleanly. Snapshot updated to a single subpath with 12 points; rendered output now shows clean `×` glyphs at all sizes (icon, slide, filled, and stroked). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Same fix as mathMultiply: two overlapping path.rect() sub-paths each stroke independently, painting a visible square outline at the centre where the bars cross. Replaced with a single closed 12-vertex polygon outlining the union (matching the basic/plus implementation, just with mathPlus's smaller default thickness).
Three sub-paths (top bar rect + bottom bar rect + rotated slash polygon) each stroke independently, painting a small parallelogram outline in each bar where the slash crosses — visible as a 4-edge artifact at the slash×bar intersection. Replaced with a single closed polygon traced clockwise from the slash's upper-right tip: alternates between slash edges and bar edges at each of the 8 slash×bar crossings. Walks 20 vertices in total — slash end-caps (4), top-bar right + left segments (8), bottom-bar right + left segments (8). Default-adjustment geometry verified by hand; existing point-in-path test points still pass.
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (29)
📒 Files selected for processing (96)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Verification: verify:selfResult: ✅ PASS in 182.6s
Verification: verify:integrationResult: ✅ PASS |
Adds the 5 new harness scenarios to scripts/verify-visual-browser.mjs's hardcoded scenario list (was missed when the scenarios first landed) and captures their baselines across all 4 profiles (desktop, mobile, desktop.dark, mobile.dark). 20 new baselines: - slides-canvas-shapes-catalog-light (full 35-shape grid) - slides-canvas-shapes-catalog-dark - slides-canvas-shapes-catalog-material - slides-canvas-donut-evenodd - slides-canvas-callout-tail Also picks up legitimate baseline drift from the toolbar (5 inline buttons → Text-box + Shape ▾ popover) and the harness-visual root images that contain the toolbar. Without this commit, the verify-browser CI job fails for any PR that adds a scenario to the harness.
CI's Linux x86_64 runner produces ULP-level (15th decimal) drift versus Apple Silicon when path builders walk Math.sin / Math.cos through the shim's arc / bezier polyline approximation, so the unrounded snapshot was not portable: e.g. y=6.005165928857689 on mac vs 6.005165928857686 on Linux. Adds a quantize() helper that rounds every numeric leaf to 6 decimals before snapshotting. Geometry regressions stay visible (micrometer precision is well below any meaningful shift), CPU- microarch noise does not leak into the snapshot.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Each package already has its own README, but there was no top-level entry point inside `packages/` that summarised what lives where. Splits the listing into engines (sheets, docs, slides) and apps and services (frontend, backend, cli, documentation) so a new contributor can orient before drilling into a single package. Four active tasks have shipped on main (PR #189 undo-destroys-initial-block, #202+#205 slides-shapes-p1, #206 slides-thumb-responsive, #204 contributing-md) but their todo files were still sitting in `docs/tasks/active/` because some checkboxes were never ticked. Mark the remaining items done and run `pnpm tasks:archive` to move the five files into `archive/2026/05/`. While there, eight April task docs were still loose at the archive root (predating the by-month layout). Move them into `archive/2026/04/` so the directory is uniformly organised and the index counts stay accurate. Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Adds @wafflebase/slides as a third surface alongside Sheets and Docs, plus 53-shape library (Phase 1+2), adjustment handles for 9 pilot shapes (P3-A.1), live snap guides, align/distribute toolbar, themed authoring, and layout-change UI. Also ships Sheets cell comments (Phase B), docs peer-avatar caret jump, yorkie-js-sdk 0.7.8 upgrade, and CLI/REST API improvements (docs export imageFetcher, expired session refresh, REST docs split). Minor bump because slides is a new top-level package. Highlights: #184 #185 #186 #187 #188 #189 #190 #191 #192 #197 #198 #201 #202 #203 #204 #205 #206 #207 #209 #210
Summary
Phase 1 of a four-phase roadmap to grow the slides shape library from 4 (
rect,ellipse,line,arrow) to OOXML's 187prstGeompresets. This PR lands 35 OOXML-aligned shape kinds (2 lines + 15 basic + 8 block arrows + 4 callouts + 6 equation), the path-builder registry architecture, the categorised picker popover, and visual harness coverage.Spec:
docs/design/slides/slides-shapes-p1.mdPlan:
docs/tasks/active/20260509-slides-shapes-p1-todo.mdLessons:
docs/tasks/active/20260509-slides-shapes-p1-lessons.mdArchitecture
packages/slides/src/view/canvas/shape-renderer.tsswitches from a per-kindswitchtoMap<ShapeKind, (size, adjustments) => Path2D>lookup. Each shape lives inview/canvas/shapes/<category>/<kind>.tsas a pure path-geometry function. The dispatcher applies fill / stroke / theme color /evenoddfill-rule centrally.lineandarrowkeep bespoke renderers inshape-special.ts(open path, two-tone arrow head). Unknown kinds fall back to a placeholder rect with one-timeconsole.warn— forward-compat for the future PPTX importer.adjustments?: number[]added toShapeElement.data, mirroring OOXML<a:avLst>. Phase 1 stores defaults only; P2 adds toolbar inputs, P3 adds drag handles, P4 adds preset-formula evaluator. Storing the field from day one means no Yorkie migration when later phases add editing.PATH_BUILDERSthe canvas uses.quadraticCurveTo/bezierCurveTo/arc/addPath/evenoddfill-rule so per-shape unit tests viaisPointInPathwork without a real browser.Coverage post-merge
adjustmentsprstGeomformula evaluatorTest plan
pnpm verify:fastgreen (2400+ tests across 5 packages, 50 commits each verified).shapes/registry.snap.test.ts).insert.test.tscovers new category defaults (callout outlined, block-arrow filled).shape-picker-helpers.ts.Backward compatibility
rect/ellipse/line/arrowdata round-trips unchanged.adjustments?field is optional; existing documents serialise it as undefined and renderers apply defaults.defaultsForShapefor newly inserted shapes — existing shapes' fill/stroke data is preserved.YorkieShapeElementparallel schema (packages/frontend/src/types/slides-document.ts) updated to importShapeKindfrom@wafflebase/slides(single source of truth).Visual fix iteration (from manual smoke)
After the first user smoke pass, 8 visual bugs were caught and fixed:
STROKE_WIDTH1.5 → 1.0lineJoin = 'round'— eliminates miter spikes at concave cornersFollow-ups (from code review)
Code review (50 commits, 96 files, +9766/−165) returned 0 Critical, 4 Important (all P2-deferrable):
pnpm verify:browser:docker:update, commit the new PNGs (5 new scenarios: catalog × 3 themes + donut + callout). Without this, the nextpnpm verify:browser:dockerrun fails.wedgeRoundRectCalloutonly renders the tail whentailY > h(default downward case). Other 3 callout variants attach to the closest edge. P2 toolbar inputs will expose this — either match the closest-edge logic in P2 or document the limitation in the spec.ADJUSTMENT_SPECSformathMinus/mathMultiplyreusesMATH_PLUS_ADJUSTMENTS(label "Arm thickness"). P2's toolbar UI will show the same label for shapes where it doesn't quite fit. Split or rename when P2 ships.Plus several Minor items captured in the lessons file:
chevronnotch math is approximate (defer to P4 preset engine), Path2D shim curve approximations have ~1px tolerance at boundaries, per-shapelineJoinoverride slot for crisp arrowheads when stroke widths grow (P2).🤖 Generated with Claude Code