From 6a0eb80903e6f3dcf517ab8945124727e64c1bc1 Mon Sep 17 00:00:00 2001 From: Yifeng Wang Date: Fri, 16 May 2025 17:10:56 +0800 Subject: [PATCH] feat(editor): support triangle and diamond shape in shape dom renderer --- .../src/element-renderer/shape-dom/index.ts | 80 +++++++++++++++++-- .../src/__tests__/edgeless/shape-dom.spec.ts | 44 ++++++++++ 2 files changed, 117 insertions(+), 7 deletions(-) diff --git a/blocksuite/affine/gfx/shape/src/element-renderer/shape-dom/index.ts b/blocksuite/affine/gfx/shape/src/element-renderer/shape-dom/index.ts index 4e9c314ebd..f89f184fe5 100644 --- a/blocksuite/affine/gfx/shape/src/element-renderer/shape-dom/index.ts +++ b/blocksuite/affine/gfx/shape/src/element-renderer/shape-dom/index.ts @@ -9,6 +9,14 @@ function applyShapeSpecificStyles( element: HTMLElement, zoom: number ) { + // Reset properties that might be set by different shape types + element.style.clipPath = ''; + element.style.borderRadius = ''; + // Clear innerHTML for shapes that don't use SVG, or if type changes from SVG-based to non-SVG-based + if (model.shapeType !== 'diamond' && model.shapeType !== 'triangle') { + element.innerHTML = ''; + } + if (model.shapeType === 'rect') { const w = model.w * zoom; const h = model.h * zoom; @@ -18,9 +26,12 @@ function applyShapeSpecificStyles( element.style.borderRadius = borderRadius; } else if (model.shapeType === 'ellipse') { element.style.borderRadius = '50%'; - } else { - element.style.borderRadius = ''; + } else if (model.shapeType === 'diamond') { + element.style.clipPath = 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)'; + } else if (model.shapeType === 'triangle') { + element.style.clipPath = 'polygon(50% 0%, 100% 100%, 0% 100%)'; } + // No 'else' needed to clear styles, as they are reset at the beginning of the function. } function applyBorderStyles( @@ -78,6 +89,9 @@ export const shapeDomRenderer = ( renderer: DomRenderer ): void => { const { zoom } = renderer.viewport; + const unscaledWidth = model.w; + const unscaledHeight = model.h; + const fillColor = renderer.getColorValue( model.fillColor, DefaultTheme.shapeFillColor, @@ -89,17 +103,69 @@ export const shapeDomRenderer = ( true ); - element.style.width = `${model.w * zoom}px`; - element.style.height = `${model.h * zoom}px`; + element.style.width = `${unscaledWidth * zoom}px`; + element.style.height = `${unscaledHeight * zoom}px`; + element.style.boxSizing = 'border-box'; + // Apply shape-specific clipping, border-radius, and potentially clear innerHTML applyShapeSpecificStyles(model, element, zoom); - element.style.backgroundColor = model.filled ? fillColor : 'transparent'; + if (model.shapeType === 'diamond' || model.shapeType === 'triangle') { + // For diamond and triangle, fill and border are handled by inline SVG + element.style.border = 'none'; // Ensure no standard CSS border interferes + element.style.backgroundColor = 'transparent'; // Host element is transparent + + const strokeW = model.strokeWidth; + const halfStroke = strokeW / 2; // Calculate half stroke width for point adjustment + + let svgPoints = ''; + if (model.shapeType === 'diamond') { + // Adjusted points for diamond + svgPoints = `\ +${unscaledWidth / 2},${halfStroke} \ +${unscaledWidth - halfStroke},${unscaledHeight / 2} \ +${unscaledWidth / 2},${unscaledHeight - halfStroke} \ +${halfStroke},${unscaledHeight / 2}`; + } else { + // triangle + // Adjusted points for triangle + svgPoints = `\ +${unscaledWidth / 2},${halfStroke} \ +${unscaledWidth - halfStroke},${unscaledHeight - halfStroke} \ +${halfStroke},${unscaledHeight - halfStroke}`; + } + + // Determine if stroke should be visible and its color + const finalStrokeColor = + model.strokeStyle !== 'none' && strokeW > 0 ? strokeColor : 'transparent'; + // Determine dash array, only if stroke is visible and style is 'dash' + const finalStrokeDasharray = + model.strokeStyle === 'dash' && finalStrokeColor !== 'transparent' + ? '12, 12' + : 'none'; + // Determine fill color + const finalFillColor = model.filled ? fillColor : 'transparent'; + + element.innerHTML = ` + + + + `; + } else { + // Standard rendering for other shapes (e.g., rect, ellipse) + // innerHTML was already cleared by applyShapeSpecificStyles if necessary + element.style.backgroundColor = model.filled ? fillColor : 'transparent'; + applyBorderStyles(model, element, strokeColor, zoom); // Uses standard CSS border + } - applyBorderStyles(model, element, strokeColor, zoom); applyTransformStyles(model, element); - element.style.boxSizing = 'border-box'; element.style.zIndex = renderer.layerManager.getZIndex(model).toString(); manageClassNames(model, element); diff --git a/blocksuite/integration-test/src/__tests__/edgeless/shape-dom.spec.ts b/blocksuite/integration-test/src/__tests__/edgeless/shape-dom.spec.ts index b0abb469df..eee3719f45 100644 --- a/blocksuite/integration-test/src/__tests__/edgeless/shape-dom.spec.ts +++ b/blocksuite/integration-test/src/__tests__/edgeless/shape-dom.spec.ts @@ -91,4 +91,48 @@ describe('Shape rendering with DOM renderer', () => { ); expect(shapeElement).toBeNull(); }); + + test('should correctly apply clip-path for diamond shape', async () => { + const surfaceView = getSurface(window.doc, window.editor); + const surfaceModel = surfaceView.model; + const shapeProps = { + type: 'shape', + subType: 'diamond', + xywh: '[150, 150, 80, 60]', + fill: '#ff0000', + stroke: '#000000', + }; + const shapeId = surfaceModel.addElement(shapeProps as any); + await wait(100); + const shapeElement = surfaceView?.renderRoot.querySelector( + `[data-element-id="${shapeId}"]` + ); + + expect(shapeElement).not.toBeNull(); + expect(shapeElement?.style.clipPath).toBe( + 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)' + ); + }); + + test('should correctly apply clip-path for triangle shape', async () => { + const surfaceView = getSurface(window.doc, window.editor); + const surfaceModel = surfaceView.model; + const shapeProps = { + type: 'shape', + subType: 'triangle', + xywh: '[150, 150, 80, 60]', + fill: '#ff0000', + stroke: '#000000', + }; + const shapeId = surfaceModel.addElement(shapeProps as any); + await wait(100); + const shapeElement = surfaceView?.renderRoot.querySelector( + `[data-element-id="${shapeId}"]` + ); + + expect(shapeElement).not.toBeNull(); + expect(shapeElement?.style.clipPath).toBe( + 'polygon(50% 0%, 100% 100%, 0% 100%)' + ); + }); });