@neta-art/cohub 12.2.0 → 12.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -3
- package/dist/board/editor/context.d.ts +2 -0
- package/dist/board/editor/document.d.ts +3 -1
- package/dist/board/editor/document.js +8 -6
- package/dist/board/editor/editor.d.ts +7 -3
- package/dist/board/editor/editor.js +8 -1
- package/dist/board/editor/index.d.ts +3 -3
- package/dist/board/editor/index.js +2 -2
- package/dist/board/editor/interaction.d.ts +23 -2
- package/dist/board/editor/interaction.js +99 -5
- package/dist/board/editor/query.d.ts +1 -1
- package/dist/board/editor/query.js +3 -7
- package/dist/board/editor/selection.js +13 -1
- package/dist/board/editor/tool.d.ts +4 -1
- package/dist/board/editor/tool.js +6 -1
- package/dist/board/model/asset-key.d.ts +9 -1
- package/dist/board/model/asset-key.js +27 -7
- package/dist/board/model/index.d.ts +5 -5
- package/dist/board/model/index.js +4 -4
- package/dist/board/model/items/create.js +1 -1
- package/dist/board/model/items/definition.d.ts +9 -1
- package/dist/board/model/items/index.d.ts +2 -2
- package/dist/board/model/items/registry.d.ts +2 -1
- package/dist/board/model/items/registry.js +1 -0
- package/dist/board/model/text-metrics.d.ts +21 -1
- package/dist/board/model/text-metrics.js +24 -1
- package/dist/board/model/themes.d.ts +49 -42
- package/dist/board/model/themes.js +49 -44
- package/dist/board/render/index.d.ts +8 -6
- package/dist/board/render/index.js +7 -5
- package/dist/board/render/labels.d.ts +15 -0
- package/dist/board/render/labels.js +14 -0
- package/dist/board/render/lod.d.ts +9 -1
- package/dist/board/render/lod.js +7 -1
- package/dist/board/render/media-interaction.d.ts +5 -2
- package/dist/board/render/media-interaction.js +14 -2
- package/dist/board/render/renderers/arrow-card-renderer.js +7 -5
- package/dist/board/render/renderers/board-renderer-registry.d.ts +12 -1
- package/dist/board/render/renderers/board-renderer-registry.js +9 -4
- package/dist/board/render/renderers/far-plate.d.ts +4 -1
- package/dist/board/render/renderers/far-plate.js +5 -1
- package/dist/board/render/renderers/file-card-renderer.js +44 -24
- package/dist/board/render/renderers/frame-card-renderer.js +9 -3
- package/dist/board/render/renderers/image-card-renderer.js +2 -2
- package/dist/board/render/renderers/model-card-renderer.js +2 -2
- package/dist/board/render/renderers/task-card-renderer.js +2 -2
- package/dist/board/render/renderers/video-card-renderer.js +2 -2
- package/dist/board/render/style.d.ts +1 -0
- package/dist/board/render/style.js +3 -1
- package/dist/board/render/text-edit.d.ts +32 -0
- package/dist/board/render/text-edit.js +134 -0
- package/dist/board/render/text-fit.d.ts +1 -3
- package/dist/board/render/text-fit.js +4 -43
- package/dist/board/render/text-measurement.d.ts +12 -2
- package/dist/board/render/text-measurement.js +55 -10
- package/dist/board/replica/index.d.ts +4 -2
- package/dist/board/replica/index.js +34 -21
- package/dist/board/replica/indexeddb.d.ts +16 -0
- package/dist/board/replica/indexeddb.js +155 -0
- package/dist/board/stage/assets.d.ts +16 -4
- package/dist/board/stage/assets.js +155 -30
- package/dist/board/stage/bench.d.ts +35 -0
- package/dist/board/stage/bench.js +153 -0
- package/dist/board/stage/device.d.ts +41 -0
- package/dist/board/stage/device.js +62 -0
- package/dist/board/stage/frame-budget.d.ts +19 -0
- package/dist/board/stage/frame-budget.js +69 -0
- package/dist/board/stage/idle.d.ts +4 -0
- package/dist/board/stage/idle.js +26 -0
- package/dist/board/stage/index.d.ts +9 -5
- package/dist/board/stage/index.js +5 -2
- package/dist/board/stage/presses.d.ts +21 -0
- package/dist/board/stage/presses.js +66 -0
- package/dist/board/stage/scene.d.ts +4 -0
- package/dist/board/stage/scene.js +45 -13
- package/dist/board/stage/stage.d.ts +21 -2
- package/dist/board/stage/stage.js +309 -89
- package/dist/board/stage/texture-pool.d.ts +6 -4
- package/dist/board/stage/texture-pool.js +42 -8
- package/dist/board/stage/views.d.ts +1 -1
- package/dist/board/stage/views.js +2 -2
- package/dist/chunks/environment.d.ts +6 -0
- package/dist/chunks/http.d.ts +9 -1
- package/dist/chunks/http.js +791 -4
- package/dist/index.d.ts +52 -1
- package/dist/index.js +65 -733
- package/dist/protocol/dist/board-model.d.ts +15 -1
- package/dist/protocol/dist/board-model.js +12 -2
- package/dist/protocol/dist/board-schema.d.ts +2 -1
- package/dist/protocol/dist/board-schema.js +4 -1
- package/dist/protocol/dist/core/media.js +4 -0
- package/dist/protocol/dist/index.d.ts +2 -2
- package/package.json +2 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BOARD_FONT_STACK, BOARD_MONO_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
|
+
import { boardItemPreview } from "../../../protocol/dist/board-model.js";
|
|
2
3
|
import { fileCategory, fileStem } from "../../model/file-snapshot.js";
|
|
3
4
|
import { fileCategoryAccent, fileMetaLine, filePreviewKind, fileTypeLabel } from "../../model/file-preview.js";
|
|
4
5
|
import { boardCardDetailed } from "../lod.js";
|
|
@@ -7,7 +8,7 @@ import { syncTextResolution } from "../text-resolution.js";
|
|
|
7
8
|
import { createCardTag, syncCardTag, zoomCardTag } from "../card-tag.js";
|
|
8
9
|
import { cardElevation, containRect, drawCardSurface, traceCardOutline } from "../surface.js";
|
|
9
10
|
import { positionShell } from "./base-card-renderer.js";
|
|
10
|
-
import { drawFarPlate } from "./far-plate.js";
|
|
11
|
+
import { drawFarPlate, farPictureTone } from "./far-plate.js";
|
|
11
12
|
import { Container, Graphics, Sprite, Text, Texture } from "pixi.js";
|
|
12
13
|
//#region src/board/render/renderers/file-card-renderer.ts
|
|
13
14
|
const PADDING = 10;
|
|
@@ -27,8 +28,34 @@ const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
|
27
28
|
function detailFor(zoom) {
|
|
28
29
|
return boardCardDetailed(zoom) ? "full" : "title";
|
|
29
30
|
}
|
|
30
|
-
function
|
|
31
|
-
if (
|
|
31
|
+
function viewOf(item) {
|
|
32
|
+
if (item.type === "file") {
|
|
33
|
+
const { src, snapshot } = item.props;
|
|
34
|
+
return {
|
|
35
|
+
src,
|
|
36
|
+
title: snapshot?.title || fileStem(src),
|
|
37
|
+
excerpt: snapshot?.excerpt ?? "",
|
|
38
|
+
kind: filePreviewKind(snapshot),
|
|
39
|
+
meta: fileMetaLine(src, snapshot?.size),
|
|
40
|
+
mark: fileTypeLabel(src),
|
|
41
|
+
category: fileCategory(src, snapshot?.mimeType)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const preview = boardItemPreview(item);
|
|
45
|
+
if (!preview) return null;
|
|
46
|
+
const label = (item.type.split(".").pop() ?? item.type).toUpperCase();
|
|
47
|
+
return {
|
|
48
|
+
src: preview.src ?? null,
|
|
49
|
+
title: preview.title || item.type,
|
|
50
|
+
excerpt: preview.excerpt ?? "",
|
|
51
|
+
kind: preview.cover ? "cover" : preview.excerpt ? "text" : "blank",
|
|
52
|
+
meta: label,
|
|
53
|
+
mark: label,
|
|
54
|
+
category: preview.src ? fileCategory(preview.src) : "other"
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function coverHeight(view, height) {
|
|
58
|
+
if (view.kind !== "cover") return 0;
|
|
32
59
|
const ideal = Math.round(height * COVER_RATIO);
|
|
33
60
|
return Math.max(0, Math.min(ideal, height - MIN_BODY_FOR_COVER));
|
|
34
61
|
}
|
|
@@ -47,31 +74,31 @@ function syncClip(parts, rect, style) {
|
|
|
47
74
|
}
|
|
48
75
|
function sync(container, item, context) {
|
|
49
76
|
const parts = partsByContainer.get(container);
|
|
50
|
-
|
|
51
|
-
|
|
77
|
+
const view = viewOf(item);
|
|
78
|
+
if (!parts || !view) return;
|
|
79
|
+
syncContent(parts, item, view, context);
|
|
52
80
|
for (const text of [
|
|
53
81
|
parts.title,
|
|
54
82
|
parts.excerpt,
|
|
55
83
|
parts.typeMark
|
|
56
84
|
]) syncTextResolution(text, context.textResolution);
|
|
57
85
|
}
|
|
58
|
-
function syncContent(parts, item, context) {
|
|
86
|
+
function syncContent(parts, item, view, context) {
|
|
59
87
|
positionShell(parts.root, item);
|
|
60
88
|
const { width, height } = item.frame;
|
|
61
89
|
const detail = detailFor(context.zoom);
|
|
62
90
|
const elevation = cardElevation(context, item.id);
|
|
63
91
|
syncCardTag(parts.tag, {
|
|
64
92
|
glyph: "file",
|
|
65
|
-
text:
|
|
93
|
+
text: view.meta,
|
|
66
94
|
width
|
|
67
95
|
}, context);
|
|
68
96
|
const key = context.assetKey(item);
|
|
69
97
|
const texture = key ? context.getTexture(key) : null;
|
|
70
98
|
const coverFailed = Boolean(key && !texture && context.hasError(key));
|
|
71
|
-
const fileState = context.fileState(
|
|
72
|
-
const band = coverHeight(
|
|
73
|
-
const kind =
|
|
74
|
-
const category = fileCategory(item.props.src, item.props.snapshot?.mimeType);
|
|
99
|
+
const fileState = view.src ? context.fileState(view.src) : "ok";
|
|
100
|
+
const band = coverHeight(view, height);
|
|
101
|
+
const { kind, category, title, excerpt, mark } = view;
|
|
75
102
|
const accent = fileCategoryAccent(category, context.palette);
|
|
76
103
|
const visualSig = [
|
|
77
104
|
key ?? "",
|
|
@@ -130,9 +157,6 @@ function syncContent(parts, item, context) {
|
|
|
130
157
|
parts.excerpt.visible = detail === "full";
|
|
131
158
|
parts.typeMark.visible = kind === "blank";
|
|
132
159
|
}
|
|
133
|
-
const title = item.props.snapshot?.title || fileStem(item.props.src);
|
|
134
|
-
const excerpt = item.props.snapshot?.excerpt ?? "";
|
|
135
|
-
const mark = fileTypeLabel(item.props.src);
|
|
136
160
|
const innerWidth = Math.max(1, width - 20);
|
|
137
161
|
const showTypeMark = kind === "blank";
|
|
138
162
|
const textSig = [
|
|
@@ -188,7 +212,7 @@ function syncContent(parts, item, context) {
|
|
|
188
212
|
}
|
|
189
213
|
const fileCardRenderer = {
|
|
190
214
|
id: "file-card",
|
|
191
|
-
canRender: (item) => item.type === "file",
|
|
215
|
+
canRender: (item) => item.type === "file" || boardItemPreview(item) !== null,
|
|
192
216
|
create: (item, context) => {
|
|
193
217
|
const root = new Container();
|
|
194
218
|
const plate = new Graphics();
|
|
@@ -252,23 +276,19 @@ const fileCardRenderer = {
|
|
|
252
276
|
visualSig: "",
|
|
253
277
|
textSig: ""
|
|
254
278
|
});
|
|
255
|
-
|
|
279
|
+
sync(root, item, context);
|
|
256
280
|
return root;
|
|
257
281
|
},
|
|
258
|
-
update:
|
|
259
|
-
|
|
260
|
-
},
|
|
261
|
-
resolution: (container, item, context) => {
|
|
262
|
-
if (item.type === "file") sync(container, item, context);
|
|
263
|
-
},
|
|
282
|
+
update: sync,
|
|
283
|
+
resolution: sync,
|
|
264
284
|
zoom: (container, zoom) => {
|
|
265
285
|
const tag = partsByContainer.get(container)?.tag;
|
|
266
286
|
if (tag) zoomCardTag(tag, zoom);
|
|
267
287
|
},
|
|
268
288
|
renderFar: (graphics, item, context) => {
|
|
269
|
-
const category =
|
|
289
|
+
const category = viewOf(item)?.category ?? "other";
|
|
270
290
|
drawFarPlate(graphics, item.frame, {
|
|
271
|
-
fill: context.palette.surface,
|
|
291
|
+
fill: farPictureTone(item, context) ?? context.palette.surface,
|
|
272
292
|
fillAlpha: .96,
|
|
273
293
|
accent: fileCategoryAccent(category, context.palette),
|
|
274
294
|
accentAlpha: .45
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
|
+
import { BOARD_FRAME_LABEL_LINE_HEIGHT, frameLabelOffset } from "../../model/text-metrics.js";
|
|
2
3
|
import { itemAlpha, itemColor } from "../palette.js";
|
|
3
4
|
import { syncTextResolution } from "../text-resolution.js";
|
|
4
5
|
import { dashPattern, roundRectPoints, tracePolyline, trimPolyline } from "../stroke.js";
|
|
5
6
|
import { createLabel, positionShell } from "./base-card-renderer.js";
|
|
6
7
|
import { drawFarPlate } from "./far-plate.js";
|
|
8
|
+
import "../labels.js";
|
|
7
9
|
import { Container, Graphics } from "pixi.js";
|
|
8
10
|
//#region src/board/render/renderers/frame-card-renderer.ts
|
|
9
11
|
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
@@ -57,19 +59,24 @@ function sync(container, item, context) {
|
|
|
57
59
|
parts.label.visible = decorated;
|
|
58
60
|
const title = locked ? `🔒 ${props.label}`.trim() : props.label;
|
|
59
61
|
const labelColor = style.stroke ? baseStroke : context.palette.muted;
|
|
62
|
+
const size = context.style.frameLabelSize;
|
|
60
63
|
const textSig = [
|
|
61
64
|
title,
|
|
62
65
|
labelColor,
|
|
63
|
-
context.style.font
|
|
66
|
+
context.style.font,
|
|
67
|
+
size
|
|
64
68
|
].join("|");
|
|
65
69
|
if (textSig !== parts.textSig) {
|
|
66
70
|
parts.textSig = textSig;
|
|
67
71
|
parts.label.text = title;
|
|
68
72
|
parts.label.style.fill = labelColor;
|
|
69
73
|
parts.label.style.fontFamily = context.style.font;
|
|
74
|
+
parts.label.style.fontSize = size;
|
|
75
|
+
parts.label.style.lineHeight = size * BOARD_FRAME_LABEL_LINE_HEIGHT;
|
|
76
|
+
const offset = frameLabelOffset(size);
|
|
77
|
+
parts.label.position.set(offset.x, offset.y);
|
|
70
78
|
}
|
|
71
79
|
syncTextResolution(parts.label, context.textResolution);
|
|
72
|
-
parts.label.position.set(4, -18);
|
|
73
80
|
}
|
|
74
81
|
const frameCardRenderer = {
|
|
75
82
|
id: "frame-card",
|
|
@@ -80,7 +87,6 @@ const frameCardRenderer = {
|
|
|
80
87
|
const label = createLabel("", {
|
|
81
88
|
fill: context.palette.muted,
|
|
82
89
|
fontFamily: BOARD_FONT_STACK,
|
|
83
|
-
fontSize: 12,
|
|
84
90
|
fontWeight: "600"
|
|
85
91
|
}, context.textResolution);
|
|
86
92
|
root.addChild(box, label);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cardElevation, containRect, drawCardSurface, fillCardPicture } from "../surface.js";
|
|
2
2
|
import { positionShell } from "./base-card-renderer.js";
|
|
3
|
-
import { drawFarPlate } from "./far-plate.js";
|
|
3
|
+
import { drawFarPlate, farPictureTone } from "./far-plate.js";
|
|
4
4
|
import { Container, Graphics } from "pixi.js";
|
|
5
5
|
//#region src/board/render/renderers/image-card-renderer.ts
|
|
6
6
|
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
@@ -137,7 +137,7 @@ const imageCardRenderer = {
|
|
|
137
137
|
lod: { kind: "media" },
|
|
138
138
|
renderFar: (graphics, item, context) => {
|
|
139
139
|
drawFarPlate(graphics, item.frame, {
|
|
140
|
-
fill: context.palette.hover,
|
|
140
|
+
fill: farPictureTone(item, context) ?? context.palette.hover,
|
|
141
141
|
fillAlpha: FAR_ALPHA
|
|
142
142
|
});
|
|
143
143
|
},
|
|
@@ -5,7 +5,7 @@ import { boardCardDetailed } from "../lod.js";
|
|
|
5
5
|
import { createCardTag, syncCardTag, traceCube, zoomCardTag } from "../card-tag.js";
|
|
6
6
|
import { cardElevation, containRect, drawCardSurface } from "../surface.js";
|
|
7
7
|
import { positionShell } from "./base-card-renderer.js";
|
|
8
|
-
import { drawFarPlate } from "./far-plate.js";
|
|
8
|
+
import { drawFarPlate, farPictureTone } from "./far-plate.js";
|
|
9
9
|
import { drawModelKey } from "./media-key.js";
|
|
10
10
|
import { Container, Graphics, Sprite, Texture } from "pixi.js";
|
|
11
11
|
//#region src/board/render/renderers/model-card-renderer.ts
|
|
@@ -120,7 +120,7 @@ const modelCardRenderer = {
|
|
|
120
120
|
},
|
|
121
121
|
renderFar: (graphics, item, context) => {
|
|
122
122
|
drawFarPlate(graphics, item.frame, {
|
|
123
|
-
fill: context.palette.surface,
|
|
123
|
+
fill: farPictureTone(item, context) ?? context.palette.surface,
|
|
124
124
|
fillAlpha: .96,
|
|
125
125
|
accent: context.palette.legendary,
|
|
126
126
|
accentAlpha: .45
|
|
@@ -7,7 +7,7 @@ import { createCardTag, formatBoardDuration, syncCardTag, zoomCardTag } from "..
|
|
|
7
7
|
import { drawAudioWaveform } from "../audio-waveform.js";
|
|
8
8
|
import { cardElevation, containRect, drawCardSurface, traceCardOutline } from "../surface.js";
|
|
9
9
|
import { positionShell } from "./base-card-renderer.js";
|
|
10
|
-
import { drawFarPlate } from "./far-plate.js";
|
|
10
|
+
import { drawFarPlate, farPictureTone } from "./far-plate.js";
|
|
11
11
|
import { Container, FillGradient, Graphics, Sprite, Text } from "pixi.js";
|
|
12
12
|
//#region src/board/render/renderers/task-card-renderer.ts
|
|
13
13
|
const PADDING = 12;
|
|
@@ -512,7 +512,7 @@ const taskCardRenderer = {
|
|
|
512
512
|
const { status } = liveStatus(item, context);
|
|
513
513
|
const active = status === "failed" || status === "running" || status === "pending";
|
|
514
514
|
drawFarPlate(graphics, item.frame, {
|
|
515
|
-
fill: context.palette.surface,
|
|
515
|
+
fill: farPictureTone(item, context) ?? context.palette.surface,
|
|
516
516
|
fillAlpha: .82,
|
|
517
517
|
...active ? {
|
|
518
518
|
accent: statusColor(status, context),
|
|
@@ -3,7 +3,7 @@ import { boardCardDetailed } from "../lod.js";
|
|
|
3
3
|
import { createCardTag, formatBoardDuration, syncCardTag, zoomCardTag } from "../card-tag.js";
|
|
4
4
|
import { cardElevation, containRect, drawCardSurface, fillCardPicture } from "../surface.js";
|
|
5
5
|
import { positionShell } from "./base-card-renderer.js";
|
|
6
|
-
import { drawFarPlate } from "./far-plate.js";
|
|
6
|
+
import { drawFarPlate, farPictureTone } from "./far-plate.js";
|
|
7
7
|
import { drawMediaPlayKey } from "./media-key.js";
|
|
8
8
|
import { Container, Graphics } from "pixi.js";
|
|
9
9
|
//#region src/board/render/renderers/video-card-renderer.ts
|
|
@@ -125,7 +125,7 @@ const videoCardRenderer = {
|
|
|
125
125
|
lod: { kind: "media" },
|
|
126
126
|
renderFar: (graphics, item, context) => {
|
|
127
127
|
drawFarPlate(graphics, item.frame, {
|
|
128
|
-
fill: context.palette.surface,
|
|
128
|
+
fill: farPictureTone(item, context) ?? context.palette.surface,
|
|
129
129
|
fillAlpha: .96,
|
|
130
130
|
accent: context.palette.brand,
|
|
131
131
|
accentAlpha: .75
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BOARD_FONT_STACK } from "../../protocol/dist/board-constants.js";
|
|
2
|
+
import "../model/text-metrics.js";
|
|
2
3
|
//#region src/board/render/style.ts
|
|
3
4
|
const BASE = {
|
|
4
5
|
corner: "round",
|
|
@@ -14,7 +15,8 @@ const BASE = {
|
|
|
14
15
|
progress: "shimmer",
|
|
15
16
|
icons: "line",
|
|
16
17
|
motion: "smooth",
|
|
17
|
-
font: BOARD_FONT_STACK
|
|
18
|
+
font: BOARD_FONT_STACK,
|
|
19
|
+
frameLabelSize: 12
|
|
18
20
|
};
|
|
19
21
|
const SHADOW = {
|
|
20
22
|
light: {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { BoardViewport } from "../model/geometry.js";
|
|
2
|
+
import { BoardScene, BoardSceneItem } from "../model/scene.js";
|
|
3
|
+
import { BoardRenderStyle } from "./style.js";
|
|
4
|
+
import { BoardTextStyle } from "./text-measurement.js";
|
|
5
|
+
//#region src/board/render/text-edit.d.ts
|
|
6
|
+
/** A content-box textarea at left/top, transformed by `rotate(rotation deg) scale(scale)` about its center. */
|
|
7
|
+
type BoardTextEditLayout = {
|
|
8
|
+
left: number;
|
|
9
|
+
top: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
padding: {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
};
|
|
16
|
+
scale: number;
|
|
17
|
+
rotation: number;
|
|
18
|
+
font: BoardTextStyle;
|
|
19
|
+
lineHeight: number;
|
|
20
|
+
textAlign: "left" | "center" | "right";
|
|
21
|
+
whiteSpace: "pre" | "pre-wrap";
|
|
22
|
+
live: boolean;
|
|
23
|
+
};
|
|
24
|
+
type BoardTextEditInput = {
|
|
25
|
+
viewport: BoardViewport;
|
|
26
|
+
scene: BoardScene;
|
|
27
|
+
style?: Partial<Pick<BoardRenderStyle, "font" | "frameLabelSize">>;
|
|
28
|
+
draft: string;
|
|
29
|
+
};
|
|
30
|
+
declare function boardTextEditLayout(item: BoardSceneItem, { viewport, scene, style, draft }: BoardTextEditInput): BoardTextEditLayout | null;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { BoardTextEditInput, BoardTextEditLayout, boardTextEditLayout };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { BOARD_FONT_STACK } from "../../protocol/dist/board-constants.js";
|
|
2
|
+
import { layoutBoardText } from "../../protocol/dist/board-layout.js";
|
|
3
|
+
import { degToRad, rotatePointAround, worldPoint, worldToScreen } from "../model/geometry.js";
|
|
4
|
+
import { resolveSceneArrow } from "../model/arrow-geometry.js";
|
|
5
|
+
import { BOARD_FRAME_LABEL_LINE_HEIGHT, frameLabelOffset, layoutShapeLabel } from "../model/text-metrics.js";
|
|
6
|
+
import { boardTextStyle, canvasFont, canvasFontFamily, layoutTextLines } from "./text-measurement.js";
|
|
7
|
+
import { ARROW_LABEL, arrowLabelAnchor } from "./labels.js";
|
|
8
|
+
//#region src/board/render/text-edit.ts
|
|
9
|
+
function place({ center, ...box }, viewport) {
|
|
10
|
+
const screen = worldToScreen(center, viewport);
|
|
11
|
+
return {
|
|
12
|
+
left: screen.x - box.width / 2 - box.padding.x,
|
|
13
|
+
top: screen.y - box.height / 2 - box.padding.y,
|
|
14
|
+
...box
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function label(draft, font, lineHeight) {
|
|
18
|
+
const { lines, width } = layoutTextLines(draft, canvasFont(font));
|
|
19
|
+
return {
|
|
20
|
+
width: Math.max(width, font.fontSize * 4),
|
|
21
|
+
height: lines.length * lineHeight
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function boardTextEditLayout(item, { viewport, scene, style, draft }) {
|
|
25
|
+
const { frame } = item;
|
|
26
|
+
const rotation = frame.rotation || 0;
|
|
27
|
+
const center = worldPoint(frame.x + frame.width / 2, frame.y + frame.height / 2);
|
|
28
|
+
const { zoom } = viewport;
|
|
29
|
+
if (item.type === "text") {
|
|
30
|
+
const { props } = item;
|
|
31
|
+
const text = layoutBoardText({
|
|
32
|
+
...props,
|
|
33
|
+
text: draft
|
|
34
|
+
});
|
|
35
|
+
const scale = zoom * frame.width / text.width;
|
|
36
|
+
return place({
|
|
37
|
+
center,
|
|
38
|
+
width: text.width,
|
|
39
|
+
height: text.height,
|
|
40
|
+
padding: {
|
|
41
|
+
x: 1 / scale,
|
|
42
|
+
y: 0
|
|
43
|
+
},
|
|
44
|
+
scale,
|
|
45
|
+
rotation,
|
|
46
|
+
font: boardTextStyle(props),
|
|
47
|
+
lineHeight: text.lineHeight,
|
|
48
|
+
textAlign: props.align ?? "left",
|
|
49
|
+
whiteSpace: props.width ? "pre-wrap" : "pre",
|
|
50
|
+
live: true
|
|
51
|
+
}, viewport);
|
|
52
|
+
}
|
|
53
|
+
if (item.type === "shape") {
|
|
54
|
+
const { props } = item;
|
|
55
|
+
const text = layoutShapeLabel({
|
|
56
|
+
text: draft,
|
|
57
|
+
fontSize: props.fontSize
|
|
58
|
+
}, frame.width);
|
|
59
|
+
return place({
|
|
60
|
+
center,
|
|
61
|
+
width: Math.max(0, frame.width - 16),
|
|
62
|
+
height: Math.min(frame.height, text.height),
|
|
63
|
+
padding: {
|
|
64
|
+
x: 8,
|
|
65
|
+
y: Math.max(0, (frame.height - text.height) / 2)
|
|
66
|
+
},
|
|
67
|
+
scale: zoom,
|
|
68
|
+
rotation,
|
|
69
|
+
font: boardTextStyle({
|
|
70
|
+
font: "sans",
|
|
71
|
+
fontSize: props.fontSize,
|
|
72
|
+
fontWeight: 500
|
|
73
|
+
}),
|
|
74
|
+
lineHeight: text.lineHeight,
|
|
75
|
+
textAlign: props.align ?? "center",
|
|
76
|
+
whiteSpace: "pre-wrap",
|
|
77
|
+
live: false
|
|
78
|
+
}, viewport);
|
|
79
|
+
}
|
|
80
|
+
if (item.type === "frame") {
|
|
81
|
+
const fontSize = style?.frameLabelSize ?? 12;
|
|
82
|
+
const lineHeight = fontSize * BOARD_FRAME_LABEL_LINE_HEIGHT;
|
|
83
|
+
const font = {
|
|
84
|
+
fontFamily: canvasFontFamily(style?.font ?? "\"Geist Variable\", \"Geist\", \"Noto Sans CJK SC\", \"Noto Sans SC\", \"PingFang SC\", \"Microsoft YaHei\", system-ui, -apple-system, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Noto Color Emoji\", \"Noto Sans Math\", \"Noto Sans Symbols 2\", \"DejaVu Sans\", \"FreeSans\", \"FreeSerif\", sans-serif"),
|
|
85
|
+
fontSize,
|
|
86
|
+
fontWeight: "600"
|
|
87
|
+
};
|
|
88
|
+
const size = label(draft, font, lineHeight);
|
|
89
|
+
const { x, y } = frameLabelOffset(fontSize);
|
|
90
|
+
const local = worldPoint(frame.x + x + size.width / 2, frame.y + y + size.height / 2);
|
|
91
|
+
return place({
|
|
92
|
+
center: rotation ? rotatePointAround(local, center, degToRad(rotation)) : local,
|
|
93
|
+
...size,
|
|
94
|
+
padding: {
|
|
95
|
+
x: 4,
|
|
96
|
+
y: 2
|
|
97
|
+
},
|
|
98
|
+
scale: zoom,
|
|
99
|
+
rotation,
|
|
100
|
+
font,
|
|
101
|
+
lineHeight,
|
|
102
|
+
textAlign: "left",
|
|
103
|
+
whiteSpace: "pre",
|
|
104
|
+
live: false
|
|
105
|
+
}, viewport);
|
|
106
|
+
}
|
|
107
|
+
if (item.type === "arrow") {
|
|
108
|
+
const { fontSize } = item.props;
|
|
109
|
+
const font = {
|
|
110
|
+
fontFamily: canvasFontFamily(BOARD_FONT_STACK),
|
|
111
|
+
fontSize,
|
|
112
|
+
fontWeight: ARROW_LABEL.fontWeight
|
|
113
|
+
};
|
|
114
|
+
const lineHeight = fontSize * ARROW_LABEL.lineHeight;
|
|
115
|
+
return place({
|
|
116
|
+
center: arrowLabelAnchor(resolveSceneArrow(item, scene), item.style),
|
|
117
|
+
...label(draft, font, lineHeight),
|
|
118
|
+
padding: {
|
|
119
|
+
x: fontSize * ARROW_LABEL.padX,
|
|
120
|
+
y: fontSize * ARROW_LABEL.padY
|
|
121
|
+
},
|
|
122
|
+
scale: zoom,
|
|
123
|
+
rotation: 0,
|
|
124
|
+
font,
|
|
125
|
+
lineHeight,
|
|
126
|
+
textAlign: "center",
|
|
127
|
+
whiteSpace: "pre",
|
|
128
|
+
live: false
|
|
129
|
+
}, viewport);
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
export { boardTextEditLayout };
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { Text } from "pixi.js";
|
|
2
2
|
//#region src/board/render/text-fit.d.ts
|
|
3
|
-
declare function ellipsizeWrappedLines(lines: string[], maxLines: number): string;
|
|
4
|
-
declare function fitLineWithEllipsis(line: string, wrapWidth: number, measureWidth: (value: string) => number): string;
|
|
5
3
|
declare function fitTextToLines(text: Text, value: string, maxLines: number, wrapWidth: number): void;
|
|
6
4
|
declare function fitTextToWidth(text: Text, value: string, width: number): void;
|
|
7
5
|
//#endregion
|
|
8
|
-
export {
|
|
6
|
+
export { fitTextToLines, fitTextToWidth };
|
|
@@ -1,49 +1,10 @@
|
|
|
1
|
-
import { layoutTextLines } from "./text-measurement.js";
|
|
1
|
+
import { fitLineWithEllipsis, layoutTextLines } from "./text-measurement.js";
|
|
2
2
|
import { CanvasTextMetrics, fontStringFromTextStyle } from "pixi.js";
|
|
3
3
|
//#region src/board/render/text-fit.ts
|
|
4
|
-
function ellipsizeWrappedLines(lines, maxLines) {
|
|
5
|
-
if (maxLines <= 0 || lines.length === 0) return "";
|
|
6
|
-
if (lines.length <= maxLines) return lines.join("\n");
|
|
7
|
-
const kept = lines.slice(0, maxLines);
|
|
8
|
-
const lastIndex = kept.length - 1;
|
|
9
|
-
const last = (kept[lastIndex] ?? "").replace(/\s+$/u, "");
|
|
10
|
-
kept[lastIndex] = last ? `${last}…` : "…";
|
|
11
|
-
return kept.join("\n");
|
|
12
|
-
}
|
|
13
|
-
function fitLineWithEllipsis(line, wrapWidth, measureWidth) {
|
|
14
|
-
const trimmed = line.replace(/\s+$/u, "");
|
|
15
|
-
if (!trimmed) return "…";
|
|
16
|
-
const full = `${trimmed}…`;
|
|
17
|
-
if (measureWidth(full) <= wrapWidth) return full;
|
|
18
|
-
let lo = 0;
|
|
19
|
-
let hi = trimmed.length;
|
|
20
|
-
let best = "…";
|
|
21
|
-
while (lo <= hi) {
|
|
22
|
-
const mid = lo + hi >> 1;
|
|
23
|
-
const candidate = mid > 0 ? `${trimmed.slice(0, mid)}…` : "…";
|
|
24
|
-
if (measureWidth(candidate) <= wrapWidth) {
|
|
25
|
-
best = candidate;
|
|
26
|
-
lo = mid + 1;
|
|
27
|
-
} else hi = mid - 1;
|
|
28
|
-
}
|
|
29
|
-
return best;
|
|
30
|
-
}
|
|
31
4
|
function fitTextToLines(text, value, maxLines, wrapWidth) {
|
|
32
|
-
const width = Math.max(1, wrapWidth);
|
|
33
5
|
text.style.wordWrap = false;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
const charCap = Math.max(maxLines * Math.ceil(width), maxLines * 4);
|
|
39
|
-
const sample = value.length > charCap ? value.slice(0, charCap) : value;
|
|
40
|
-
const { lines } = layoutTextLines(sample, fontStringFromTextStyle(text.style), width);
|
|
41
|
-
const kept = lines.slice(0, maxLines);
|
|
42
|
-
if (lines.length > maxLines || sample !== value) {
|
|
43
|
-
const lastIndex = kept.length - 1;
|
|
44
|
-
kept[lastIndex] = fitLineWithEllipsis(kept[lastIndex] ?? "", width, (candidate) => CanvasTextMetrics.measureText(candidate, text.style).width);
|
|
45
|
-
}
|
|
46
|
-
const fitted = kept.join("\n");
|
|
6
|
+
const { lines } = layoutTextLines(value, fontStringFromTextStyle(text.style), Math.max(1, wrapWidth), { maxLines });
|
|
7
|
+
const fitted = lines.join("\n");
|
|
47
8
|
if (text.text !== fitted) text.text = fitted;
|
|
48
9
|
}
|
|
49
10
|
function fitTextToWidth(text, value, width) {
|
|
@@ -53,4 +14,4 @@ function fitTextToWidth(text, value, width) {
|
|
|
53
14
|
if (text.text !== fitted) text.text = fitted;
|
|
54
15
|
}
|
|
55
16
|
//#endregion
|
|
56
|
-
export {
|
|
17
|
+
export { fitTextToLines, fitTextToWidth };
|
|
@@ -15,10 +15,20 @@ type BoardTextStyle = {
|
|
|
15
15
|
declare function canvasFontFamily(families: string): string;
|
|
16
16
|
declare function boardTextStyle({ font, fontSize, fontWeight }: BoardTextFont): BoardTextStyle;
|
|
17
17
|
declare function canvasFont({ fontFamily, fontSize, fontWeight }: BoardTextStyle): string;
|
|
18
|
-
declare function
|
|
18
|
+
declare function measureTextWidth(text: string, font: string): number;
|
|
19
|
+
type BoardTextWhiteSpace = "normal" | "pre-wrap";
|
|
20
|
+
type BoardTextLineOptions = {
|
|
21
|
+
whiteSpace?: BoardTextWhiteSpace;
|
|
22
|
+
maxLines?: number;
|
|
23
|
+
};
|
|
24
|
+
type BoardTextLineLayout = BoardTextLines & {
|
|
25
|
+
truncated: boolean;
|
|
26
|
+
};
|
|
27
|
+
declare function layoutTextLines(text: string, font: string, maxWidth?: number, { whiteSpace, maxLines }?: BoardTextLineOptions): BoardTextLineLayout;
|
|
28
|
+
declare function fitLineWithEllipsis(line: string, width: number, measureWidth: (value: string) => number): string;
|
|
19
29
|
declare function cssBaselineShift(style: TextStyle): number;
|
|
20
30
|
declare function installBoardTextMeasurement(): void;
|
|
21
31
|
declare function ensureBoardTextMeasurement(): void;
|
|
22
32
|
declare function onBoardTextFontsChange(listener: () => void): () => void;
|
|
23
33
|
//#endregion
|
|
24
|
-
export { BOARD_FONT_STACKS, BoardTextStyle, boardTextStyle, canvasFont, canvasFontFamily, cssBaselineShift, ensureBoardTextMeasurement, installBoardTextMeasurement, layoutTextLines, onBoardTextFontsChange };
|
|
34
|
+
export { BOARD_FONT_STACKS, BoardTextLineLayout, BoardTextLineOptions, BoardTextStyle, BoardTextWhiteSpace, boardTextStyle, canvasFont, canvasFontFamily, cssBaselineShift, ensureBoardTextMeasurement, fitLineWithEllipsis, installBoardTextMeasurement, layoutTextLines, measureTextWidth, onBoardTextFontsChange };
|
|
@@ -43,8 +43,8 @@ const measuredFonts = /* @__PURE__ */ new Set();
|
|
|
43
43
|
const measuredFamilies = /* @__PURE__ */ new Set();
|
|
44
44
|
const TRAILING_SPACE = /[ \t]+$/u;
|
|
45
45
|
const unquote = (name) => name.trim().replace(/^["']|["']$/gu, "");
|
|
46
|
-
function prepareText(text, font) {
|
|
47
|
-
const key = `${font}\n${text}`;
|
|
46
|
+
function prepareText(text, font, whiteSpace) {
|
|
47
|
+
const key = `${whiteSpace}\n${font}\n${text}`;
|
|
48
48
|
let value = prepared.get(key);
|
|
49
49
|
if (value) {
|
|
50
50
|
prepared.delete(key);
|
|
@@ -55,30 +55,75 @@ function prepareText(text, font) {
|
|
|
55
55
|
measuredFonts.add(font);
|
|
56
56
|
for (const name of /px\s+(.+)$/u.exec(font)?.[1]?.split(",") ?? []) measuredFamilies.add(unquote(name));
|
|
57
57
|
}
|
|
58
|
-
value = prepareWithSegments(text, font, { whiteSpace
|
|
58
|
+
value = prepareWithSegments(text, font, { whiteSpace });
|
|
59
59
|
if (prepared.size >= PREPARED_LIMIT) prepared.delete(prepared.keys().next().value);
|
|
60
60
|
prepared.set(key, value);
|
|
61
61
|
return value;
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
let measureContext;
|
|
64
|
+
function measureTextWidth(text, font) {
|
|
65
|
+
measureContext ??= DOMAdapter.get().createCanvas(1, 1).getContext("2d");
|
|
66
|
+
if (!measureContext) return 0;
|
|
67
|
+
measureContext.font = font;
|
|
68
|
+
return measureContext.measureText(text).width;
|
|
69
|
+
}
|
|
70
|
+
function layoutTextLines(text, font, maxWidth = Number.POSITIVE_INFINITY, { whiteSpace = "pre-wrap", maxLines = Number.POSITIVE_INFINITY } = {}) {
|
|
64
71
|
const lines = [];
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
const widths = [];
|
|
73
|
+
if (maxLines <= 0) return {
|
|
74
|
+
lines,
|
|
75
|
+
width: 0,
|
|
76
|
+
truncated: text.length > 0
|
|
77
|
+
};
|
|
78
|
+
let source = whiteSpace === "normal" ? text.replace(/\s+/gu, " ").trim() : text.replace(/\r\n?/gu, "\n").replaceAll(" ", " ");
|
|
79
|
+
const cap = Number.isFinite(maxLines) && Number.isFinite(maxWidth) ? maxLines * (Math.ceil(maxWidth) + 1) : Number.POSITIVE_INFINITY;
|
|
80
|
+
let truncated = source.length > cap;
|
|
81
|
+
if (truncated) source = source.slice(0, cap);
|
|
82
|
+
const paragraphs = whiteSpace === "normal" ? source ? [source] : [] : source.split("\n");
|
|
83
|
+
for (const paragraph of paragraphs) {
|
|
84
|
+
if (lines.length > maxLines) break;
|
|
67
85
|
if (!paragraph) {
|
|
68
86
|
lines.push("");
|
|
87
|
+
widths.push(0);
|
|
69
88
|
continue;
|
|
70
89
|
}
|
|
71
|
-
const wrapped = layoutWithLines(prepareText(paragraph, font), maxWidth, 1).lines;
|
|
90
|
+
const wrapped = layoutWithLines(prepareText(paragraph, font, whiteSpace), maxWidth, 1).lines;
|
|
72
91
|
for (const [index, line] of wrapped.entries()) {
|
|
73
92
|
lines.push(index === wrapped.length - 1 ? line.text : line.text.replace(TRAILING_SPACE, ""));
|
|
74
|
-
|
|
93
|
+
widths.push(line.width);
|
|
75
94
|
}
|
|
76
95
|
}
|
|
96
|
+
if (lines.length > maxLines) {
|
|
97
|
+
truncated = true;
|
|
98
|
+
lines.length = maxLines;
|
|
99
|
+
widths.length = maxLines;
|
|
100
|
+
}
|
|
101
|
+
if (truncated && lines.length > 0) {
|
|
102
|
+
const last = lines.length - 1;
|
|
103
|
+
lines[last] = fitLineWithEllipsis(lines[last], maxWidth, (value) => measureTextWidth(value, font));
|
|
104
|
+
widths[last] = measureTextWidth(lines[last], font);
|
|
105
|
+
}
|
|
77
106
|
return {
|
|
78
107
|
lines,
|
|
79
|
-
width
|
|
108
|
+
width: widths.reduce((max, width) => Math.max(max, width), 0),
|
|
109
|
+
truncated
|
|
80
110
|
};
|
|
81
111
|
}
|
|
112
|
+
const graphemes = typeof Intl.Segmenter === "function" ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
|
|
113
|
+
function fitLineWithEllipsis(line, width, measureWidth) {
|
|
114
|
+
const trimmed = line.replace(/\s+$/u, "");
|
|
115
|
+
if (!trimmed) return "…";
|
|
116
|
+
if (measureWidth(`${trimmed}…`) <= width) return `${trimmed}…`;
|
|
117
|
+
const parts = graphemes ? Array.from(graphemes.segment(trimmed), ({ segment }) => segment) : Array.from(trimmed);
|
|
118
|
+
let lo = 0;
|
|
119
|
+
let hi = parts.length - 1;
|
|
120
|
+
while (lo < hi) {
|
|
121
|
+
const mid = lo + hi + 1 >> 1;
|
|
122
|
+
if (measureWidth(`${parts.slice(0, mid).join("")}…`) <= width) lo = mid;
|
|
123
|
+
else hi = mid - 1;
|
|
124
|
+
}
|
|
125
|
+
return `${parts.slice(0, lo).join("")}…`;
|
|
126
|
+
}
|
|
82
127
|
const baselineShifts = /* @__PURE__ */ new Map();
|
|
83
128
|
function cssBaselineShift(style) {
|
|
84
129
|
const font = fontStringFromTextStyle(style);
|
|
@@ -128,4 +173,4 @@ function onBoardTextFontsChange(listener) {
|
|
|
128
173
|
return () => fontListeners.delete(listener);
|
|
129
174
|
}
|
|
130
175
|
//#endregion
|
|
131
|
-
export { BOARD_FONT_STACKS, boardTextStyle, canvasFont, canvasFontFamily, cssBaselineShift, ensureBoardTextMeasurement, installBoardTextMeasurement, layoutTextLines, onBoardTextFontsChange };
|
|
176
|
+
export { BOARD_FONT_STACKS, boardTextStyle, canvasFont, canvasFontFamily, cssBaselineShift, ensureBoardTextMeasurement, fitLineWithEllipsis, installBoardTextMeasurement, layoutTextLines, measureTextWidth, onBoardTextFontsChange };
|