docs: record CAD semantic context and vector crop planning notes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+88
@@ -1,5 +1,93 @@
|
||||
# Findings: Server-Backed Brand Kit Canvas Context
|
||||
|
||||
## 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 `<img>`, 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 `<metadata>` 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.
|
||||
|
||||
Reference in New Issue
Block a user