Why: 2026-05-10 user report — "你看不到任何真正的生产级设计工具的希望"
called out three persistent visible issues with the food-app design:
1. Search bar shows a "weird inner rounded border" (image 7) — the
wrapper around address+search section sets fill + stroke +
cornerRadius together. strip-redundant-section-fills only cleared
the fill; the leftover stroke + cornerRadius kept drawing the
visible inner pill, which has been there for a long time.
2. Card image has all 4 corners rounded (image 8) — Taco Fiesta /
Bella Italia cards show the food image with bottom corners
rounded too, leaving them visually "ragged" against the title
text below that's flush with the card surface.
(The third — Categories padding — is a layout/intent question; left
for later since it can't be auto-fixed safely without knowing whether
the design wants edge-to-edge content.)
What — two coordinated fixes:
Fix #1: strip-redundant-section-fills now removes stroke and
cornerRadius alongside the fill. A misroll wrapper sets all three
together to "look like a card"; the fill gets stripped on detection
but the leftover chrome kept drawing a phantom card outline. The
three travel together so they should be cleared together. New test
pins the search-bar wrapper case.
Fix #2: new clipCardImageCorners pass in pen-core. When a card-shape
parent (scalar cornerRadius > 0, 2+ children, first child is an
image or canonical image-placeholder, image has its own scalar
cornerRadius) is detected, set parent.clipContent = true and remove
the image's scalar cornerRadius. The card's own corner clip then
cleanly handles the image — top corners round with the card, bottom
corners flush against the title below. 9 unit tests pin the
conservative match policy (silent on standalone image, title-first
card, array-form cornerRadius, cornerRadius:0, no image
cornerRadius, nested cards, existing clipContent).
Wired into applyPostStreamingTreeHeuristics right after
unwrapFakePhoneMockups. 1448 / 1448 pen-core tests pass (was 1438; +10);
1110 / 1110 AI service tests still pass.
|
||
|---|---|---|
| .. | ||
| src | ||
| CLAUDE.md | ||
| LICENSE | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
@zseven-w/pen-core
Core document operations for OpenPencil — tree manipulation, layout engine, design variables, boolean path operations, 3-way merge, and more.
Install
npm install @zseven-w/pen-core
# or
bun add @zseven-w/pen-core
Overview
pen-core is the foundation layer of the OpenPencil stack. It provides pure functions for every document operation — tree CRUD, auto-layout computation, variable resolution, SVG path booleans, and document normalization. All operations are immutable (structural sharing) and framework-free.
Features
Document Tree Operations
Create, query, and mutate the document tree:
import {
createEmptyDocument,
findNodeInTree,
findParentInTree,
insertNodeInTree,
removeNodeFromTree,
updateNodeInTree,
flattenNodes,
isDescendantOf,
getNodeBounds,
} from '@zseven-w/pen-core';
const doc = createEmptyDocument();
const node = findNodeInTree(doc.children, 'node-id');
const parent = findParentInTree(doc.children, 'node-id');
const flat = flattenNodes(doc.children); // all nodes in flat array
Node Cloning
Deep clone nodes with optional ID regeneration:
import { deepCloneNode, cloneNodeWithNewIds, cloneNodesWithNewIds } from '@zseven-w/pen-core';
const clone = deepCloneNode(node); // preserve IDs
const fresh = cloneNodeWithNewIds(node); // new IDs for all descendants
const batch = cloneNodesWithNewIds([a, b, c]); // batch clone
Multi-Page Support
import {
getActivePage,
getActivePageChildren,
setActivePageChildren,
migrateToPages,
ensureDocumentNodeIds,
} from '@zseven-w/pen-core';
// Migrate a single-page document to multi-page format
const multiPageDoc = migrateToPages(doc);
Layout Engine
Flexbox-like auto-layout computation supporting fill_container, fit_content, gap, padding, and alignment:
import {
computeLayoutPositions,
inferLayout,
fitContentWidth,
fitContentHeight,
resolvePadding,
getNodeWidth,
getNodeHeight,
isNodeVisible,
} from '@zseven-w/pen-core';
// Compute absolute positions for all children in a layout container
const positions = computeLayoutPositions(frame, frame.children);
// Infer layout direction from child arrangement
const layout = inferLayout(children); // 'horizontal' | 'vertical' | 'none'
Text Measurement
Estimate text dimensions for layout without a browser DOM:
import {
estimateTextWidth,
estimateTextWidthPrecise,
estimateTextHeight,
resolveTextContent,
hasCjkText,
defaultLineHeight,
} from '@zseven-w/pen-core';
const width = estimateTextWidth('Hello World', 16, 400); // font size, weight
const height = estimateTextHeight(textNode, containerWidth);
const isCjk = hasCjkText('こんにちは'); // true
Design Variables
Resolve $variable references against document variables and theme axes:
import {
isVariableRef,
resolveVariableRef,
resolveNodeForCanvas,
replaceVariableRefsInTree,
getDefaultTheme,
} from '@zseven-w/pen-core';
isVariableRef('$primary'); // true
// Resolve all $refs in a node tree for rendering
const resolved = resolveNodeForCanvas(node, doc.variables, doc.themes);
// Rename $old-name → $new-name across entire tree
replaceVariableRefsInTree(children, 'old-name', 'new-name');
Boolean Path Operations
Union, subtract, intersect, and exclude paths via Paper.js:
import { executeBooleanOp, canBooleanOp, BooleanOpType } from '@zseven-w/pen-core';
if (canBooleanOp(selectedNodes)) {
const result = executeBooleanOp(selectedNodes, BooleanOpType.Union);
}
Document Normalization
Sanitize and fix documents from external sources (Figma imports, AI generation):
import { normalizePenDocument } from '@zseven-w/pen-core';
const cleaned = normalizePenDocument(rawDoc);
// Fixes: fill type "color" → "solid", gradient stop position → offset,
// sizing strings, padding arrays. Preserves $variable refs.
Layout Normalization
Repair AI-generated layout issues:
import {
normalizeTreeLayout,
unwrapFakePhoneMockups,
stripRedundantSectionFills,
normalizeStrokeFillSchema,
} from '@zseven-w/pen-core';
// Infer missing layout modes, strip child x/y in layout containers
normalizeTreeLayout(rootNode);
3-Way Document Merge
Diff and merge document trees for collaborative editing and git integration:
import { diffDocuments, mergeDocuments } from '@zseven-w/pen-core';
// One-direction diff: base → current
const patches = diffDocuments(base, current);
// patches: NodePatch[] — add, remove, modify, move
// 3-way merge: base + ours + theirs
const result = mergeDocuments(base, ours, theirs);
// result: { document, conflicts }
Design.md Parser
Parse and generate design specification documents:
import {
parseDesignMd,
generateDesignMd,
designMdColorsToVariables,
extractDesignMdFromDocument,
} from '@zseven-w/pen-core';
Path Anchors
Convert between anchor point representation and SVG path data:
import {
anchorsToPathData,
pathDataToAnchors,
getPathBoundsFromAnchors,
inferPathAnchorPointType,
} from '@zseven-w/pen-core';
API Reference
| Category | Key Functions |
|---|---|
| Tree CRUD | findNodeInTree, insertNodeInTree, removeNodeFromTree, updateNodeInTree, flattenNodes |
| Cloning | deepCloneNode, cloneNodeWithNewIds, cloneNodesWithNewIds |
| Pages | getActivePage, migrateToPages, ensureDocumentNodeIds |
| Layout | computeLayoutPositions, inferLayout, fitContentWidth, fitContentHeight |
| Text | estimateTextWidth, estimateTextHeight, hasCjkText |
| Variables | resolveVariableRef, resolveNodeForCanvas, replaceVariableRefsInTree |
| Boolean | executeBooleanOp, canBooleanOp |
| Normalize | normalizePenDocument, normalizeTreeLayout |
| Merge | diffDocuments, mergeDocuments |
| IDs | generateId |