Skip to content

Commit

Permalink
feat(edgeless): mindmap gen (#6675)
Browse files Browse the repository at this point in the history
  • Loading branch information
doouding committed Apr 9, 2024
1 parent 4585926 commit 978e35e
Show file tree
Hide file tree
Showing 37 changed files with 1,627 additions and 536 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@
"postinstall": "husky"
},
"lint-staged": {
"*": "prettier --write --cache --ignore-unknown",
"*.{ts,tsx,js,jsx}": "eslint --cache --fix"
"*": "pnpm exec prettier --write --cache --ignore-unknown",
"!(examples/**/*).{ts,tsx,js,jsx}": "pnpm exec eslint --cache --fix"
},
"keywords": [],
"author": "toeverything",
Expand Down
6 changes: 6 additions & 0 deletions packages/blocks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export type {
TemplateCategory,
TemplateManager,
} from './root-block/edgeless/components/toolbar/template/template-type.js';
export { CopilotSelectionController } from './root-block/edgeless/controllers/tools/copilot-tool.js';
export * from './root-block/index.js';
export * from './schemas.js';
export {
Expand All @@ -110,6 +111,10 @@ export {
ElementModel,
generateKeyBetween,
GroupElementModel,
MindmapElementModel,
MindmapRootBlock,
MindmapService,
MindmapSurfaceBlock,
type PointStyle,
type SerializedXYWH,
ShapeElementModel,
Expand All @@ -118,6 +123,7 @@ export {
SurfaceBlockModel,
TextElementModel,
} from './surface-block/index.js';
export { MiniMindmapPreview } from './surface-block/mini-mindmap/mindmap-preview.js';
export { SurfaceBlockComponent } from './surface-block/surface-block.js';
export { SurfaceBlockSchema } from './surface-block/surface-model.js';
export * from './surface-block/surface-service.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
return this.inlineEditor.rootElement;
}

private _lastXYWH = '';
private _keeping = false;
private _resizeObserver: ResizeObserver | null = null;

Expand All @@ -62,6 +63,10 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
const containerWidth = this.richText.offsetWidth;
const textResizing = this.element.textResizing;

if (this._lastXYWH !== this.element.xywh) {
this.requestUpdate();
}

if (
(containerHeight !== this.element.h &&
textResizing === TextResizing.AUTO_HEIGHT) ||
Expand Down Expand Up @@ -286,6 +291,8 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
zIndex: '1',
});

this._lastXYWH = this.element.xywh;

return html`<rich-text
.yText=${this.element.text}
.enableFormat=${false}
Expand Down
6 changes: 3 additions & 3 deletions packages/blocks/src/root-block/edgeless/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ import type {
ElementModel,
GroupLikeModel,
} from '../../surface-block/element-model/base.js';
import type { SurfaceBlockModel } from '../../surface-block/surface-model.js';
import { Bound } from '../../surface-block/utils/bound.js';
import {
getBoundsWithRotation,
getPointsFromBoundsWithRotation,
linePolygonIntersects,
polygonGetPointTangent,
polygonNearestPoint,
rotatePoints,
} from '../../surface-block/index.js';
import type { SurfaceBlockModel } from '../../surface-block/surface-model.js';
import { Bound } from '../../surface-block/utils/bound.js';
} from '../../surface-block/utils/math-utils.js';
import { PointLocation } from '../../surface-block/utils/point-location.js';
import type { IVec } from '../../surface-block/utils/vec.js';
import type { SerializedXYWH } from '../../surface-block/utils/xywh.js';
Expand Down
7 changes: 5 additions & 2 deletions packages/blocks/src/root-block/edgeless/utils/viewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export class Viewport {
private _syncFlag = false;
protected _cumulativeParentScale = 1;

ZOOM_MAX = ZOOM_MAX;
ZOOM_MIN = ZOOM_MIN;

viewportUpdated = new Slot<{ zoom: number; center: IVec2 }>();
sizeUpdated = new Slot<{
width: number;
Expand Down Expand Up @@ -176,7 +179,7 @@ export class Viewport {
setZoom(zoom: number, focusPoint?: IPoint) {
const prevZoom = this.zoom;
focusPoint = (focusPoint ?? this._center) as IPoint;
this._zoom = clamp(zoom, ZOOM_MIN, ZOOM_MAX);
this._zoom = clamp(zoom, this.ZOOM_MIN, this.ZOOM_MAX);
const newZoom = this.zoom;

const offset = Vec.sub(Vec.toVec(this.center), Vec.toVec(focusPoint));
Expand Down Expand Up @@ -240,7 +243,7 @@ export class Viewport {
const [pt, pr, pb, pl] = padding;
const zoom = clamp(
(this.width - (pr + pl)) / bound.w,
ZOOM_MIN,
this.ZOOM_MIN,
(this.height - (pt + pb)) / bound.h
);
const center = [
Expand Down
39 changes: 24 additions & 15 deletions packages/blocks/src/root-block/widgets/ai-panel/ai-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ import type {
} from './components/index.js';

export interface AffineAIPanelWidgetConfig {
answerRenderer: (answer: string) => TemplateResult<1>;
answerRenderer: (
answer: string,
state: AffineAIPanelState
) => TemplateResult<1> | typeof nothing;
generateAnswer?: (props: {
input: string;
update: (answer: string) => void;
Expand Down Expand Up @@ -79,6 +82,8 @@ export class AffineAIPanelWidget extends WidgetElement {
}
`;

ctx: unknown = null;

@property({ attribute: false })
config: AffineAIPanelWidgetConfig | null = null;

Expand All @@ -88,6 +93,8 @@ export class AffineAIPanelWidget extends WidgetElement {
@query('.mock-selection-container')
mockSelectionContainer!: HTMLDivElement;

private _stopAutoUpdate?: undefined | (() => void);

toggle = (reference: ReferenceElement, input?: string) => {
if (input) {
this._inputText = input;
Expand All @@ -98,26 +105,26 @@ export class AffineAIPanelWidget extends WidgetElement {
this.state = 'input';
}

this._abortController.signal.addEventListener(
'abort',
autoUpdate(reference, this, () => {
computePosition(reference, this, {
placement: 'bottom-start',
})
.then(({ x, y }) => {
this.style.left = `${x}px`;
this.style.top = `${y}px`;
})
.catch(console.error);
this._stopAutoUpdate?.();
this._stopAutoUpdate = autoUpdate(reference, this, () => {
computePosition(reference, this, {
placement: 'bottom-start',
})
);
.then(({ x, y }) => {
this.style.left = `${x}px`;
this.style.top = `${y}px`;
})
.catch(console.error);
});
};

hide = () => {
this._resetAbortController();
this._stopAutoUpdate?.();
this.state = 'hidden';
this._inputText = null;
this._answer = null;
this._stopAutoUpdate = undefined;
};

/**
Expand All @@ -130,6 +137,7 @@ export class AffineAIPanelWidget extends WidgetElement {
assertExists(this.config.generateAnswer);

this._resetAbortController();

// reset answer
this._answer = null;

Expand Down Expand Up @@ -269,7 +277,8 @@ export class AffineAIPanelWidget extends WidgetElement {
.finish=${false}
.config=${config.finishStateConfig}
>
${this.answer && config.answerRenderer(this.answer)}
${this.answer &&
config.answerRenderer(this.answer, this.state)}
</ai-panel-answer>
`
: nothing}
Expand All @@ -282,7 +291,7 @@ export class AffineAIPanelWidget extends WidgetElement {
'finished',
() => html`
<ai-panel-answer .config=${config.finishStateConfig}>
${this.answer && config.answerRenderer(this.answer)}
${this.answer && config.answerRenderer(this.answer, this.state)}
</ai-panel-answer>
`,
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export class EdgelessCopilotPanel extends WithDisposable(LitElement) {
return this.edgeless.service.std.command.chain();
}

hide() {
this.remove();
}

override render() {
const chain = this._getChain();
const groups = this.groups.reduce((pre, group) => {
Expand Down
36 changes: 30 additions & 6 deletions packages/blocks/src/root-block/widgets/edgeless-copilot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import { styleMap } from 'lit/directives/style-map.js';
import type { AIItemGroupConfig } from '../../../_common/components/ai-item/types.js';
import {
MOUSE_BUTTON,
on,
once,
requestConnectedFrame,
} from '../../../_common/utils/event.js';
import type { CopilotSelectionController } from '../../edgeless/controllers/tools/copilot-tool.js';
import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js';
import type { RootBlockModel } from '../../index.js';
import { actionWithAI, dragWithAI } from '../edgeless-copilot-panel/config.js';
import { type RootBlockModel } from '../../root-model.js';
import type { AffineAIPanelWidget } from '../ai-panel/ai-panel.js';
import { AFFINE_AI_PANEL_WIDGET } from '../ai-panel/ai-panel.js';
import { EdgelessCopilotPanel } from '../edgeless-copilot-panel/index.js';

export const AFFINE_EDGELESS_COPILOT_WIDGET = 'affine-edgeless-copilot-widget';
Expand Down Expand Up @@ -47,19 +47,36 @@ export class EdgelessCopilotWidget extends WidgetElement<
@query('.copilot-selection-rect')
private _selectionRectEl!: HTMLDivElement;

private _selectionModelRect!: DOMRect;

private _clickOutsideOff: (() => void) | null = null;
private _listenClickOutsideId: number | null = null;

private _copilotPanel!: EdgelessCopilotPanel | null;
private _showCopilotPanelOff: (() => void) | null = null;

groups: AIItemGroupConfig[] = [actionWithAI, dragWithAI];
groups: AIItemGroupConfig[] = [];

get selectionRect() {
return this._selectionRect;
}

get selectionModelRect() {
return this._selectionModelRect;
}

get edgeless() {
return this.blockElement;
}

hide() {
this._copilotPanel?.hide();
this._showCopilotPanelOff?.();
}

private _updateSelection(rect: DOMRect) {
this._selectionModelRect = rect;

const zoom = this.edgeless.service.viewport.zoom;
const [x, y] = this.edgeless.service.viewport.toViewCoord(
rect.left,
Expand All @@ -83,10 +100,17 @@ export class EdgelessCopilotWidget extends WidgetElement<
return;
}

const off = on(this.ownerDocument, 'mousedown', e => {
const off = this.blockElement.dispatcher.add('pointerDown', ctx => {
const e = ctx.get('pointerState').raw;
const aiPanel = this.host.view.getWidget(
AFFINE_AI_PANEL_WIDGET,
this.doc.root!.id
) as AffineAIPanelWidget;

if (
e.button === MOUSE_BUTTON.MAIN &&
!this.contains(e.target as HTMLElement)
!this.contains(e.target as HTMLElement) &&
(!aiPanel || !aiPanel.contains(e.target as HTMLElement))
) {
off();
this._copilotPanel?.remove();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import {
deltaInsertsToChunks,
getFontString,
getLineHeight,
getLineWidth,
getTextWidth,
type TextDelta,
wrapText,
wrapTextDeltas,
} from '../text/utils.js';

export const SHAPE_TEXT_PADDING = 20;
Expand Down Expand Up @@ -185,3 +187,32 @@ export function normalizeShapeBound(

return bound;
}

export function updateMindmapNodeRect(shape: ShapeElementModel) {
const font = getFontString(shape);

if (!shape.text) {
return;
}

const lines = deltaInsertsToChunks(
wrapTextDeltas(shape.text, font, shape.maxWidth || Number.MAX_SAFE_INTEGER)
);
const lineHeight = getLineHeight(font, shape.fontSize);
let maxWidth = 0;
let height = 0;

lines.forEach(line => {
for (const delta of line) {
const str = delta.insert;

maxWidth = Math.max(maxWidth, getLineWidth(str, font));
height += lineHeight;
}
});

maxWidth += SHAPE_TEXT_PADDING * 2;
height += SHAPE_TEXT_VERTICAL_PADDING * 2;

shape.xywh = `[${shape.x},${shape.y},${maxWidth},${height}]`;
}

0 comments on commit 978e35e

Please sign in to comment.