@lingfan-engine/shell 0.0.0-stage → 0.1.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.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * 历史面板的数据整形:引擎给出的历史条目 → 可直接渲染的块列表。
3
+ *
4
+ * 连续 NVL 条目聚合成一个块——玩家所见本来就是整块累积画面,块级回溯 =
5
+ * 回到「块尾检查点」(整块可见的时刻,检查点语义的自然延伸);buffer 收缩
6
+ * (nvl clear)即分段。回溯粒度不减:逐检查点仍全在历史里,只是呈现聚合。
7
+ *
8
+ * 本模块平台无关:不触碰任何框架、DOM 或宿主全局。
9
+ */
10
+
11
+ /** 一条历史条目(引擎 `historyView` 的行) */
12
+ export interface HistoryEntry {
13
+ /** 回溯目标:引擎检查点序号 */
14
+ index: number;
15
+ /** 说话人(空串 = 无) */
16
+ speaker: string;
17
+ /** 正文(非 NVL 模式下的一行) */
18
+ text: string;
19
+ /** 是否 NVL 累积模式下的行 */
20
+ nvl: boolean;
21
+ /** NVL 模式下的整块行(回溯目标为块尾) */
22
+ nvlLines: string[];
23
+ }
24
+
25
+ /** 呈现块:连续 NVL 聚合后的一行 */
26
+ export interface HistoryBlock {
27
+ /** 块首条目 index(稳定,避免重建) */
28
+ key: string;
29
+ /** 回溯目标 = 块尾检查点 */
30
+ index: number;
31
+ speaker: string;
32
+ /** 该块要显示的正文行 */
33
+ lines: string[];
34
+ nvl: boolean;
35
+ }
36
+
37
+ /** 丢掉说话人与正文都空的行(不构成一次可回溯的呈现) */
38
+ export function filterHistoryEntries(
39
+ entries: readonly HistoryEntry[],
40
+ ): HistoryEntry[] {
41
+ return entries.filter((h) => h.text !== "" || h.speaker !== "");
42
+ }
43
+
44
+ /** 把历史条目聚合成呈现块 */
45
+ export function buildHistoryBlocks(
46
+ entries: readonly HistoryEntry[],
47
+ ): HistoryBlock[] {
48
+ const blocks: HistoryBlock[] = [];
49
+ for (const entry of entries) {
50
+ const prev = blocks[blocks.length - 1];
51
+ // 同段延续:块尾推进为最新检查点;行数变少说明 buffer 已收缩(分段)
52
+ if (
53
+ entry.nvl &&
54
+ prev?.nvl === true &&
55
+ entry.nvlLines.length >= prev.lines.length
56
+ ) {
57
+ prev.index = entry.index;
58
+ prev.lines = entry.nvlLines;
59
+ continue;
60
+ }
61
+ blocks.push({
62
+ key: `blk-${entry.index}`,
63
+ index: entry.index,
64
+ speaker: entry.speaker,
65
+ lines: entry.nvl ? [...entry.nvlLines] : [entry.text],
66
+ nvl: entry.nvl,
67
+ });
68
+ }
69
+ return blocks;
70
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 面板数据域:历史与槽位面板的数据整形与装配。
3
+ *
4
+ * 面板本身(DOM/组件)属于各平台宿主;这里只做「引擎数据 → 可直接渲染的视图」,
5
+ * 让每个宿主的模板保持零逻辑。
6
+ */
7
+
8
+ export * from "./history.ts";
9
+ export * from "./slotTexts.ts";
10
+ export * from "./slots.ts";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 宿主注入引擎的文案整形:把句子里的槽位名换成面板叫法。
3
+ *
4
+ * 引擎缺省模板把槽位 id 原样嵌进句子(`槽位 ${slot}`),而宿主的槽位 id 是
5
+ * `slot_1` 这种形式,直出就是「槽位 slot_2」——玩家在面板上点的是「槽位 2」,
6
+ * 失败横幅却报一个他从没见过的 id。这里按 `defaultSlotLabel` 改名,让面板怎么
7
+ * 称呼、报错里就怎么称呼。
8
+ *
9
+ * 只摊开引擎缺省表改这两句,其余句子跟着引擎演进:整表手写会在引擎新增句子时
10
+ * 静默漏接。整体改写或本地化玩家文案时改的是底表,这套槽位名规则不动。
11
+ */
12
+ import { type EngineTexts } from "@lingfan-engine/core";
13
+ import { defaultSlotLabel } from "./slots.ts";
14
+
15
+ /** 槽位名与面板同源的引擎文案表(未改写的句子 = 传入底表的原句) */
16
+ export function withSlotLabels(texts: EngineTexts): EngineTexts {
17
+ return {
18
+ ...texts,
19
+ saveWriteFailed: (slot, error) =>
20
+ `${defaultSlotLabel(slot)} 写档失败:${error}`,
21
+ loadReadFailed: (slot, error) =>
22
+ `${defaultSlotLabel(slot)} 读取或解析失败:${error}`,
23
+ };
24
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * 槽位面板的数据装配:把存档端口的清单与逐槽快照整理成可直接渲染的视图。
3
+ *
4
+ * 面板只负责「呈现槽位」:写读与校验由引擎命令面承担(`engine.save` /
5
+ * `engine.load`),此处读取存档端口仅为取标题与缩略图;个别槽读取失败只丢这两项,
6
+ * 不影响整张面板列出槽位,真实读档失败由引擎 fail-closed 上报。
7
+ *
8
+ * 本模块平台无关:不触碰任何框架、DOM 或宿主全局。
9
+ */
10
+ import type { SaveDataV1, SavePort } from "@lingfan-engine/core";
11
+
12
+ /** 面板用途:存或读 */
13
+ export type SlotPanelMode = "save" | "load";
14
+
15
+ /** 一个槽位的呈现数据 */
16
+ export interface SlotView {
17
+ /** 槽位 id(引擎命令参数) */
18
+ id: string;
19
+ /** 槽位显示名(与提示条文案同源,见 {@link defaultSlotLabel}) */
20
+ label: string;
21
+ /** 无存档(存档模式仍可点选=覆盖写入;读档模式不可点选,见 {@link canChooseSlot}) */
22
+ empty: boolean;
23
+ /** 存档时间戳(无存档时不呈现) */
24
+ timestamp?: number;
25
+ /** 存档标题(快照里没有则缺省) */
26
+ title?: string;
27
+ /** 缩略图 data URL(快照里没有则缺省) */
28
+ screenshot?: string;
29
+ }
30
+
31
+ /** 槽位装配需要的外部事实 */
32
+ export interface SlotPanelOptions {
33
+ /** 存档端口(清单与逐槽读取) */
34
+ savePort: SavePort;
35
+ /** 本工程的槽位 id 序列(顺序即呈现顺序) */
36
+ slotIds: readonly string[];
37
+ /** 槽位显示名:把槽位 id 变成面板上给玩家看的一行字;缺省 = 去掉 `slot_` 前缀后加「槽位 」 */
38
+ labelFor?(id: string): string;
39
+ }
40
+
41
+ /**
42
+ * 从快照原文里取出标题与缩略图。
43
+ *
44
+ * 单点收敛解析与容错:读到的内容可能不是本版本的存档(或已损坏),
45
+ * 解析失败与字段类型不符都按「没有」处理,不中断面板装配。
46
+ */
47
+ export function parseSlotMeta(raw: string): {
48
+ title?: string;
49
+ screenshot?: string;
50
+ } {
51
+ try {
52
+ const parsed = JSON.parse(raw) as Partial<SaveDataV1>;
53
+ return {
54
+ title: typeof parsed.title === "string" ? parsed.title : undefined,
55
+ screenshot:
56
+ typeof parsed.screenshot === "string" ? parsed.screenshot : undefined,
57
+ };
58
+ } catch {
59
+ return {};
60
+ }
61
+ }
62
+
63
+ /** 装配槽位视图列表 */
64
+ export async function loadSlotViews(
65
+ options: SlotPanelOptions,
66
+ ): Promise<SlotView[]> {
67
+ const labelFor = options.labelFor ?? defaultSlotLabel;
68
+ const summaries = await options.savePort.list().catch(() => []);
69
+ const byId = new Map(summaries.map((s) => [s.slot, s]));
70
+ const views: SlotView[] = [];
71
+ for (const id of options.slotIds) {
72
+ const summary = byId.get(id);
73
+ let meta: { title?: string; screenshot?: string } = {};
74
+ if (summary !== undefined) {
75
+ // 这次读只用来取标题与缩略图,端口拒读(存档策略拒绝旧档、记录损坏等)不该
76
+ // 让整张面板列不出槽位:那种槽位照列,只是没有标题与缩略图。
77
+ meta = await options.savePort.read(id).then(parseSlotMeta, () => ({}));
78
+ }
79
+ views.push({
80
+ id,
81
+ label: labelFor(id),
82
+ empty: summary === undefined,
83
+ timestamp: summary?.timestamp,
84
+ title: meta.title,
85
+ screenshot: meta.screenshot,
86
+ });
87
+ }
88
+ return views;
89
+ }
90
+
91
+ /**
92
+ * 槽位显示名的缺省规则:`slot_1` → `槽位 1`,其余 id 原样透传。
93
+ *
94
+ * 导出是为了让槽位名的规则全仓只有一处:面板的 `labelFor` 与提示条的
95
+ * `labelFor` 都缺省到它。宿主自定义槽位命名时应把同一个函数传给两处,
96
+ * 好让同一次操作在面板与提示条上叫法一致。
97
+ */
98
+ export function defaultSlotLabel(id: string): string {
99
+ return id.startsWith("slot_") ? `槽位 ${id.slice(5)}` : id;
100
+ }
101
+
102
+ /**
103
+ * 槽位是否可点选。
104
+ *
105
+ * 读档模式下的空槽点下去必然失败(引擎读不到存档即 fail-closed),玩家只会收到
106
+ * 一条报错。空槽在读档模式置灰,在存档模式下可点——那是一次覆盖写入。
107
+ * `mode` 为 null(面板未打开)时一律不可点。
108
+ */
109
+ export function canChooseSlot(
110
+ view: SlotView,
111
+ mode: SlotPanelMode | null,
112
+ ): boolean {
113
+ if (mode === "save") return true;
114
+ return mode === "load" && !view.empty;
115
+ }
@@ -0,0 +1,21 @@
1
+ /** 舞台适配域:纯计算,视口尺寸由宿主提供。 */
2
+
3
+ export { computeStageLayout, stageTransform } from "./layout.ts";
4
+ export {
5
+ DEFAULT_STAGE_FIT,
6
+ DEFAULT_STAGE_HEIGHT,
7
+ DEFAULT_STAGE_WIDTH,
8
+ resolveStagePreset,
9
+ STAGE_BASELINES,
10
+ STAGE_PRESETS,
11
+ type StageBaseline,
12
+ type StagePreset,
13
+ } from "./presets.ts";
14
+ export type {
15
+ StageAlign,
16
+ StageConfig,
17
+ StageFit,
18
+ StageLayout,
19
+ StageScalePolicy,
20
+ StageViewport,
21
+ } from "./types.ts";
@@ -0,0 +1,149 @@
1
+ /**
2
+ * 舞台适配计算:把「视口尺寸 + 设计基准 + 适配策略」算成一组可直接套用的变换。
3
+ *
4
+ * 作者按设计基准像素书写元素与控件坐标,演出前由这里算出缩放与偏移,宿主把结果套到
5
+ * 舞台容器上。本模块只做纯计算:不读平台全局、不碰 DOM。视口尺寸由宿主提供——浏览器
6
+ * 宿主读窗口,小程序宿主读系统信息,桌面宿主读窗口内可用区域。
7
+ *
8
+ * 尺寸不可用(视口或基准非正、非有限)时退化为原始尺寸左上角摆放:调用方拿到的仍是
9
+ * 一组可安全套用的数字,而不是 NaN。夹取(`minScale`/`maxScale`)按轴生效,等比策略
10
+ * 下两轴同值,所以夹取不会破坏等比。
11
+ */
12
+ import { resolveStagePreset } from "./presets.ts";
13
+ import {
14
+ type StageAlign,
15
+ type StageConfig,
16
+ type StageLayout,
17
+ type StageScalePolicy,
18
+ type StageViewport,
19
+ } from "./types.ts";
20
+
21
+ /** 非正或非有限一律按 0 处理:比值算不出来时统一落到退化分支 */
22
+ function positiveOrZero(value: number): number {
23
+ return Number.isFinite(value) && value > 0 ? value : 0;
24
+ }
25
+
26
+ /** 两轴缩放倍率:`fill` 与固定数字是仅有的两支可能不等比 */
27
+ function scaleFactors(
28
+ policy: StageScalePolicy,
29
+ viewport: StageViewport,
30
+ width: number,
31
+ height: number,
32
+ ): { readonly x: number; readonly y: number } {
33
+ const byWidth = viewport.width / width;
34
+ const byHeight = viewport.height / height;
35
+ if (typeof policy === "number") {
36
+ const fixed = Number.isFinite(policy) && policy > 0 ? policy : 1;
37
+ return { x: fixed, y: fixed };
38
+ }
39
+ if (policy === "fill") return { x: byWidth, y: byHeight };
40
+ if (policy === "width") return { x: byWidth, y: byWidth };
41
+ if (policy === "height") return { x: byHeight, y: byHeight };
42
+ if (policy === "max") {
43
+ const longest = Math.max(byWidth, byHeight);
44
+ return { x: longest, y: longest };
45
+ }
46
+ const shortest = Math.min(byWidth, byHeight);
47
+ return { x: shortest, y: shortest };
48
+ }
49
+
50
+ /** 夹取到配置范围:未配置或不合法的一侧不设限 */
51
+ function clampScale(
52
+ value: number,
53
+ min: number | undefined,
54
+ max: number | undefined,
55
+ ): number {
56
+ let scale = value;
57
+ if (typeof min === "number" && Number.isFinite(min) && scale < min) {
58
+ scale = min;
59
+ }
60
+ if (typeof max === "number" && Number.isFinite(max) && scale > max) {
61
+ scale = max;
62
+ }
63
+ return scale;
64
+ }
65
+
66
+ /** 留边方向上的摆放偏移:多出的空间按锚点分给两侧,裁切时偏移为负 */
67
+ function alignOffset(
68
+ viewportEdge: number,
69
+ scaledEdge: number,
70
+ align: StageAlign,
71
+ ): number {
72
+ if (align === "start") return 0;
73
+ if (align === "end") return viewportEdge - scaledEdge;
74
+ return (viewportEdge - scaledEdge) / 2;
75
+ }
76
+
77
+ /** 非有限一律按 0:变换串会直接进 CSS,NaN 会让整条声明被静默丢弃 */
78
+ function finite(value: number): number {
79
+ return Number.isFinite(value) ? value : 0;
80
+ }
81
+
82
+ /** 小数截到六位:变换串进 CSS 与快照判据都要求稳定文本 */
83
+ function round6(value: number): number {
84
+ return Math.round(finite(value) * 1e6) / 1e6;
85
+ }
86
+
87
+ /**
88
+ * 把布局结果写成 CSS 变换串:先平移再缩放,原点取舞台容器左上角。
89
+ *
90
+ * 宿主把这一串原样写进舞台容器的 `transform`(并配 `transform-origin: top left`),
91
+ * 于是「作者坐标 → 屏幕坐标」的换算全仓只有 `computeStageLayout` 与本函数这一条路径,
92
+ * 浏览器宿主与小程序宿主不会各写一份变换顺序(顺序错了就变成先缩后移,偏移量会跟着被缩放)。
93
+ */
94
+ export function stageTransform(layout: StageLayout): string {
95
+ const x = round6(layout.offsetX);
96
+ const y = round6(layout.offsetY);
97
+ const sx = round6(layout.scaleX);
98
+ const sy = round6(layout.scaleY);
99
+ return `translate(${x}px, ${y}px) scale(${sx}, ${sy})`;
100
+ }
101
+
102
+ /** 算一次适配:策略与对齐都是「配置显式值优先,缺省取预设」 */
103
+ export function computeStageLayout(
104
+ viewport: StageViewport,
105
+ config: StageConfig,
106
+ ): StageLayout {
107
+ const viewWidth = positiveOrZero(viewport.width);
108
+ const viewHeight = positiveOrZero(viewport.height);
109
+ const width = positiveOrZero(config.width);
110
+ const height = positiveOrZero(config.height);
111
+ const preset = resolveStagePreset(config.fit);
112
+ const policy = config.scaleBy ?? preset.scaleBy;
113
+ if (viewWidth === 0 || viewHeight === 0 || width === 0 || height === 0) {
114
+ return {
115
+ policy,
116
+ scaleX: 1,
117
+ scaleY: 1,
118
+ width,
119
+ height,
120
+ scaledWidth: width,
121
+ scaledHeight: height,
122
+ offsetX: 0,
123
+ offsetY: 0,
124
+ };
125
+ }
126
+ const factors = scaleFactors(
127
+ policy,
128
+ { width: viewWidth, height: viewHeight },
129
+ width,
130
+ height,
131
+ );
132
+ const scaleX = clampScale(factors.x, config.minScale, config.maxScale);
133
+ const scaleY = clampScale(factors.y, config.minScale, config.maxScale);
134
+ const scaledWidth = width * scaleX;
135
+ const scaledHeight = height * scaleY;
136
+ const alignX = config.alignX ?? preset.alignX;
137
+ const alignY = config.alignY ?? preset.alignY;
138
+ return {
139
+ policy,
140
+ scaleX,
141
+ scaleY,
142
+ width,
143
+ height,
144
+ scaledWidth,
145
+ scaledHeight,
146
+ offsetX: alignOffset(viewWidth, scaledWidth, alignX),
147
+ offsetY: alignOffset(viewHeight, scaledHeight, alignY),
148
+ };
149
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * 舞台适配的预设表:基准尺寸与内置策略。
3
+ *
4
+ * 内置策略不是独立实现,而是「缩放策略 + 对齐锚点」的命名组合:`contain` 短边铺满 +
5
+ * 居中,`cover` 长边铺满 + 居中,`fill` 两轴各自拉满,`none` 固定 1 倍。作者需要特殊
6
+ * 行为时直接写 `scaleBy`/`alignX`/`alignY`,与预设走同一条计算路径,所以预设与自定义
7
+ * 之间不存在两套代码。
8
+ */
9
+ import { DEFAULT_STAGE_CONFIG } from "@lingfan-engine/core";
10
+ import {
11
+ type StageAlign,
12
+ type StageFit,
13
+ type StageScalePolicy,
14
+ } from "./types.ts";
15
+
16
+ /** 基准尺寸预设:编辑器与宿主的下拉项,作者另填任意尺寸同样可用 */
17
+ export interface StageBaseline {
18
+ readonly id: string;
19
+ readonly label: string;
20
+ readonly width: number;
21
+ readonly height: number;
22
+ }
23
+
24
+ /** 内置策略展开后的参数 */
25
+ export interface StagePreset {
26
+ readonly fit: StageFit;
27
+ readonly label: string;
28
+ readonly scaleBy: StageScalePolicy;
29
+ readonly alignX: StageAlign;
30
+ readonly alignY: StageAlign;
31
+ }
32
+
33
+ /**
34
+ * 缺省基准宽:取自引擎内建舞台配置(横屏 16:9,与视频、存档缩略图同比例)。
35
+ * 基准的**唯一事实源**在契约层;这里只把它摆成预设下拉要用的名字,避免两处各写一个 1280。
36
+ */
37
+ export const DEFAULT_STAGE_WIDTH = DEFAULT_STAGE_CONFIG.width;
38
+ /** 缺省基准高 */
39
+ export const DEFAULT_STAGE_HEIGHT = DEFAULT_STAGE_CONFIG.height;
40
+
41
+ /** 基准尺寸预设(横竖屏各四档:720p / 1080p / 2K / 4K,覆盖常见发布形态) */
42
+ export const STAGE_BASELINES: readonly StageBaseline[] = [
43
+ {
44
+ id: "landscape-1280x720",
45
+ label: "横屏 1280 × 720",
46
+ width: 1280,
47
+ height: 720,
48
+ },
49
+ {
50
+ id: "landscape-1920x1080",
51
+ label: "横屏 1920 × 1080",
52
+ width: 1920,
53
+ height: 1080,
54
+ },
55
+ {
56
+ id: "landscape-2560x1440",
57
+ label: "横屏 2K 2560 × 1440",
58
+ width: 2560,
59
+ height: 1440,
60
+ },
61
+ {
62
+ id: "landscape-3840x2160",
63
+ label: "横屏 4K 3840 × 2160",
64
+ width: 3840,
65
+ height: 2160,
66
+ },
67
+ {
68
+ id: "portrait-750x1334",
69
+ label: "竖屏 750 × 1334",
70
+ width: 750,
71
+ height: 1334,
72
+ },
73
+ {
74
+ id: "portrait-1080x1920",
75
+ label: "竖屏 1080 × 1920",
76
+ width: 1080,
77
+ height: 1920,
78
+ },
79
+ {
80
+ id: "portrait-1440x2560",
81
+ label: "竖屏 2K 1440 × 2560",
82
+ width: 1440,
83
+ height: 2560,
84
+ },
85
+ {
86
+ id: "portrait-2160x3840",
87
+ label: "竖屏 4K 2160 × 3840",
88
+ width: 2160,
89
+ height: 3840,
90
+ },
91
+ ];
92
+
93
+ /** 缺省策略(同样取契约层的内建值;工程未声明 `fit` 时落到这里) */
94
+ export const DEFAULT_STAGE_FIT: StageFit =
95
+ DEFAULT_STAGE_CONFIG.fit ?? "contain";
96
+
97
+ /** 内置策略表 */
98
+ export const STAGE_PRESETS: Readonly<Record<StageFit, StagePreset>> = {
99
+ contain: {
100
+ fit: "contain",
101
+ label: "完整显示(留边)",
102
+ scaleBy: "min",
103
+ alignX: "center",
104
+ alignY: "center",
105
+ },
106
+ cover: {
107
+ fit: "cover",
108
+ label: "铺满裁切",
109
+ scaleBy: "max",
110
+ alignX: "center",
111
+ alignY: "center",
112
+ },
113
+ fill: {
114
+ fit: "fill",
115
+ label: "拉伸铺满",
116
+ scaleBy: "fill",
117
+ alignX: "center",
118
+ alignY: "center",
119
+ },
120
+ none: {
121
+ fit: "none",
122
+ label: "原始尺寸",
123
+ scaleBy: 1,
124
+ alignX: "center",
125
+ alignY: "center",
126
+ },
127
+ };
128
+
129
+ /** 取内置策略:名字不在表内时回退缺省(配置来自外部,运行期不假定合法) */
130
+ export function resolveStagePreset(fit: StageFit | undefined): StagePreset {
131
+ const fallback = STAGE_PRESETS[DEFAULT_STAGE_FIT];
132
+ if (fit === undefined) return fallback;
133
+ return STAGE_PRESETS[fit] ?? fallback;
134
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * 舞台适配的公共类型:视口、设计基准与适配策略。
3
+ *
4
+ * 配置契约(`StageConfig` 与三个策略别名)**声明在引擎**(`project.json shell.stage` 是工程
5
+ * 事实,与 `shell.saves` 同属契约层,解析也在引擎),此处按原路径转出,消费方无需改 import。
6
+ * 本域只留「算出来的东西」:视口与布局结果。类型单独成文件,是让策略表与计算实现都只依赖
7
+ * 类型,实现文件之间不成环。
8
+ */
9
+
10
+ import type { StageScalePolicy } from "@lingfan-engine/core";
11
+
12
+ export type {
13
+ StageAlign,
14
+ StageConfig,
15
+ StageFit,
16
+ StageScalePolicy,
17
+ } from "@lingfan-engine/core";
18
+
19
+ /** 视口尺寸(宿主提供的逻辑像素尺寸) */
20
+ export interface StageViewport {
21
+ readonly width: number;
22
+ readonly height: number;
23
+ }
24
+
25
+ /** 计算结果:宿主把它套成舞台容器的缩放与定位 */
26
+ export interface StageLayout {
27
+ /** 实际生效的缩放策略 */
28
+ readonly policy: StageScalePolicy;
29
+ readonly scaleX: number;
30
+ readonly scaleY: number;
31
+ /** 设计基准宽高(作者坐标系) */
32
+ readonly width: number;
33
+ readonly height: number;
34
+ /** 缩放后的实际占位尺寸 */
35
+ readonly scaledWidth: number;
36
+ readonly scaledHeight: number;
37
+ /** 舞台内容左上角相对视口左上角的偏移(逻辑像素) */
38
+ readonly offsetX: number;
39
+ readonly offsetY: number;
40
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 叙事视图状态域出口:形状见 `./types`,装配见 `./narrativeState`。
3
+ */
4
+ export { createNarrativeState } from "./narrativeState";
5
+ export type {
6
+ MenuChoice,
7
+ NarrativeState,
8
+ NarrativeStateOptions,
9
+ StateCells,
10
+ } from "./types";