feat(vector-shapes): shadow effects, group transform composition, and shape-clipped image rendering (SD-2999 and SD-3033)#3713
Conversation
|
I verified the changed handler files against my knowledge of ECMA-376 (the live Here's what I checked across the OOXML-facing changes:
One issue stands out: Status: FAIL
When Caveat: I couldn't confirm this against the live schema (MCP permission was denied), so please double-check the default for |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eb9f8f7388
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
ECMA-376 §21.1.2.1.1 specifies that an omitted spcFirstLastPara on bodyPr implies false, meaning before-spacing on the first paragraph and after-spacing on the last paragraph of a text body are suppressed by default. The helper treated the absent attribute as true, honoring edge spacing the spec says to drop. Honor edge spacing only when the attribute explicitly enables it. Addresses the OOXML spec review finding on PR #3713; verified against the spec prose (the XSD declares no default, the prose defines it).
createBlockImageContent only applied shapeClipPath to a caller-supplied clip container, so standalone DrawingML image drawings rendered through createDrawingImageElement (which passes no container) silently lost their preset shape mask and painted rectangular. When a shape mask is present and no container is supplied, wrap the image in its own clip container so the mask and the srcRect crop keep separate clip-paths. Addresses the Codex P2 review finding on PR #3713.
This was fixed |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
When an SVG path's viewBox maps to a coordinate space more than 10x the rendered size on either axis, the scale transform shrinks explicit strokes to sub-pixel widths. Apply vector-effect="non-scaling-stroke" to colored strokes in this case so shape outlines stay visible.
Expand the set of arrow presets that render from Word-expanded XML path geometry to include bentArrow, bentUpArrow, downArrow, leftArrow, leftRightUpArrow, leftUpArrow, quadArrow, rightArrow, upArrow, and uturnArrow (previously only leftRightArrow and upDownArrow). Rework the rightArrow geometry guides so the arrow head uses Word's default proportions, and force leftUpArrow through the expanded-geometry path even at equal width/height. Add regression tests asserting the generated viewBox and path data for each preset.
Anchored header/footer drawings and images were shifted when normalizing decoration positions for negative minY, pulling foreground anchored shapes out of place. Broaden the exclusion beyond behindDoc fragments to also skip fragments that are anchored or carry a sourceAnchor, so their original Y is preserved.
Carry w:spacing before/after from textbox paragraphs through the layout contract (ShapeTextParagraph + isParagraphBoundary) and paint it as paragraph wrapper margins in the DOM painter. Honor wps:bodyPr spcFirstLastPara to suppress first/last paragraph spacing, and only mark paragraph-boundary line breaks so intra-paragraph w:br breaks don't advance spacing. Normalize the new paragraphs metadata in the v1 layout-adapter. Also refactor handleShapeGroup into reusable helpers and add nested (wpg:grpSp) group flattening with EMU-space transform composition, so child coordinates round only after composing parent/child transforms.
…ings Behind-doc wrapNone drawings (including image drawings) are absolute overlays positioned independently of the content region, so they should keep their authored width/height rather than be scaled to fit maxWidth/ maxHeight. Extract isBehindDocOverlay and hasNegativeVerticalPosition helpers and apply the bypass uniformly across measureImageBlock, the image branch of measureDrawingBlock, and the vector-shape branch. Previously only negative vertical positioning bypassed the height constraint, so behind-doc overlays with positive page/paragraph-relative offsets were shrunk and mispositioned.
…int room Render grouped pictures with the same fidelity as top-level images: preserve srcRect crop clipping, preset-geometry shape masks (e.g. ellipse → clip-path), and cover/fill objectFit. The DOM painter now wraps grouped images in a clip container so the shape mask and source crop apply independently rather than colliding. Give grouped vector children room to paint their stroke: derive a group-level effectExtent from child stroke overflow during import, grow the drawing geometry/fragment by that extent, and inset group content so edge strokes are no longer clipped. Connector line-end markers keep using effectExtent for marker sizing and are excluded from stroke paint room. Thread effectExtent through the converter, contract, version signature, and the shapeGroup node attrs, and factor the shared picture-presentation logic out of handleImageNode for reuse by grouped children.
Empty paragraphs that only carry section properties were dropped when followed by a forced page break, discarding any page-relative images, drawings, or tables anchored to them. Keep these marker paragraphs alive when they own anchored objects, suppress their visible text spacing so they stay invisible, and anchor their drawings/tables from the section's raw top margin plus header distance to match Word's placement. Mixed markers that also own page-relative anchors retain the effective body top so companion lines stay aligned with their page-relative art. The v1 paragraph converter now tags synthetic sectPr-marker paragraphs with sectPrMarker so the layout engine can identify them.
…bsolute overlays Header/footer anchored objects with wrap=None create no text exclusion zone, so they should render as absolute story overlays without reserving header/footer text-band height. Previously only full-canvas-covering shapes were excluded from measurement; narrower wrapNone overlays (e.g. cover-page art set to column/paragraph relativity) still inflated pagination height and rendered clipped inside the header/footer container. Drop the canvas-coverage heuristic in favor of a simple wrapNone check for header/footer fragments, and paint these overlays directly on the page element so they are not clipped by the container. Clean up page-level decoration fragments via dedicated selectors on re-render and when the section becomes empty.
Pictures placed inside a non-rectangular preset shape (pic:spPr/prstGeom) were rendered as plain rectangles, ignoring the shape mask. Extract the preset geometry into a shapeClipPath attribute on import, mark stretched shape fills as objectFit: cover, and apply the clip-path to the image's clip container at paint time so the picture is masked to the shape. - super-converter: derive shapeClipPath/shouldCoverShapeStretch in extractPicturePresentation and thread them through the image and shape-group-child handlers - image extension: add unrendered shapeClipPath and objectFit attrs - DomPainter: apply shapeClipPath to the clip container with overflow hidden
… container origin Header/footer absolute overlay fragments that are not page-relative were painted at their raw fragment.y, ignoring the header/footer container offset and landing in the wrong vertical position. Anchor these overlays from the container origin (effectiveOffset plus the footer Y offset for footers) while page-relative overlays keep their page-relative placement.
Reserve layout room for outer shadows so they are not clipped when the docx-provided effectExtent is missing or too small. - Derive a shadow paint extent from blur spread (3·stdDeviation) and the directional offset, in both the layout-adapter (standalone shapes and group children) and the DOM painter's group-child paint extent. - Merge the shadow extent with the centered-stroke extent, taking the per-side maximum, and apply it to no-fill and zero-stroke shapes too. - Extract a shared getShadowStdDeviation helper clamped at zero.
Layer behindDoc and header/footer wrapNone decorations within the page-background story by their OOXML z-order instead of DOM insertion order, while keeping the whole story at CSS z-index 0 so high OOXML z-indices never lift wrapNone shapes above body content. - Add getPageBackgroundDecorationZOrder to derive a stable z-order from behindDoc state and normalized OOXML z-index, offsetting non-behindDoc overlays above the behindDoc background tier. - Add insertPageBackgroundDecoration to insert each decoration in z-order among the leading background-decoration run, tolerating stale nodes from patch renders.
ECMA-376 §21.1.2.1.1 specifies that an omitted spcFirstLastPara on bodyPr implies false, meaning before-spacing on the first paragraph and after-spacing on the last paragraph of a text body are suppressed by default. The helper treated the absent attribute as true, honoring edge spacing the spec says to drop. Honor edge spacing only when the attribute explicitly enables it. Addresses the OOXML spec review finding on PR #3713; verified against the spec prose (the XSD declares no default, the prose defines it).
createBlockImageContent only applied shapeClipPath to a caller-supplied clip container, so standalone DrawingML image drawings rendered through createDrawingImageElement (which passes no container) silently lost their preset shape mask and painted rectangular. When a shape mask is present and no container is supplied, wrap the image in its own clip container so the mask and the srcRect crop keep separate clip-paths. Addresses the Codex P2 review finding on PR #3713.
- Add the optional effects field to TextboxDrawing; versionSignature and layout-bridge diff read effects on the VectorShapeDrawing | TextboxDrawing union, and textbox shapes carry the same spPr effect list as plain vector shapes. - Fix the applyShapeEffects parameter type to the existing ShapeTextDrawingWithEffects alias. - Guard the group rotation read with optional chaining; the normalized rotation check already implies groupTransform exists, but tsc cannot narrow through it.
Anchored wrapNone header/footer drawings are painted as page-level overlays with pointer-events: none, which made their content (e.g. footer textboxes) unclickable even while editing the footer. Keep them inert in body mode for Word parity, but enable pointer events when the section's session is active, and teach the input manager to treat clicks on data-header-footer-overlay-section fragments like behindDoc fragments so the click falls through to the active editor instead of exiting the session.
When a shape group's transform lacks explicit dimensions, fall back to the block geometry minus effect extents instead of the fragment geometry, which includes the extents and skewed the transform origin.
971112a to
0511538
Compare
Summary
Major improvements to vector shape rendering fidelity, covering outer shadow effects, shape-group transform composition, pictures clipped to preset shape geometry, and the anchoring/layout corrections needed to position this artwork where Word puts it.
Outer shadow effects on vector shapes
ShapeEffects/ShapeOuterShadowEffectcontract types, with shared paint-extent math incontracts/src/shape-effects.ts(resolveOuterShadowOffset,getOuterShadowStdDeviation,getOuterShadowPaintExtent) so the painter and measurer agree on how much room a shadow needs.feDropShadowfor filled shapes; a blur/offset/flood/composite chain that clones the path for no-fill outline-only shadows).Shape-group transforms
ShapeGroupTransformgainsrotation/flipH/flipV; group transforms are applied as container transforms so nested groups compose correctly (child-local orientation, nested affine orientation, rotation around the visible group box).Pictures clipped to preset shape geometry
prstGeomonpic:spPr) now carry ashapeClipPathplus anobjectFitthrough the converter → layout adapter → painter pipeline, for inline, anchored, and grouped images alike.coverfor shape-masked stretches,fillfor srcRect-clipped stretches, with negativesrcRectvalues (which Word treats as canvas growth, not crop) handled explicitly.Anchoring and layout corrections
sectPr-only empty paragraphs) are preserved: the marker paragraph is visually suppressed while its anchors register at the marker origin, with Word-compatible offsets for marker image groups.wrapNonemedia render as page-level absolute overlays, non-page-relative overlays position from the container origin, ordinarywrapNonemedia are measured, and anchored media are excluded from decoration normalization.Text in shapes
before/after) is carried per logical paragraph inShapeTextContent.paragraphsand rendered in vector shape text, with a newisParagraphBoundaryflag distinguishing paragraph breaks from intra-paragraph<w:br>.Preset geometry
roundRectpaths generated in target coordinate space.vector-effect: non-scaling-stroke.Testing
painters/dom/src/renderer-shape-regressions.test.ts(~1,000 lines) plus expanded unit coverage across contracts, layout-engine, layout-bridge, layout-resolved, measuring, layout-adapter, converter helpers, and header/footer session management.