# Findings: Server-Backed Brand Kit Canvas Context
## 2026-07-17 Pen Tool and Shape Path Editing
- The request is frontend-only unless canvas persistence lacks a path-compatible node representation; the existing API-first go-zero workflow does not apply until a server contract change is proven necessary.
- The integration must preserve the existing workspace visual language and place a Pen icon immediately after Shape.
- Required behavior includes free path drawing, double-click entry/exit for path editing, automatic vertex/control-point editing through `leafer-x-path-editor`, and edit support for shapes created by the existing Shape tool.
- The toolbar already has a Pen icon immediately after Shape, but it currently activates a freehand brush that stores a flattened SVG data URL as an image node; those strokes are not vertex-editable.
- Shape-tool nodes are structured `frame` nodes whose JSON content describes rectangle, ellipse, polygon, star, line, or arrow geometry. They render through React SVG, while the existing Leafer adapter only owns successful image textures and manual frames.
- The integration can stay within the existing `CanvasNode` contract by persisting editable path data inside `content`; no REST or database field is needed.
- `leafer-x-path-editor@1.1.3` installed successfully, but its published peer dependencies are pinned to Leafer 1.1.0 while this project uses `leafer-ui@2.2.2`; the initial install pulled a second Leafer 1.x graph and reported peer conflicts.
- The plugin registers an inner editor on Leafer `Path`, emits `PathEditorEvent.CHANGE`, and expects an `App` with the editor plugin enabled. Runtime integration must ensure the plugin and project resolve the same Leafer core/editor classes.
- The plugin README matches the requested vertex/control behavior. Its current source uses Backspace for anchor deletion even though the requested interaction says Enter, so the application may need an explicit Enter-key bridge while editing.
- Runtime registration succeeds with the project's `@leafer-ui/core@2.2.2` and `@leafer-in/editor@2.2.2`; `@leafer-in/state` is overridden to 2.2.2 so only one Leafer generation is shipped. The package manifest still emits peer warnings because it declares exact 1.1.0 peers.
- Editable shape and Pen paths are normalized inside the existing opaque shape JSON `content`; Brush strokes remain SVG data URLs. Both survive the current frontend/server snapshot contract without adding fields or changing APIs.
- Path edits that cross the current node bounds expand the node while preserving vertex world coordinates, preventing clipping after editor exit.
- User clarification: the existing freehand brush must remain a separate tool. The new Pen is additive, sits immediately after Shape, and creates structured editable shape paths; Brush keeps its existing SVG image-node behavior and width/color controls.
## 2026-07-16 Canvas Performance and LeaferJS Evaluation
- The current canvas is a React/DOM renderer: every visual node is a `CanvasNodeCard`, and the viewport is applied through a transformed DOM content layer.
- Wheel zoom and canvas pan update React `viewport` state at pointer/wheel frequency. Node dragging rebuilds the full `nodes` array and calls `setNodes` on every pointer event, so high-frequency interaction can reconcile the workspace and every un-memoized node even when only one transform changed.
- Large CAD drawings are displayed as one external SVG `
`, not thousands of React elements. Their 9 MB SVG decode/paint cost can amplify frame drops, but React state churn is a separate engine-wide bottleneck that also affects normal images and shapes.
- LeaferJS exposes a retained scene tree, Canvas rendering, hit testing, dragging, zooming, and an editor plugin. Those capabilities align with the current visual/interaction layer, while project persistence, Agent Chat, CAD conversion/semantics, crop, toolbars, and backend contracts should remain application-owned.
- The LeaferJS README's million-element benchmark is vendor-provided and has not been reproduced for this application's image-heavy/CAD workload. The migration decision must use this repo's pan/zoom/selection frame-time baseline.
- A full rewrite would unnecessarily couple chat, persistence, and action panels to the engine. The likely boundary is a canvas renderer adapter driven by the existing `CanvasNode[]` domain model, with React retained for toolbars, panels, overlays, and text editing.
- Viewport changes invalidate more than the `.canvas-world` transform: every `CanvasNodeView` receives `selectionScale` and `viewportScale`, fresh inline event callbacks, a fresh transform object, and a newly built context-menu handler object. A shallow `React.memo` alone would therefore not stop node rerenders.
- `workspaceCropTarget` scans all nodes and recomputes screen bounds on every viewport update. Selection overlays and annotations also consume React viewport state, adding more work to every wheel/pan event.
- Node drag calls `moveNodesByDelta` for the whole document, filters moved nodes, computes alignment guides against the full node list, replaces the `nodes` array, and updates React state on every pointer event. This is a stronger predicted bottleneck than SVG decoding once the image is loaded.
- The latest `leafer-ui` package observed is 2.2.2, MIT licensed, with about 4.3 MB unpacked package contents. The base engine can render images and vector paths, but editor/plugin bundle size and exact SVG/image behavior require a prototype before dependency adoption.
- The separate `@leafer-in/editor` 2.2.2 package is also MIT and about 1.23 MB unpacked. It provides selection/transform events, but React DOM overlays and the existing tool model still need an adapter rather than direct component replacement.
- `useCanvasPersistence` depends on both `viewport` and `nodes`; each interaction update reruns its effect, clears the existing 1.2-second timer, and creates a new timer. It does not save over the network each frame, but it adds avoidable effect/timer churn to the render path.
- Alignment-guide computation is linear in all document nodes for a single active node (with nine edge/center comparisons per candidate), then React still renders the resulting guides and the full node list. The measured browser baseline should vary node count to separate algorithmic scaling from a single large SVG's paint cost.
- Playwright production baseline on a 120Hz browser with lightweight frame nodes:
- 100 nodes: zoom p95 16.1ms, drag p95 9.1ms, one drag frame over 20ms.
- 500 nodes: zoom p95 40.9ms and drag p95 41.4ms; more than 90% of measured frames exceeded 20ms.
- 1000 nodes: zoom p95 83.4ms and drag p95 82.8ms; almost every frame exceeded 34ms and many 50ms+ long tasks were observed.
- The decisive differential probe kept the same 1000 DOM nodes but bypassed React. Direct `.canvas-world` transform updates measured p95 9.2ms with no frame over 20ms; direct single-node position updates also measured p95 9.2ms. This confirms React reconciliation/state/effect work, not raw DOM compositing capacity, as the primary bottleneck.
- Ranked causes after measurement: (1) confirmed React full-tree high-frequency updates; (2) drag-only full-array/alignment/persistence amplification; (3) unmeasured large SVG decode/paint cost; (4) disproved raw DOM node count as the primary cause.
- The first exact-DWG paint probe served the raw `convertCadBufferToSvg` output directly and Chromium rejected it. This is expected for the internal boundary: the third-party writer emits anonymous `>` closing tags, while the normal browser import path repairs and sanitizes them in `normalizeCadSvg` before upload. The valid CAD paint baseline must capture the final UI-uploaded SVG, not the worker's raw intermediate string.
- The real browser upload path converted `/Users/liangxu/Desktop/花语江南6-204方案.dwg` into a valid 8,868,302-byte SVG with a 1200 x 839 preview viewport.
- With that single CAD node, React zoom measured roughly 192ms p95 and even direct `.canvas-world` transform measured roughly 158ms p95, with nearly every frame over 34ms. This isolates a second bottleneck: Chromium repeatedly rasterizes the complex SVG during transform/zoom.
- Replacing only the mocked display source with a one-time 1200 x 839 transparent WebP preview (119,676 bytes) reduced both React and direct zoom to 9.2ms p95 with no frame over 20ms. The persisted node/source remained conceptually SVG; only the display surface changed in the probe.
- The engine therefore needs two independent mechanisms: retained/incremental scene updates for general node count, and cached raster display textures for complex SVG/CAD sources. LeaferJS is relevant because its Canvas image nodes naturally draw cached bitmap textures while the application can keep the original SVG URL and CAD semantics in `CanvasNode`.
- LeaferJS's React guide explicitly warns against directly binding Leafer elements to reactive data because proxy/reactive wrapping significantly reduces performance. The adapter must own mutable engine objects outside React and emit domain updates only at transaction boundaries.
- `App.zoomLayer` exposes imperative `x`, `y`, and `scale`, matching the existing `CanvasViewport` without requiring React rerenders. `App` also separates ground/tree/sky engine layers, which maps cleanly to grid, document content, and editor handles.
- Image paint supports SVG URLs and maintains an asynchronous high-performance image-layer cache by default (`sync: false`). The global default image cache ceiling is documented as 2560 x 1600, which covers the current 1200 x 839 CAD preview texture.
- The recommended dependency boundary is `leafer-ui` plus narrowly selected plugins such as `@leafer-in/editor`, not the full `leafer-editor` bundle, which also includes viewport, scrollbars, arrows, HTML, export, text editing, and other features already owned by the application.
- Editor selection/move/scale events expose world-coordinate deltas and selected UI objects. These can update the Leafer scene immediately and commit the resulting `CanvasNode[]` once on interaction completion, keeping existing persistence and Agent Chat contracts unchanged.
- A throwaway LeaferJS 2.2.2 browser benchmark with 1000 draggable rectangles measured 9.2ms p95 for both viewport updates and one-node movement, with no frame over 20ms. This validates the retained renderer against the same node count where the current React path measured about 83ms p95.
- LeaferJS does not automatically solve complex CAD SVG paint: the exact 8.87 MB SVG used directly as an image fill measured 275.4ms p95 during viewport updates. The same Leafer scene using the one-time transparent WebP preview measured 9.1ms p95.
- Recommended architecture:
- Keep `CanvasNode[]`, original SVG URLs, CAD metadata/semantics, crop/export/model-reference logic, persistence, and Agent Chat application-owned.
- Add a renderer adapter whose Leafer scene objects are mutable and never wrapped in React state. Use `App.tree` for document nodes, `zoomLayer` for viewport interaction, and engine hit testing/editor events for selection and transforms.
- Keep React for workspace chrome, Agent Chat, action panels, menus, and temporary DOM text editing. React receives selection/screen-bounds snapshots and committed transforms, not per-frame movement.
- Persist a separate optional render-preview URL for complex vector nodes. Generate transparent WebP levels from the SVG once, use the current level throughout interaction, and upgrade the level asynchronously after zoom settles. The source `content` remains SVG and is still used by crop/download/semantic/model boundaries.
- Proposed acceptance gates before replacing the default renderer: 1000 mixed nodes and the exact CAD fixture both at <=16.7ms p95 for pan/zoom/drag after resources are ready; zero 50ms+ interaction long tasks; source SVG and semantic context byte/contracts unchanged; crop and Agent Chat reference tests unchanged; pixel comparison for CAD previews at each texture level.
- Production implementation discovery: canvas-node persistence uses explicit field lists across the go-zero `.api` type, Go domain/API mappers, PostgreSQL schema/queries/SQLC models, and frontend snapshot normalization. A durable preview URL therefore requires an explicit `renderContent` field through every layer.
- The canvas upload flow currently batches one file/public URL per optimistic node. Vector preview support must upload the source SVG and transparent WebP together, then commit both URLs to the node only when the source upload succeeds; failed preview upload should fail the vector node rather than silently reverting to the known-stalling raw SVG renderer.
## 2026-07-16 CAD SVG Crop Fidelity
- The crop overlay's `1934 × 1671` label is the selected region size in canvas coordinates, not the generated sibling node size. `createVectorCropSiblingNode` still caps the sibling's longest displayed edge at 640.
- `cropImageNodeFile` currently loads the complete SVG as an `HTMLImageElement`, uses intrinsic-image source coordinates in the nine-argument `drawImage`, and scales that crop to a 2048px long edge.
- This path does not change the SVG `viewBox` before decoding, so the browser can rasterize the full drawing first and then magnify the selected pixels. That explains thick CAD strokes and degraded text/detail in the crop.
- The fix seam is to map the normalized selection into the source SVG `viewBox`, set the cropped SVG's width/height to the final WebP dimensions, decode that rewritten SVG, and draw it once with no source-rectangle resampling.
- Raster image crop behavior remains on the current source-rectangle path.
- A real Chromium pixel probe imported the production `createCroppedSvgRasterSource` function and used an SVG line with `vector-effect="non-scaling-stroke"`. The old full-SVG source-rectangle upscale expanded the line from 4px to 82px; the cropped-viewBox render stayed 4px before and after WebP encoding.
- The generated sibling display cap is independent of the crop overlay's canvas-coordinate label. The crop asset remains a 2048px-long-edge WebP for model readability while the canvas sibling stays bounded to a 640-unit long edge.
- CAD crop semantics are carried separately from the raster bytes: `canvasNodeToReferenceImage` copies `semanticContext`, the composer emits it as hidden `cad-context`, and the backend includes that context in model prompts while excluding it from visible chat text.
## 2026-07-15 Canvas Wheel Passive-Listener Bug
- The reported stack points exactly to `CanvasWorkspace/index.tsx:1242`, where the React `onWheel` handler calls `event.preventDefault()`.
- React delegates `wheel` as a passive event in this runtime, so the browser rejects `preventDefault()` even though the handler is correct to suppress page scrolling while zooming the infinite canvas.
- There is no competing native wheel listener in the workspace. The correct boundary is one native listener on `.canvas-stage` registered with `{ passive: false }`, with explicit cleanup and the existing toolbar/popover bypass guard preserved.
- DWG parsing does not directly cause the warning; the long import makes canvas wheel interaction more likely while the React passive handler is mounted.
- A first native-listener implementation still missed the canvas because its effect ran during the loading view. Binding on `project.id` attaches after `.canvas-stage` mounts; browser verification then changed zoom from 100% to 91%, returned `defaultPrevented=true`, and emitted zero passive-listener warnings.
- The persistent workspace Crop action now follows Text directly. It remains visible/disabled without a selected image, enables for ready raster/vector/CAD images, and opens the same normalized crop overlay used by the floating image toolbars.
## 2026-07-16 Enlarged Single-Image Crop
- The screenshot reproduces a different contract from selected-node cropping: one image fills or exceeds the viewport, no node is selected, and the workspace Crop button is disabled.
- Enabling Crop for every unselected multi-image canvas would be ambiguous. The deterministic fallback should apply only when exactly one visible, unlocked, ready image exists; explicit selection remains authoritative otherwise.
- Initializing an oversized image to the full `{0,0,1,1}` crop leaves resize handles and the crop panel offscreen. The initial normalized rectangle must be the intersection of the image's screen bounds with a safe visible stage area.
- Crop controls should anchor to the active selection's screen bounds, not the full oversized image bounds.
- Browser verification used an unselected 2000 x 1600 image extending beyond a 680 x 672 stage. Workspace Crop enabled automatically, selected the image, and initialized a 632 x 576 crop at the safe visible inset; the panel remained within the 1280 x 720 browser viewport.
- With two visible images and no explicit selection, Workspace Crop remains disabled as intended.
## 2026-07-15 CAD Semantics and Crop Follow-up
- The user explicitly declined legacy-project semantic recovery and will re-upload all DWG/DXF assets. Semantic guarantees therefore apply to newly converted CAD nodes; no heuristic reconstruction from old SVG snapshots is included.
- The requested feature has two coupled outputs: a visually small crop for efficient inspection and a bounded semantic payload that preserves CAD meaning beyond raster pixels.
- The source CAD-derived SVG must remain unchanged. Crop must create a sibling node and asset, enabling repeated crops from the same large drawing and independent Agent Chat follow-ups.
- Useful model semantics should prioritize layer names, block names, visible text, entity-type counts, units/extents, and crop bounds while enforcing size/count limits so large files cannot inflate every prompt.
- Existing crop infrastructure already renders any image source, including SVG, into transparent WebP, but `applyCropImage` currently replaces and repositions the selected node. CAD can reuse the overlay/rasterizer while branching to a new sibling-node save path.
- Vector nodes currently expose only the compact vector/download toolbar. Adding a crop icon there is sufficient to enter the existing crop overlay without exposing unrelated raster AI actions.
- `AgentContent` already supports `textSource`; a `cad-context` text part can carry hidden model context without a new chat payload type. Frontend optimistic display and backend user-message serialization must explicitly exclude that source while agent prompt assembly retains it.
- `CanvasNode` has no semantic field and PostgreSQL persists node columns explicitly. Durable semantics therefore require a spec-first optional node field plus domain/mapping/schema/query updates.
- The CAD converter still has the parsed `CadDocument` in the worker and can embed a structured `` payload into the derived SVG. This preserves richer layer/block/text/entity data in the vector asset while the node stores only a bounded prompt-ready summary.
- Selected contract: the SVG metadata keeps version, source/format/units, drawing bounds, layer/block/entity counts, bounded visible text, and a bounded top-level entity index with visual-coordinate bounds. The node persists a compact human-readable `semanticContext` derived from it.
- Crop semantics can intersect the normalized crop rectangle with the SVG metadata's visual-coordinate entity bounds, producing a crop-specific layer/block/entity/text summary even though the sibling visual is WebP.
- CAD imports will use a distinct `cad-vector` layer role; crop outputs use `cad-crop`. Generic SVGs may reuse the crop action but do not claim CAD semantics.
- Existing `TextSource` behavior is ideal for model delivery: `cad-context` remains in prompt/reference-memory builders, while optimistic UI and persisted user-message rendering need a narrow visibility filter.
- PostgreSQL `canvas_nodes` uses explicit columns and SQLC-generated queries. Adding `semantic_context TEXT NOT NULL DEFAULT ''` requires schema/query regeneration plus repository/domain/API mapping.
- Exact R12 semantic verification exposed legacy GBK bytes decoded as Latin-1 by the CAD library (`¿î·âÑùÖ½°å`). The converter now repairs this high-confidence mojibake before both SVG writing and semantic extraction so labels remain meaningful.
- Exact R12 verification now completes in about 0.37 seconds with 15 model entities, 11,198 reusable block entities, corrected Chinese garment labels, and a 1,050-character bounded model context.
- Exact `花语江南6-204方案.dwg` verification completes in about 11 seconds with 4,376 model entities, 3,397 reusable block entities, 4,422 crop-indexed entries, meaningful Chinese layer/block/text names, and a 1,912-character bounded model context.
- Semantic metadata increases the large DWG-derived SVG from about 8.49 MB to about 9.05 MB while retaining the reusable-block performance model; the increase is bounded and materially smaller than the former 50 MB recursive output.
## 2026-07-15 Vector Reference Follow-up
- Raw CAD/SVG reference text is caused by the prompt-directive parser stopping at the first closing parenthesis in filenames such as `...2026-05-05(1) (DXF).svg`; the existing capsule and hover-preview UI is already shared with WebP references.
- GPT Image rejects SVG input bytes even when the canvas and browser can render them. The SVG source must remain unchanged on canvas and be rasterized to alpha-preserving WebP only when building model-bound image contents.
- Browser rasterization is the appropriate conversion boundary because the generated CAD SVG relies on reusable definitions, `