mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 08:38:34 +00:00
Compare commits
5 Commits
63e602a6f5
...
0524/mock_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f723d41bd8 | ||
|
|
cd753dcd83 | ||
|
|
f1608d4298 | ||
|
|
a4dd931b71 | ||
|
|
ddc9cb7a3d |
@@ -0,0 +1,3 @@
|
||||
import { ConnectorDomRendererExtension } from '../renderer/dom-elements/index.js';
|
||||
|
||||
export { ConnectorDomRendererExtension };
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './clipboard-config';
|
||||
export * from './connector-dom-renderer';
|
||||
export * from './crud-extension';
|
||||
export * from './dom-element-renderer';
|
||||
export * from './edit-props-middleware-builder';
|
||||
|
||||
@@ -0,0 +1,398 @@
|
||||
import type { ConnectorElementModel } from '@blocksuite/affine-model';
|
||||
import { ConnectorMode, DefaultTheme } from '@blocksuite/affine-model';
|
||||
import {
|
||||
getBezierParameters,
|
||||
type PointLocation,
|
||||
} from '@blocksuite/global/gfx';
|
||||
|
||||
import { DomElementRendererExtension } from '../../extensions/dom-element-renderer.js';
|
||||
import type { DomRenderer } from '../dom-renderer.js';
|
||||
import type { DomElementRenderer } from './index.js';
|
||||
|
||||
/**
|
||||
* DOM renderer for connector elements.
|
||||
* Uses SVG to render connector paths, endpoints, and labels.
|
||||
*/
|
||||
export const connectorDomRenderer: DomElementRenderer<ConnectorElementModel> = (
|
||||
elementModel,
|
||||
domElement,
|
||||
renderer
|
||||
) => {
|
||||
const {
|
||||
mode,
|
||||
path: points,
|
||||
strokeStyle,
|
||||
frontEndpointStyle,
|
||||
rearEndpointStyle,
|
||||
strokeWidth,
|
||||
stroke,
|
||||
w,
|
||||
h,
|
||||
} = elementModel;
|
||||
|
||||
// Clear previous content
|
||||
domElement.innerHTML = '';
|
||||
|
||||
// Points might not be built yet in some scenarios (undo/redo, copy/paste)
|
||||
if (!points.length || points.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create SVG element
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.style.width = `${w * renderer.viewport.zoom}px`;
|
||||
svg.style.height = `${h * renderer.viewport.zoom}px`;
|
||||
svg.style.position = 'absolute';
|
||||
svg.style.top = '0';
|
||||
svg.style.left = '0';
|
||||
svg.style.pointerEvents = 'none';
|
||||
svg.style.overflow = 'visible';
|
||||
|
||||
const strokeColor = renderer.getColorValue(
|
||||
stroke,
|
||||
DefaultTheme.connectorColor,
|
||||
true
|
||||
);
|
||||
|
||||
// Render connector path
|
||||
renderConnectorPath(
|
||||
svg,
|
||||
points,
|
||||
mode,
|
||||
strokeStyle,
|
||||
strokeWidth,
|
||||
strokeColor,
|
||||
renderer.viewport.zoom
|
||||
);
|
||||
|
||||
// Render endpoints
|
||||
if (frontEndpointStyle && frontEndpointStyle !== 'None') {
|
||||
renderEndpoint(
|
||||
svg,
|
||||
points,
|
||||
frontEndpointStyle,
|
||||
'front',
|
||||
strokeWidth,
|
||||
strokeColor,
|
||||
mode,
|
||||
renderer.viewport.zoom
|
||||
);
|
||||
}
|
||||
|
||||
if (rearEndpointStyle && rearEndpointStyle !== 'None') {
|
||||
renderEndpoint(
|
||||
svg,
|
||||
points,
|
||||
rearEndpointStyle,
|
||||
'rear',
|
||||
strokeWidth,
|
||||
strokeColor,
|
||||
mode,
|
||||
renderer.viewport.zoom
|
||||
);
|
||||
}
|
||||
|
||||
// Render label if exists
|
||||
if (elementModel.hasLabel()) {
|
||||
renderConnectorLabel(elementModel, domElement, renderer);
|
||||
}
|
||||
|
||||
domElement.appendChild(svg);
|
||||
};
|
||||
|
||||
function renderConnectorPath(
|
||||
svg: SVGSVGElement,
|
||||
points: PointLocation[],
|
||||
mode: ConnectorMode,
|
||||
strokeStyle: string,
|
||||
strokeWidth: number,
|
||||
strokeColor: string,
|
||||
zoom: number
|
||||
) {
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
|
||||
let pathData = '';
|
||||
|
||||
if (mode === ConnectorMode.Curve) {
|
||||
// Bezier curve
|
||||
const bezierParams = getBezierParameters(points);
|
||||
const [p0, p1, p2, p3] = bezierParams;
|
||||
pathData = `M ${p0[0]} ${p0[1]} C ${p1[0]} ${p1[1]} ${p2[0]} ${p2[1]} ${p3[0]} ${p3[1]}`;
|
||||
} else {
|
||||
// Straight or orthogonal lines
|
||||
pathData = `M ${points[0][0]} ${points[0][1]}`;
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
pathData += ` L ${points[i][0]} ${points[i][1]}`;
|
||||
}
|
||||
}
|
||||
|
||||
path.setAttribute('d', pathData);
|
||||
path.setAttribute('stroke', strokeColor);
|
||||
path.setAttribute('stroke-width', (strokeWidth * zoom).toString());
|
||||
path.setAttribute('fill', 'none');
|
||||
path.setAttribute('stroke-linecap', 'round');
|
||||
path.setAttribute('stroke-linejoin', 'round');
|
||||
|
||||
if (strokeStyle === 'dash') {
|
||||
const dashArray = `${12 * zoom},${12 * zoom}`;
|
||||
path.setAttribute('stroke-dasharray', dashArray);
|
||||
}
|
||||
|
||||
svg.appendChild(path);
|
||||
}
|
||||
|
||||
function renderEndpoint(
|
||||
svg: SVGSVGElement,
|
||||
points: PointLocation[],
|
||||
endpointStyle: string,
|
||||
position: 'front' | 'rear',
|
||||
strokeWidth: number,
|
||||
strokeColor: string,
|
||||
mode: ConnectorMode,
|
||||
zoom: number
|
||||
) {
|
||||
const pointIndex = position === 'rear' ? points.length - 1 : 0;
|
||||
const point = points[pointIndex];
|
||||
const size = 15 * (strokeWidth / 2) * zoom;
|
||||
|
||||
// Calculate tangent direction for endpoint orientation
|
||||
let tangent: [number, number];
|
||||
if (mode === ConnectorMode.Curve) {
|
||||
const bezierParams = getBezierParameters(points);
|
||||
// For curve mode, use bezier tangent
|
||||
if (position === 'rear') {
|
||||
const lastIdx = points.length - 1;
|
||||
const prevPoint = points[lastIdx - 1];
|
||||
tangent = [point[0] - prevPoint[0], point[1] - prevPoint[1]];
|
||||
} else {
|
||||
const nextPoint = points[1];
|
||||
tangent = [nextPoint[0] - point[0], nextPoint[1] - point[1]];
|
||||
}
|
||||
} else {
|
||||
// For straight/orthogonal mode
|
||||
if (position === 'rear') {
|
||||
const prevPoint = points[points.length - 2];
|
||||
tangent = [point[0] - prevPoint[0], point[1] - prevPoint[1]];
|
||||
} else {
|
||||
const nextPoint = points[1];
|
||||
tangent = [nextPoint[0] - point[0], nextPoint[1] - point[1]];
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize tangent
|
||||
const length = Math.sqrt(tangent[0] * tangent[0] + tangent[1] * tangent[1]);
|
||||
if (length > 0) {
|
||||
tangent[0] /= length;
|
||||
tangent[1] /= length;
|
||||
}
|
||||
|
||||
// Adjust tangent direction for front endpoint
|
||||
if (position === 'front') {
|
||||
tangent[0] = -tangent[0];
|
||||
tangent[1] = -tangent[1];
|
||||
}
|
||||
|
||||
switch (endpointStyle) {
|
||||
case 'Arrow':
|
||||
renderArrowEndpoint(svg, point, tangent, size, strokeColor, zoom);
|
||||
break;
|
||||
case 'Triangle':
|
||||
renderTriangleEndpoint(svg, point, tangent, size, strokeColor, zoom);
|
||||
break;
|
||||
case 'Circle':
|
||||
renderCircleEndpoint(svg, point, tangent, size, strokeColor, zoom);
|
||||
break;
|
||||
case 'Diamond':
|
||||
renderDiamondEndpoint(svg, point, tangent, size, strokeColor, zoom);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function renderArrowEndpoint(
|
||||
svg: SVGSVGElement,
|
||||
point: PointLocation,
|
||||
tangent: [number, number],
|
||||
size: number,
|
||||
color: string,
|
||||
zoom: number
|
||||
) {
|
||||
const angle = Math.PI / 4; // 45 degrees
|
||||
const arrowPath = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'path'
|
||||
);
|
||||
|
||||
// Calculate arrow points
|
||||
const cos1 = Math.cos(angle);
|
||||
const sin1 = Math.sin(angle);
|
||||
const cos2 = Math.cos(-angle);
|
||||
const sin2 = Math.sin(-angle);
|
||||
|
||||
const x1 = point[0] + size * (tangent[0] * cos1 - tangent[1] * sin1);
|
||||
const y1 = point[1] + size * (tangent[0] * sin1 + tangent[1] * cos1);
|
||||
const x2 = point[0] + size * (tangent[0] * cos2 - tangent[1] * sin2);
|
||||
const y2 = point[1] + size * (tangent[0] * sin2 + tangent[1] * cos2);
|
||||
|
||||
const pathData = `M ${x1} ${y1} L ${point[0]} ${point[1]} L ${x2} ${y2}`;
|
||||
arrowPath.setAttribute('d', pathData);
|
||||
arrowPath.setAttribute('stroke', color);
|
||||
arrowPath.setAttribute('stroke-width', (2 * zoom).toString());
|
||||
arrowPath.setAttribute('fill', 'none');
|
||||
arrowPath.setAttribute('stroke-linecap', 'round');
|
||||
arrowPath.setAttribute('stroke-linejoin', 'round');
|
||||
|
||||
svg.appendChild(arrowPath);
|
||||
}
|
||||
|
||||
function renderTriangleEndpoint(
|
||||
svg: SVGSVGElement,
|
||||
point: PointLocation,
|
||||
tangent: [number, number],
|
||||
size: number,
|
||||
color: string,
|
||||
zoom: number
|
||||
) {
|
||||
const triangle = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'polygon'
|
||||
);
|
||||
|
||||
const angle = Math.PI / 3; // 60 degrees
|
||||
const cos1 = Math.cos(angle);
|
||||
const sin1 = Math.sin(angle);
|
||||
const cos2 = Math.cos(-angle);
|
||||
const sin2 = Math.sin(-angle);
|
||||
|
||||
const x1 = point[0] + size * (tangent[0] * cos1 - tangent[1] * sin1);
|
||||
const y1 = point[1] + size * (tangent[0] * sin1 + tangent[1] * cos1);
|
||||
const x2 = point[0] + size * (tangent[0] * cos2 - tangent[1] * sin2);
|
||||
const y2 = point[1] + size * (tangent[0] * sin2 + tangent[1] * cos2);
|
||||
|
||||
const points = `${point[0]},${point[1]} ${x1},${y1} ${x2},${y2}`;
|
||||
triangle.setAttribute('points', points);
|
||||
triangle.setAttribute('fill', color);
|
||||
triangle.setAttribute('stroke', color);
|
||||
triangle.setAttribute('stroke-width', (1 * zoom).toString());
|
||||
|
||||
svg.appendChild(triangle);
|
||||
}
|
||||
|
||||
function renderCircleEndpoint(
|
||||
svg: SVGSVGElement,
|
||||
point: PointLocation,
|
||||
tangent: [number, number],
|
||||
size: number,
|
||||
color: string,
|
||||
zoom: number
|
||||
) {
|
||||
const circle = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'circle'
|
||||
);
|
||||
|
||||
const radius = size * 0.5;
|
||||
const centerX = point[0] + radius * tangent[0];
|
||||
const centerY = point[1] + radius * tangent[1];
|
||||
|
||||
circle.setAttribute('cx', centerX.toString());
|
||||
circle.setAttribute('cy', centerY.toString());
|
||||
circle.setAttribute('r', radius.toString());
|
||||
circle.setAttribute('fill', color);
|
||||
circle.setAttribute('stroke', color);
|
||||
circle.setAttribute('stroke-width', (1 * zoom).toString());
|
||||
|
||||
svg.appendChild(circle);
|
||||
}
|
||||
|
||||
function renderDiamondEndpoint(
|
||||
svg: SVGSVGElement,
|
||||
point: PointLocation,
|
||||
tangent: [number, number],
|
||||
size: number,
|
||||
color: string,
|
||||
zoom: number
|
||||
) {
|
||||
const diamond = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'polygon'
|
||||
);
|
||||
|
||||
// Calculate diamond points
|
||||
const perpX = -tangent[1]; // Perpendicular to tangent
|
||||
const perpY = tangent[0];
|
||||
|
||||
const halfSize = size * 0.5;
|
||||
const x1 = point[0] + halfSize * tangent[0]; // Front point
|
||||
const y1 = point[1] + halfSize * tangent[1];
|
||||
const x2 = point[0] + halfSize * perpX; // Right point
|
||||
const y2 = point[1] + halfSize * perpY;
|
||||
const x3 = point[0] - halfSize * tangent[0]; // Back point
|
||||
const y3 = point[1] - halfSize * tangent[1];
|
||||
const x4 = point[0] - halfSize * perpX; // Left point
|
||||
const y4 = point[1] - halfSize * perpY;
|
||||
|
||||
const points = `${x1},${y1} ${x2},${y2} ${x3},${y3} ${x4},${y4}`;
|
||||
diamond.setAttribute('points', points);
|
||||
diamond.setAttribute('fill', color);
|
||||
diamond.setAttribute('stroke', color);
|
||||
diamond.setAttribute('stroke-width', (1 * zoom).toString());
|
||||
|
||||
svg.appendChild(diamond);
|
||||
}
|
||||
|
||||
function renderConnectorLabel(
|
||||
elementModel: ConnectorElementModel,
|
||||
domElement: HTMLElement,
|
||||
renderer: DomRenderer
|
||||
) {
|
||||
if (!elementModel.text || !elementModel.labelXYWH) {
|
||||
return;
|
||||
}
|
||||
|
||||
const labelElement = document.createElement('div');
|
||||
const [lx, ly, lw, lh] = elementModel.labelXYWH;
|
||||
const { x, y } = elementModel;
|
||||
|
||||
// Position label relative to the connector
|
||||
const relativeX = (lx - x) * renderer.viewport.zoom;
|
||||
const relativeY = (ly - y) * renderer.viewport.zoom;
|
||||
|
||||
labelElement.style.position = 'absolute';
|
||||
labelElement.style.left = `${relativeX}px`;
|
||||
labelElement.style.top = `${relativeY}px`;
|
||||
labelElement.style.width = `${lw * renderer.viewport.zoom}px`;
|
||||
labelElement.style.height = `${lh * renderer.viewport.zoom}px`;
|
||||
labelElement.style.pointerEvents = 'auto';
|
||||
labelElement.style.display = 'flex';
|
||||
labelElement.style.alignItems = 'center';
|
||||
labelElement.style.justifyContent = 'center';
|
||||
labelElement.style.backgroundColor = 'white';
|
||||
labelElement.style.border = '1px solid #e0e0e0';
|
||||
labelElement.style.borderRadius = '4px';
|
||||
labelElement.style.padding = '2px 4px';
|
||||
labelElement.style.fontSize = `${(elementModel.labelStyle?.fontSize || 16) * renderer.viewport.zoom}px`;
|
||||
labelElement.style.fontFamily =
|
||||
elementModel.labelStyle?.fontFamily || 'Inter';
|
||||
labelElement.style.color = renderer.getColorValue(
|
||||
elementModel.labelStyle?.color || DefaultTheme.black,
|
||||
DefaultTheme.black,
|
||||
true
|
||||
);
|
||||
labelElement.style.textAlign = elementModel.labelStyle?.textAlign || 'center';
|
||||
labelElement.style.overflow = 'hidden';
|
||||
labelElement.style.whiteSpace = 'nowrap';
|
||||
labelElement.style.textOverflow = 'ellipsis';
|
||||
|
||||
// Set label text content
|
||||
labelElement.textContent = elementModel.text.toString();
|
||||
|
||||
domElement.appendChild(labelElement);
|
||||
}
|
||||
|
||||
// Export the extension
|
||||
import { DomElementRendererExtension } from '../../extensions/dom-element-renderer.js';
|
||||
|
||||
export const ConnectorDomRendererExtension = DomElementRendererExtension(
|
||||
'connector',
|
||||
connectorDomRenderer
|
||||
);
|
||||
@@ -29,3 +29,9 @@ export const DomElementRendererIdentifier = (type: string) =>
|
||||
export type DomElementRenderer<
|
||||
T extends SurfaceElementModel = SurfaceElementModel,
|
||||
> = (elementModel: T, domElement: HTMLElement, renderer: DomRenderer) => void;
|
||||
|
||||
// Export the connector DOM renderer
|
||||
export {
|
||||
connectorDomRenderer,
|
||||
ConnectorDomRendererExtension,
|
||||
} from './connector.js';
|
||||
|
||||
@@ -81,53 +81,6 @@ function getOpacity(elementModel: SurfaceElementModel) {
|
||||
return { opacity: `${elementModel.opacity ?? 1}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* @class DomRenderer
|
||||
* Renders surface elements directly to the DOM using HTML elements and CSS.
|
||||
*
|
||||
* This renderer supports an extension mechanism to handle different types of surface elements.
|
||||
* To add rendering support for a new element type (e.g., 'my-custom-element'), follow these steps:
|
||||
*
|
||||
* 1. **Define the Renderer Function**:
|
||||
* Create a function that implements the rendering logic for your element.
|
||||
* This function will receive the element's model, the target HTMLElement, and the DomRenderer instance.
|
||||
* Signature: `(model: MyCustomElementModel, domElement: HTMLElement, renderer: DomRenderer) => void;`
|
||||
* Example: `shapeDomRenderer` in `blocksuite/affine/gfx/shape/src/element-renderer/shape-dom/index.ts`.
|
||||
* In this function, you'll apply styles and attributes to the `domElement` based on the `model`.
|
||||
*
|
||||
* 2. **Create the Renderer Extension**:
|
||||
* Create a new file (e.g., `my-custom-element-dom-renderer.extension.ts`).
|
||||
* Import `DomElementRendererExtension` (e.g., from `@blocksuite/affine-block-surface` or its source location
|
||||
* `blocksuite/affine/blocks/surface/src/extensions/dom-element-renderer.ts`).
|
||||
* Import your renderer function (from step 1).
|
||||
* Use the factory to create your extension:
|
||||
* `export const MyCustomElementDomRendererExtension = DomElementRendererExtension('my-custom-element', myCustomElementRendererFn);`
|
||||
* Example: `ShapeDomRendererExtension` in `blocksuite/affine/gfx/shape/src/element-renderer/shape-dom.ts`.
|
||||
*
|
||||
* 3. **Register the Extension**:
|
||||
* In your application setup where BlockSuite services and view extensions are registered (e.g., a `ViewExtensionProvider`
|
||||
* or a central DI configuration place), import your new extension (from step 2) and register it with the
|
||||
* dependency injection container.
|
||||
* Example: `context.register(MyCustomElementDomRendererExtension);`
|
||||
* As seen with `ShapeDomRendererExtension` being registered in `blocksuite/affine/gfx/shape/src/view.ts`.
|
||||
*
|
||||
* 4. **Core Infrastructure (Provided by DomRenderer System)**:
|
||||
* - `DomElementRenderer` (type): The function signature for renderers, defined in
|
||||
* `blocksuite/affine/blocks/surface/src/renderer/dom-elements/index.ts`.
|
||||
* - `DomElementRendererIdentifier` (function): Creates unique service identifiers for DI,
|
||||
* used by `DomRenderer` to look up specific renderers. Defined in the same file.
|
||||
* - `DomElementRendererExtension` (factory): A helper to create extension objects for easy registration.
|
||||
* (e.g., from `@blocksuite/affine-block-surface` or its source).
|
||||
* - `DomRenderer._renderElement()`: This method automatically looks up the registered renderer using
|
||||
* `DomElementRendererIdentifier(elementType)` and calls it if found.
|
||||
*
|
||||
* 5. **Ensure Exports**:
|
||||
* - The `DomRenderer` class itself should be accessible (e.g., exported from `@blocksuite/affine/blocks/surface`).
|
||||
* - The `DomElementRendererExtension` factory should be accessible.
|
||||
*
|
||||
* By following these steps, `DomRenderer` will automatically pick up and use your custom rendering logic
|
||||
* when it encounters elements of 'my-custom-element' type.
|
||||
*/
|
||||
export class DomRenderer {
|
||||
private _container!: HTMLElement;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { effects } from './effects';
|
||||
import {
|
||||
ConnectorDomRendererExtension,
|
||||
EdgelessCRUDExtension,
|
||||
EdgelessLegacySlotExtension,
|
||||
EditPropsMiddlewareBuilder,
|
||||
@@ -26,6 +27,7 @@ export class SurfaceViewExtension extends ViewExtensionProvider {
|
||||
super.setup(context);
|
||||
context.register([
|
||||
FlavourExtension('affine:surface'),
|
||||
ConnectorDomRendererExtension,
|
||||
EdgelessCRUDExtension,
|
||||
EdgelessLegacySlotExtension,
|
||||
ExportManagerExtension,
|
||||
|
||||
@@ -9,18 +9,35 @@ function applyShapeSpecificStyles(
|
||||
element: HTMLElement,
|
||||
zoom: number
|
||||
) {
|
||||
if (model.shapeType === 'rect') {
|
||||
const w = model.w * zoom;
|
||||
const h = model.h * zoom;
|
||||
const r = model.radius ?? 0;
|
||||
const borderRadius =
|
||||
r < 1 ? `${Math.min(w * r, h * r)}px` : `${r * zoom}px`;
|
||||
element.style.borderRadius = borderRadius;
|
||||
} else if (model.shapeType === 'ellipse') {
|
||||
element.style.borderRadius = '50%';
|
||||
} else {
|
||||
element.style.borderRadius = '';
|
||||
// Reset properties that might be set by different shape types
|
||||
element.style.removeProperty('clip-path');
|
||||
element.style.removeProperty('border-radius');
|
||||
// Clear DOM 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') {
|
||||
while (element.firstChild) element.firstChild.remove();
|
||||
}
|
||||
|
||||
switch (model.shapeType) {
|
||||
case 'rect': {
|
||||
const w = model.w * zoom;
|
||||
const h = model.h * zoom;
|
||||
const r = model.radius ?? 0;
|
||||
const borderRadius =
|
||||
r < 1 ? `${Math.min(w * r, h * r)}px` : `${r * zoom}px`;
|
||||
element.style.borderRadius = borderRadius;
|
||||
break;
|
||||
}
|
||||
case 'ellipse':
|
||||
element.style.borderRadius = '50%';
|
||||
break;
|
||||
case 'diamond':
|
||||
element.style.clipPath = 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)';
|
||||
break;
|
||||
case 'triangle':
|
||||
element.style.clipPath = 'polygon(50% 0%, 100% 100%, 0% 100%)';
|
||||
break;
|
||||
}
|
||||
// No 'else' needed to clear styles, as they are reset at the beginning of the function.
|
||||
}
|
||||
|
||||
function applyBorderStyles(
|
||||
@@ -78,6 +95,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 +109,80 @@ 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}`,
|
||||
].join(' ');
|
||||
} else {
|
||||
// triangle
|
||||
// Adjusted points for triangle
|
||||
svgPoints = [
|
||||
`${unscaledWidth / 2},${halfStroke}`,
|
||||
`${unscaledWidth - halfStroke},${unscaledHeight - halfStroke}`,
|
||||
`${halfStroke},${unscaledHeight - halfStroke}`,
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
// 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';
|
||||
|
||||
// Build SVG safely with DOM-API
|
||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
const svg = document.createElementNS(SVG_NS, 'svg');
|
||||
svg.setAttribute('width', '100%');
|
||||
svg.setAttribute('height', '100%');
|
||||
svg.setAttribute('viewBox', `0 0 ${unscaledWidth} ${unscaledHeight}`);
|
||||
svg.setAttribute('preserveAspectRatio', 'none');
|
||||
|
||||
const polygon = document.createElementNS(SVG_NS, 'polygon');
|
||||
polygon.setAttribute('points', svgPoints);
|
||||
polygon.setAttribute('fill', finalFillColor);
|
||||
polygon.setAttribute('stroke', finalStrokeColor);
|
||||
polygon.setAttribute('stroke-width', String(strokeW));
|
||||
if (finalStrokeDasharray !== 'none') {
|
||||
polygon.setAttribute('stroke-dasharray', finalStrokeDasharray);
|
||||
}
|
||||
svg.append(polygon);
|
||||
|
||||
// Replace existing children to avoid memory leaks
|
||||
element.replaceChildren(svg);
|
||||
} 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);
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('Shape rendering with DOM renderer', () => {
|
||||
fill: '#ff0000',
|
||||
stroke: '#000000',
|
||||
};
|
||||
const shapeId = surfaceModel.addElement(shapeProps as any);
|
||||
const shapeId = surfaceModel.addElement(shapeProps);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
const shapeElement = surfaceView?.renderRoot.querySelector(
|
||||
@@ -73,7 +73,7 @@ describe('Shape rendering with DOM renderer', () => {
|
||||
subType: 'ellipse',
|
||||
xywh: '[200, 200, 50, 50]',
|
||||
};
|
||||
const shapeId = surfaceModel.addElement(shapeProps as any);
|
||||
const shapeId = surfaceModel.addElement(shapeProps);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
@@ -91,4 +91,48 @@ describe('Shape rendering with DOM renderer', () => {
|
||||
);
|
||||
expect(shapeElement).toBeNull();
|
||||
});
|
||||
|
||||
test('should correctly render 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]',
|
||||
fillColor: '#ff0000',
|
||||
strokeColor: '#000000',
|
||||
filled: true,
|
||||
};
|
||||
const shapeId = surfaceModel.addElement(shapeProps);
|
||||
await wait(100);
|
||||
const shapeElement = surfaceView?.renderRoot.querySelector<HTMLElement>(
|
||||
`[data-element-id="${shapeId}"]`
|
||||
);
|
||||
|
||||
expect(shapeElement).not.toBeNull();
|
||||
expect(shapeElement?.style.width).toBe('80px');
|
||||
expect(shapeElement?.style.height).toBe('60px');
|
||||
});
|
||||
|
||||
test('should correctly render 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]',
|
||||
fillColor: '#ff0000',
|
||||
strokeColor: '#000000',
|
||||
filled: true,
|
||||
};
|
||||
const shapeId = surfaceModel.addElement(shapeProps);
|
||||
await wait(100);
|
||||
const shapeElement = surfaceView?.renderRoot.querySelector<HTMLElement>(
|
||||
`[data-element-id="${shapeId}"]`
|
||||
);
|
||||
|
||||
expect(shapeElement).not.toBeNull();
|
||||
expect(shapeElement?.style.width).toBe('80px');
|
||||
expect(shapeElement?.style.height).toBe('60px');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user