Skip to content

Commit

Permalink
feat: property panel for ellipse
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaoiver committed Dec 2, 2024
1 parent e98c778 commit 123e275
Show file tree
Hide file tree
Showing 8 changed files with 310 additions and 54 deletions.
54 changes: 46 additions & 8 deletions packages/core/src/plugins/Selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export class Selector implements Plugin {

movingEvent: CustomEvent;
movedEvent: CustomEvent;
resizingEvent: CustomEvent;
resizedEvent: CustomEvent;

apply(context: PluginContext) {
const {
Expand All @@ -40,6 +42,8 @@ export class Selector implements Plugin {

this.movingEvent = createCustomEvent(SelectableEvent.MOVING);
this.movedEvent = createCustomEvent(SelectableEvent.MOVED);
this.resizingEvent = createCustomEvent(SelectableEvent.RESIZING);
this.resizedEvent = createCustomEvent(SelectableEvent.RESIZED);

const handleClick = (e: FederatedPointerEvent) => {
const mode = getCanvasMode();
Expand Down Expand Up @@ -102,13 +106,48 @@ export class Selector implements Plugin {
}
};

const handleResizingTarget = (e: CustomEvent) => {
const target = e.target as Shape;
// @ts-expect-error - CustomEventInit is not defined
const { tlX, tlY, brX, brY } = e.detail;
const width = brX - tlX;
const height = brY - tlY;

if (target instanceof Circle || target instanceof RoughCircle) {
target.cx = tlX + width / 2;
target.cy = tlY + height / 2;
target.r = width / 2;
} else if (target instanceof Ellipse || target instanceof RoughEllipse) {
target.cx = tlX + width / 2;
target.cy = tlY + height / 2;
target.rx = width / 2;
target.ry = height / 2;
}
};

const handleResizedTarget = (e: CustomEvent) => {
// const target = e.target as Shape;
const target = e.target as Shape;
// @ts-expect-error - CustomEventInit is not defined
const { tlX, tlY, brX, brY } = e.detail;
const width = brX - tlX;
const height = brY - tlY;

if (target instanceof Circle || target instanceof RoughCircle) {
target.cx = tlX + width / 2;
target.cy = tlY + height / 2;
target.r = width / 2;
} else if (target instanceof Ellipse || target instanceof RoughEllipse) {
target.cx = tlX + width / 2;
target.cy = tlY + height / 2;
target.rx = width / 2;
target.ry = height / 2;
}
};

root.addEventListener('click', handleClick);
root.addEventListener(SelectableEvent.MOVING, handleMovingTarget);
root.addEventListener(SelectableEvent.MOVED, handleMovedTarget);
root.addEventListener(SelectableEvent.RESIZING, handleResizingTarget);
root.addEventListener(SelectableEvent.RESIZED, handleResizedTarget);

root.appendChild(this.#activeSelectableLayer);
Expand Down Expand Up @@ -143,14 +182,13 @@ export class Selector implements Plugin {
if (selectable && this.#selected.indexOf(shape) === -1) {
selectable.visible = true;
this.#selected.push(shape);
}

const {
root,
api: { createCustomEvent },
} = this.#context;

root.dispatchEvent(createCustomEvent('selected', shape));
const {
root,
api: { createCustomEvent },
} = this.#context;
root.dispatchEvent(createCustomEvent('selected', shape));
}
}

deselectShape(shape: Shape) {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/shapes/selectable/AbstractSelectable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export enum SelectableEvent {
SELECTED = 'selected',
DESELECTED = 'deselected',

RESIZING = 'resizing',

/**
* resized
*/
Expand Down
113 changes: 78 additions & 35 deletions packages/core/src/shapes/selectable/SelectableRect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export class SelectableRect extends AbstractSelectable {
cullable: false,
draggable: true,
sizeAttenuation: true,
selectable: false,
});
this.appendChild(this.mask);

Expand All @@ -50,6 +51,7 @@ export class SelectableRect extends AbstractSelectable {
opacity: this.anchorOpacity,
batchable: false,
cullable: false,
selectable: false,
};

this.tlAnchor = new Circle({
Expand Down Expand Up @@ -143,40 +145,45 @@ export class SelectableRect extends AbstractSelectable {
) {
// TODO: account for target's rotation
if (target === this.tlAnchor) {
const { cx: brCx, cy: brCy } = this.brAnchor;
this.tlAnchor.cx = x;
this.tlAnchor.cy = y;
this.trAnchor.cx = brCx;
this.trAnchor.cy = y;
this.blAnchor.cx = x;
this.blAnchor.cy = brCy;
this.tlAnchor.position.x = x - this.tlAnchor.cx;
this.tlAnchor.position.y = y - this.tlAnchor.cy;
this.trAnchor.position.y = this.tlAnchor.position.y;
this.blAnchor.position.x = this.tlAnchor.position.x;
} else if (target === this.trAnchor) {
const { cx: blCx, cy: blCy } = this.blAnchor;
this.trAnchor.cx = x;
this.trAnchor.cy = y;
this.tlAnchor.cx = blCx;
this.tlAnchor.cy = y;
this.brAnchor.cx = x;
this.brAnchor.cy = blCy;
this.trAnchor.position.x = x - this.trAnchor.cx;
this.trAnchor.position.y = y - this.trAnchor.cy;
this.tlAnchor.position.y = y - this.tlAnchor.cy;
this.brAnchor.position.x = x - this.brAnchor.cx;
} else if (target === this.blAnchor) {
const { cx: trCx, cy: trCy } = this.trAnchor;
this.blAnchor.cx = x;
this.blAnchor.cy = y;
this.brAnchor.cx = trCx;
this.brAnchor.cy = y;
this.tlAnchor.cx = x;
this.tlAnchor.cy = trCy;
this.blAnchor.position.x = x - this.blAnchor.cx;
this.blAnchor.position.y = y - this.blAnchor.cy;
this.brAnchor.position.y = y - this.brAnchor.cy;
this.tlAnchor.position.x = x - this.tlAnchor.cx;
} else if (target === this.brAnchor) {
const { cx: tlCx, cy: tlCy } = this.tlAnchor;
this.brAnchor.cx = x;
this.brAnchor.cy = y;
this.blAnchor.cx = tlCx;
this.blAnchor.cy = y;
this.trAnchor.cx = x;
this.trAnchor.cy = tlCy;
this.brAnchor.position.x = x - this.brAnchor.cx;
this.brAnchor.position.y = y - this.brAnchor.cy;
this.blAnchor.position.y = y - this.blAnchor.cy;
this.trAnchor.position.x = x - this.trAnchor.cx;
}

this.mask.d = `M${this.tlAnchor.cx} ${this.tlAnchor.cy}L${this.trAnchor.cx} ${this.trAnchor.cy}L${this.brAnchor.cx} ${this.brAnchor.cy}L${this.blAnchor.cx} ${this.blAnchor.cy}Z`;
this.mask.d = `M${this.tlAnchor.cx + this.tlAnchor.position.x} ${
this.tlAnchor.cy + this.tlAnchor.position.y
}L${this.trAnchor.cx + this.trAnchor.position.x} ${
this.trAnchor.cy + this.trAnchor.position.y
}L${this.brAnchor.cx + this.brAnchor.position.x} ${
this.brAnchor.cy + this.brAnchor.position.y
}L${this.blAnchor.cx + this.blAnchor.position.x} ${
this.blAnchor.cy + this.blAnchor.position.y
}Z`;

// @ts-expect-error - CustomEventInit is not defined
this.plugin.resizingEvent.detail = {
tlX: this.tlAnchor.cx + this.tlAnchor.position.x,
tlY: this.tlAnchor.cy + this.tlAnchor.position.y,
brX: this.brAnchor.cx + this.brAnchor.position.x,
brY: this.brAnchor.cy + this.brAnchor.position.y,
};
this.target.dispatchEvent(this.plugin.resizingEvent);
}
});

Expand All @@ -198,15 +205,51 @@ export class SelectableRect extends AbstractSelectable {
this.brAnchor.cx += this.mask.position.x;
this.brAnchor.cy += this.mask.position.y;

const { cx: tlCx, cy: tlCy } = this.tlAnchor;
const { cx: trCx, cy: trCy } = this.trAnchor;
const { cx: brCx, cy: brCy } = this.brAnchor;
const { cx: blCx, cy: blCy } = this.blAnchor;

this.mask.position.x = 0;
this.mask.position.y = 0;
this.mask.d = `M${tlCx} ${tlCy}L${trCx} ${trCy}L${brCx} ${brCy}L${blCx} ${blCy}Z`;

this.target.dispatchEvent(this.plugin.movedEvent);
} else if (
target === this.tlAnchor ||
target === this.trAnchor ||
target === this.blAnchor ||
target === this.brAnchor
) {
this.tlAnchor.cx = this.tlAnchor.position.x + this.tlAnchor.cx;
this.tlAnchor.cy = this.tlAnchor.position.y + this.tlAnchor.cy;
this.tlAnchor.position.x = 0;
this.tlAnchor.position.y = 0;

this.trAnchor.cx = this.trAnchor.position.x + this.trAnchor.cx;
this.trAnchor.cy = this.trAnchor.position.y + this.trAnchor.cy;
this.trAnchor.position.x = 0;
this.trAnchor.position.y = 0;

this.blAnchor.cx = this.blAnchor.position.x + this.blAnchor.cx;
this.blAnchor.cy = this.blAnchor.position.y + this.blAnchor.cy;
this.blAnchor.position.x = 0;
this.blAnchor.position.y = 0;

this.brAnchor.cx = this.brAnchor.position.x + this.brAnchor.cx;
this.brAnchor.cy = this.brAnchor.position.y + this.brAnchor.cy;
this.brAnchor.position.x = 0;
this.brAnchor.position.y = 0;

// @ts-expect-error - CustomEventInit is not defined
this.plugin.resizedEvent.detail = {
tlX: this.tlAnchor.cx,
tlY: this.tlAnchor.cy,
brX: this.brAnchor.cx,
brY: this.brAnchor.cy,
};
this.target.dispatchEvent(this.plugin.resizedEvent);
}

const { cx: tlCx, cy: tlCy } = this.tlAnchor;
const { cx: trCx, cy: trCy } = this.trAnchor;
const { cx: brCx, cy: brCy } = this.brAnchor;
const { cx: blCx, cy: blCy } = this.blAnchor;
this.mask.d = `M${tlCx} ${tlCy}L${trCx} ${trCy}L${brCx} ${brCy}L${blCx} ${blCy}Z`;
});
}
}
26 changes: 23 additions & 3 deletions packages/site/docs/zh/guide/lesson-014.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ publish: false

- 实现 `zIndex``sizeAttenuation` 绘图属性
- 在手型模式下移动、旋转、缩放画布
- 在选择模式下单选、多选、移动图形
- 在选择模式下单选、多选、移动图形,展示图形属性面板
- 在绘制模式下向画布中添加图形

在实现画布模式之前,我们需要做一些准备工作,支持 `zIndex``sizeAttenuation` 这两个绘图属性。
Expand Down Expand Up @@ -466,7 +466,7 @@ call(() => {
// $icCanvas.setAttribute('zoom', '200');
$icCanvas.setAttribute('mode', CanvasMode.SELECT);
$icCanvas.style.width = '100%';
$icCanvas.style.height = '500px';
$icCanvas.style.height = '200px';

$icCanvas.parentElement.style.position = 'relative';
$icCanvas.parentElement.appendChild($stats);
Expand All @@ -486,7 +486,9 @@ call(() => {
});
canvas.appendChild(ellipse);

// setTimeout(() => {
canvas.selectShape(ellipse);
// }, 1000);
});

$icCanvas.addEventListener('ic-frame', (e) => {
Expand Down Expand Up @@ -557,7 +559,24 @@ this.addEventListener('dragend', (e: FederatedEvent) => {

### 展示属性面板 {#property-panel}

[Drawer - Contained to an Element]
选中图形时,我们希望展示图形对应的属性面板,详见 [Drawer - Contained to an Element],这里就不展开了。以 `stroke` 属性为例,我们进行双向绑定,监听当前选中的图形,在用户手动修改后,将新值同步到图形上:

```html
<sl-color-picker
hoist
size="small"
value="${this.shape?.stroke}"
@sl-input="${this.handleStrokeChange}"
opacity
></sl-color-picker>
```

需要注意的是,对于颜色我们希望将透明度分离出来,因此需要使用 [sl-color-picker]`getFormattedValue` 方法获取颜色值,随后使用 `d3-color` 库进行解析,分别赋值给 `stroke``strokeOpacity`

```ts
const strokeAndOpacity = (e.target as any).getFormattedValue('rgba') as string;
const { rgb, opacity } = rgbaToRgbAndOpacity(strokeAndOpacity); // with d3-color
```

### 合并选中成组 {#group-selection}

Expand Down Expand Up @@ -590,3 +609,4 @@ this.addEventListener('dragend', (e: FederatedEvent) => {
[Graphics Tech in Cesium - Vertex Compression]: https://cesium.com/blog/2015/05/18/vertex-compression/
[Drag'n'Drop with mouse events]: https://javascript.info/mouse-drag-and-drop
[Drawer - Contained to an Element]: https://shoelace.style/components/drawer#contained-to-an-element
[sl-color-picker]: https://shoelace.style/components/color-picker
4 changes: 4 additions & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@
"dependencies": {
"@lit/context": "^1.1.2",
"@lit/task": "^1.0.1",
"d3-color": "^3.1.0",
"lit": "^3.1.3",
"@infinite-canvas-tutorial/core": "workspace:*"
},
"devDependencies": {
"@types/d3-color": "^3.1.0"
}
}
Loading

0 comments on commit 123e275

Please sign in to comment.