@sliiu/present-three-bake 0.0.0-stage → 0.1.1
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/COPYRIGHT +5 -0
- package/LICENSE +661 -0
- package/README.md +8 -2
- package/THIRD-PARTY-NOTICES.md +221 -0
- package/dist/cjs/bake-sp3d.js +176 -0
- package/dist/cjs/camera.js +94 -0
- package/dist/cjs/index.js +47 -0
- package/dist/cjs/light-rig.js +162 -0
- package/dist/cjs/material.js +136 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/path-to-shape.js +331 -0
- package/dist/cjs/presets.js +112 -0
- package/dist/cjs/resolve.js +98 -0
- package/dist/cjs/runtime.js +146 -0
- package/dist/cjs/scene-builder.js +329 -0
- package/dist/cjs/shape-path.js +71 -0
- package/dist/cjs/types.js +2 -0
- package/dist/esm/bake-sp3d.js +140 -0
- package/dist/esm/camera.js +58 -0
- package/dist/esm/index.js +9 -0
- package/dist/esm/light-rig.js +126 -0
- package/dist/esm/material.js +98 -0
- package/dist/esm/path-to-shape.js +295 -0
- package/dist/esm/presets.js +109 -0
- package/dist/esm/resolve.js +91 -0
- package/dist/esm/runtime.js +141 -0
- package/dist/esm/scene-builder.js +290 -0
- package/dist/esm/shape-path.js +64 -0
- package/dist/esm/types.js +1 -0
- package/dist/types/bake-sp3d.d.ts +2 -0
- package/dist/types/camera.d.ts +7 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/light-rig.d.ts +10 -0
- package/dist/types/material.d.ts +17 -0
- package/dist/types/path-to-shape.d.ts +11 -0
- package/dist/types/presets.d.ts +23 -0
- package/dist/types/resolve.d.ts +67 -0
- package/dist/types/runtime.d.ts +22 -0
- package/dist/types/scene-builder.d.ts +35 -0
- package/dist/types/shape-path.d.ts +15 -0
- package/dist/types/types.d.ts +1 -0
- package/package.json +40 -4
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
const RIG_PRESETS = {
|
|
3
|
+
threePt: {
|
|
4
|
+
ambient: 0.3,
|
|
5
|
+
lights: [
|
|
6
|
+
{ color: 0xffffff, intensity: 0.9, pos: [3, 4, 5] },
|
|
7
|
+
{ color: 0xffffff, intensity: 0.45, pos: [-4, 2, 3] },
|
|
8
|
+
{ color: 0xffffff, intensity: 0.25, pos: [0, -4, -3] },
|
|
9
|
+
],
|
|
10
|
+
},
|
|
11
|
+
balanced: {
|
|
12
|
+
ambient: 0.45,
|
|
13
|
+
lights: [
|
|
14
|
+
{ color: 0xffffff, intensity: 0.7, pos: [3, 3, 4] },
|
|
15
|
+
{ color: 0xffffff, intensity: 0.5, pos: [-3, 3, 4] },
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
soft: {
|
|
19
|
+
ambient: 0.55,
|
|
20
|
+
lights: [{ color: 0xffffff, intensity: 0.55, pos: [2, 4, 4] }],
|
|
21
|
+
},
|
|
22
|
+
harsh: {
|
|
23
|
+
ambient: 0.15,
|
|
24
|
+
lights: [{ color: 0xffffff, intensity: 1.4, pos: [4, 6, 4] }],
|
|
25
|
+
},
|
|
26
|
+
flood: {
|
|
27
|
+
ambient: 0.65,
|
|
28
|
+
lights: [
|
|
29
|
+
{ color: 0xffffff, intensity: 0.4, pos: [3, 0, 5] },
|
|
30
|
+
{ color: 0xffffff, intensity: 0.4, pos: [-3, 0, 5] },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
sunset: {
|
|
34
|
+
ambient: 0.3,
|
|
35
|
+
lights: [
|
|
36
|
+
{ color: 0xffd9a8, intensity: 1.0, pos: [4, 2, 3] },
|
|
37
|
+
{ color: 0x6a8fa6, intensity: 0.3, pos: [-3, 3, 4] },
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
morning: {
|
|
41
|
+
ambient: 0.4,
|
|
42
|
+
lights: [
|
|
43
|
+
{ color: 0xfff1d6, intensity: 0.85, pos: [3, 5, 4] },
|
|
44
|
+
{ color: 0xffffff, intensity: 0.3, pos: [-3, 2, 2] },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
freezing: {
|
|
48
|
+
ambient: 0.4,
|
|
49
|
+
lights: [
|
|
50
|
+
{ color: 0xd6e7ff, intensity: 0.9, pos: [3, 4, 4] },
|
|
51
|
+
{ color: 0xffffff, intensity: 0.4, pos: [-3, 2, 3] },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
flat: {
|
|
55
|
+
ambient: 0.85,
|
|
56
|
+
lights: [{ color: 0xffffff, intensity: 0.2, pos: [0, 0, 5] }],
|
|
57
|
+
},
|
|
58
|
+
twoPt: {
|
|
59
|
+
ambient: 0.3,
|
|
60
|
+
lights: [
|
|
61
|
+
{ color: 0xffffff, intensity: 0.8, pos: [4, 3, 4] },
|
|
62
|
+
{ color: 0xffffff, intensity: 0.4, pos: [-4, -1, 3] },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
glow: {
|
|
66
|
+
ambient: 0.8,
|
|
67
|
+
lights: [{ color: 0xfff8d0, intensity: 0.4, pos: [0, 0, 6] }],
|
|
68
|
+
},
|
|
69
|
+
brightRoom: {
|
|
70
|
+
ambient: 0.7,
|
|
71
|
+
lights: [{ color: 0xffffff, intensity: 0.45, pos: [3, 4, 4] }],
|
|
72
|
+
},
|
|
73
|
+
chilly: {
|
|
74
|
+
ambient: 0.35,
|
|
75
|
+
lights: [
|
|
76
|
+
{ color: 0xb8d0ff, intensity: 0.9, pos: [3, 3, 4] },
|
|
77
|
+
{ color: 0xffffff, intensity: 0.3, pos: [-3, 1, 2] },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
contrasting: {
|
|
81
|
+
ambient: 0.2,
|
|
82
|
+
lights: [
|
|
83
|
+
{ color: 0xffffff, intensity: 1.2, pos: [4, 5, 4] },
|
|
84
|
+
{ color: 0x000000, intensity: 0, pos: [0, 0, 0] },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
export function applyLightRig(scene, rigName, dir) {
|
|
89
|
+
const spec = RIG_PRESETS[rigName ?? "threePt"] ?? RIG_PRESETS.threePt;
|
|
90
|
+
const amb = new THREE.AmbientLight(0xffffff, spec.ambient);
|
|
91
|
+
scene.add(amb);
|
|
92
|
+
const yaw = dirToYaw(dir);
|
|
93
|
+
for (const l of spec.lights) {
|
|
94
|
+
const light = new THREE.DirectionalLight(l.color, l.intensity);
|
|
95
|
+
const [x, y, z] = rotateXZ(l.pos, yaw);
|
|
96
|
+
light.position.set(x, y, z);
|
|
97
|
+
scene.add(light);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function dirToYaw(dir) {
|
|
101
|
+
switch (dir) {
|
|
102
|
+
case "t":
|
|
103
|
+
return 0;
|
|
104
|
+
case "tr":
|
|
105
|
+
return Math.PI / 4;
|
|
106
|
+
case "r":
|
|
107
|
+
return Math.PI / 2;
|
|
108
|
+
case "br":
|
|
109
|
+
return (3 * Math.PI) / 4;
|
|
110
|
+
case "b":
|
|
111
|
+
return Math.PI;
|
|
112
|
+
case "bl":
|
|
113
|
+
return -3 * Math.PI / 4;
|
|
114
|
+
case "l":
|
|
115
|
+
return -Math.PI / 2;
|
|
116
|
+
case "tl":
|
|
117
|
+
return -Math.PI / 4;
|
|
118
|
+
default:
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function rotateXZ(p, yaw) {
|
|
123
|
+
const cos = Math.cos(yaw);
|
|
124
|
+
const sin = Math.sin(yaw);
|
|
125
|
+
return [p[0] * cos - p[2] * sin, p[1], p[0] * sin + p[2] * cos];
|
|
126
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
const MATERIAL_PRESETS = {
|
|
3
|
+
legacyMatte: { metalness: 0, roughness: 1 },
|
|
4
|
+
legacyPlastic: { metalness: 0, roughness: 0.4 },
|
|
5
|
+
legacyMetal: { metalness: 0.9, roughness: 0.35 },
|
|
6
|
+
legacyWireframe: { metalness: 0, roughness: 1 },
|
|
7
|
+
matte: { metalness: 0, roughness: 0.95 },
|
|
8
|
+
plastic: { metalness: 0, roughness: 0.35 },
|
|
9
|
+
metal: { metalness: 0.85, roughness: 0.3 },
|
|
10
|
+
warmMatte: { metalness: 0, roughness: 0.95 },
|
|
11
|
+
translucentPowder: { metalness: 0, roughness: 0.6 },
|
|
12
|
+
powder: { metalness: 0, roughness: 0.8 },
|
|
13
|
+
dkEdge: { metalness: 0, roughness: 0.6 },
|
|
14
|
+
softEdge: { metalness: 0, roughness: 0.5 },
|
|
15
|
+
clear: { metalness: 0, roughness: 0.05, clearcoat: 1 },
|
|
16
|
+
flat: { metalness: 0, roughness: 1 },
|
|
17
|
+
softmetal: { metalness: 0.6, roughness: 0.5 },
|
|
18
|
+
};
|
|
19
|
+
export function pickPrimaryColor(paint, fallback) {
|
|
20
|
+
if (!paint)
|
|
21
|
+
return fallback;
|
|
22
|
+
if (paint.kind === "solid")
|
|
23
|
+
return paint.color;
|
|
24
|
+
if (paint.kind === "linear" || paint.kind === "radial") {
|
|
25
|
+
const stops = paint.stops;
|
|
26
|
+
if (stops.length === 0)
|
|
27
|
+
return fallback;
|
|
28
|
+
const mid = stops[Math.floor(stops.length / 2)];
|
|
29
|
+
return mid?.color ?? stops[0]?.color ?? fallback;
|
|
30
|
+
}
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
export function buildMaterial(paint, opts = {}) {
|
|
34
|
+
const fallback = opts.fallbackColor ?? "#7f7f7f";
|
|
35
|
+
const color = pickPrimaryColor(paint, fallback);
|
|
36
|
+
// ECMA-376 §20.1.5.12 sp3d/@prstMaterial 默认是 `warmMatte` —— 缺省 prstMaterial
|
|
37
|
+
// 时之前回落到 `plastic`,体高光太亮,看起来像塑料板而不是 PPT 默认的暖哑光纸/卡片
|
|
38
|
+
// 质感(用户在 WPS 里看到 "材料:暖色粗糙" = warmMatte)。
|
|
39
|
+
const preset = MATERIAL_PRESETS[opts.prst ?? "warmMatte"] ?? MATERIAL_PRESETS.warmMatte;
|
|
40
|
+
const useTexture = !!opts.imageTexture;
|
|
41
|
+
// image 贴图:lighting 有时会把 hi-contrast 黑白图(比如棋盘)压成灰白灰黑——
|
|
42
|
+
// PBR diffuse 永远把入射光按 albedo/π 漫反射回去,多光源叠加之后白只能勉强达
|
|
43
|
+
// 1.0、黑被 ambient 提到非零灰。视觉上的"暖色卡片纸 + 倒角高光"在没有 texture
|
|
44
|
+
// 的纯色面上才有用,有 texture 时 PowerPoint / WPS 表现是"贴图原色直接透出,
|
|
45
|
+
// 仅做轻微 shading"。
|
|
46
|
+
// → 有 texture 时切到 MeshBasicMaterial:完全不参与 lighting,texture 像素直
|
|
47
|
+
// 接 round-trip 出去,再配合 Three.js outputColorSpace=SRGB 就能 1:1 还原原图
|
|
48
|
+
// 的黑白对比度。lightRig 仍会落到几何,但只通过倒角/侧面(无贴图区)产生明暗
|
|
49
|
+
// 差,用户感知就是"贴图清晰 + 边缘有 3D 立体感"。
|
|
50
|
+
if (useTexture) {
|
|
51
|
+
if (opts.litTexture) {
|
|
52
|
+
// 折边 / 侧壁:贴图照常透出,但走 PBR 受光 + tint 压暗——正面(basic、满亮)与
|
|
53
|
+
// 折边之间形成稳定可辨的明暗折痕,区分「上面 / 前面」。roughness 取 1 让过渡柔和。
|
|
54
|
+
return new THREE.MeshStandardMaterial({
|
|
55
|
+
color: opts.tint ?? 0xffffff,
|
|
56
|
+
side: THREE.DoubleSide,
|
|
57
|
+
map: opts.imageTexture ?? null,
|
|
58
|
+
metalness: 0,
|
|
59
|
+
roughness: 1,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return new THREE.MeshBasicMaterial({
|
|
63
|
+
color: opts.tint ?? 0xffffff,
|
|
64
|
+
side: THREE.DoubleSide,
|
|
65
|
+
map: opts.imageTexture ?? null,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (preset.clearcoat !== undefined) {
|
|
69
|
+
const m = new THREE.MeshPhysicalMaterial({
|
|
70
|
+
color,
|
|
71
|
+
metalness: preset.metalness,
|
|
72
|
+
roughness: preset.roughness,
|
|
73
|
+
clearcoat: preset.clearcoat,
|
|
74
|
+
side: THREE.DoubleSide,
|
|
75
|
+
});
|
|
76
|
+
return m;
|
|
77
|
+
}
|
|
78
|
+
const m = new THREE.MeshStandardMaterial({
|
|
79
|
+
color,
|
|
80
|
+
metalness: preset.metalness,
|
|
81
|
+
roughness: preset.roughness,
|
|
82
|
+
side: THREE.DoubleSide,
|
|
83
|
+
});
|
|
84
|
+
return m;
|
|
85
|
+
}
|
|
86
|
+
// `<a:blip>` 上的 srcRect 把 0..1 的图像截到 (l, t, 1-r, 1-b) 区域。three.js
|
|
87
|
+
// 直接拿原图 + 调整 texture.offset / repeat 就等价裁剪——比预先把 imageData
|
|
88
|
+
// 切片成新 PNG 省一次 putImageData。
|
|
89
|
+
export function applyTextureCrop(texture, cropRect) {
|
|
90
|
+
if (!cropRect)
|
|
91
|
+
return;
|
|
92
|
+
const { l, t, r, b } = cropRect;
|
|
93
|
+
const w = Math.max(1e-6, 1 - l - r);
|
|
94
|
+
const h = Math.max(1e-6, 1 - t - b);
|
|
95
|
+
texture.offset.set(l, b);
|
|
96
|
+
texture.repeat.set(w, h);
|
|
97
|
+
texture.needsUpdate = true;
|
|
98
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
export function pathDToShapes(d) {
|
|
3
|
+
const out = [];
|
|
4
|
+
let current = null;
|
|
5
|
+
let x = 0;
|
|
6
|
+
let y = 0;
|
|
7
|
+
let startX = 0;
|
|
8
|
+
let startY = 0;
|
|
9
|
+
let cpx = 0;
|
|
10
|
+
let cpy = 0;
|
|
11
|
+
let lastCmd = "";
|
|
12
|
+
const minMax = { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity };
|
|
13
|
+
const track = (px, py) => {
|
|
14
|
+
if (px < minMax.minX)
|
|
15
|
+
minMax.minX = px;
|
|
16
|
+
if (py < minMax.minY)
|
|
17
|
+
minMax.minY = py;
|
|
18
|
+
if (px > minMax.maxX)
|
|
19
|
+
minMax.maxX = px;
|
|
20
|
+
if (py > minMax.maxY)
|
|
21
|
+
minMax.maxY = py;
|
|
22
|
+
};
|
|
23
|
+
const tokens = tokenizePath(d);
|
|
24
|
+
let i = 0;
|
|
25
|
+
while (i < tokens.length) {
|
|
26
|
+
const t = tokens[i++];
|
|
27
|
+
if (typeof t !== "string")
|
|
28
|
+
continue;
|
|
29
|
+
const cmd = t;
|
|
30
|
+
const lower = cmd.toLowerCase();
|
|
31
|
+
const isRel = cmd !== cmd.toUpperCase();
|
|
32
|
+
const read = () => {
|
|
33
|
+
const v = tokens[i++];
|
|
34
|
+
return typeof v === "number" ? v : 0;
|
|
35
|
+
};
|
|
36
|
+
if (lower === "m") {
|
|
37
|
+
const nx = read() + (isRel ? x : 0);
|
|
38
|
+
const ny = read() + (isRel ? y : 0);
|
|
39
|
+
if (current)
|
|
40
|
+
out.push(current);
|
|
41
|
+
current = new THREE.Shape();
|
|
42
|
+
current.moveTo(nx, ny);
|
|
43
|
+
x = nx;
|
|
44
|
+
y = ny;
|
|
45
|
+
startX = nx;
|
|
46
|
+
startY = ny;
|
|
47
|
+
track(x, y);
|
|
48
|
+
while (i < tokens.length && typeof tokens[i] === "number") {
|
|
49
|
+
const lx = read() + (isRel ? x : 0);
|
|
50
|
+
const ly = read() + (isRel ? y : 0);
|
|
51
|
+
current.lineTo(lx, ly);
|
|
52
|
+
x = lx;
|
|
53
|
+
y = ly;
|
|
54
|
+
track(x, y);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else if (lower === "l") {
|
|
58
|
+
while (i < tokens.length && typeof tokens[i] === "number") {
|
|
59
|
+
const lx = read() + (isRel ? x : 0);
|
|
60
|
+
const ly = read() + (isRel ? y : 0);
|
|
61
|
+
current?.lineTo(lx, ly);
|
|
62
|
+
x = lx;
|
|
63
|
+
y = ly;
|
|
64
|
+
track(x, y);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else if (lower === "h") {
|
|
68
|
+
while (i < tokens.length && typeof tokens[i] === "number") {
|
|
69
|
+
const lx = read() + (isRel ? x : 0);
|
|
70
|
+
current?.lineTo(lx, y);
|
|
71
|
+
x = lx;
|
|
72
|
+
track(x, y);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else if (lower === "v") {
|
|
76
|
+
while (i < tokens.length && typeof tokens[i] === "number") {
|
|
77
|
+
const ly = read() + (isRel ? y : 0);
|
|
78
|
+
current?.lineTo(x, ly);
|
|
79
|
+
y = ly;
|
|
80
|
+
track(x, y);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else if (lower === "c") {
|
|
84
|
+
while (i < tokens.length && typeof tokens[i] === "number") {
|
|
85
|
+
const c1x = read() + (isRel ? x : 0);
|
|
86
|
+
const c1y = read() + (isRel ? y : 0);
|
|
87
|
+
const c2x = read() + (isRel ? x : 0);
|
|
88
|
+
const c2y = read() + (isRel ? y : 0);
|
|
89
|
+
const ex = read() + (isRel ? x : 0);
|
|
90
|
+
const ey = read() + (isRel ? y : 0);
|
|
91
|
+
current?.bezierCurveTo(c1x, c1y, c2x, c2y, ex, ey);
|
|
92
|
+
cpx = c2x;
|
|
93
|
+
cpy = c2y;
|
|
94
|
+
x = ex;
|
|
95
|
+
y = ey;
|
|
96
|
+
track(x, y);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (lower === "s") {
|
|
100
|
+
while (i < tokens.length && typeof tokens[i] === "number") {
|
|
101
|
+
const c1x = lastCmd === "c" || lastCmd === "s" ? 2 * x - cpx : x;
|
|
102
|
+
const c1y = lastCmd === "c" || lastCmd === "s" ? 2 * y - cpy : y;
|
|
103
|
+
const c2x = read() + (isRel ? x : 0);
|
|
104
|
+
const c2y = read() + (isRel ? y : 0);
|
|
105
|
+
const ex = read() + (isRel ? x : 0);
|
|
106
|
+
const ey = read() + (isRel ? y : 0);
|
|
107
|
+
current?.bezierCurveTo(c1x, c1y, c2x, c2y, ex, ey);
|
|
108
|
+
cpx = c2x;
|
|
109
|
+
cpy = c2y;
|
|
110
|
+
x = ex;
|
|
111
|
+
y = ey;
|
|
112
|
+
track(x, y);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else if (lower === "q") {
|
|
116
|
+
while (i < tokens.length && typeof tokens[i] === "number") {
|
|
117
|
+
const c1x = read() + (isRel ? x : 0);
|
|
118
|
+
const c1y = read() + (isRel ? y : 0);
|
|
119
|
+
const ex = read() + (isRel ? x : 0);
|
|
120
|
+
const ey = read() + (isRel ? y : 0);
|
|
121
|
+
current?.quadraticCurveTo(c1x, c1y, ex, ey);
|
|
122
|
+
cpx = c1x;
|
|
123
|
+
cpy = c1y;
|
|
124
|
+
x = ex;
|
|
125
|
+
y = ey;
|
|
126
|
+
track(x, y);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else if (lower === "t") {
|
|
130
|
+
while (i < tokens.length && typeof tokens[i] === "number") {
|
|
131
|
+
const c1x = lastCmd === "q" || lastCmd === "t" ? 2 * x - cpx : x;
|
|
132
|
+
const c1y = lastCmd === "q" || lastCmd === "t" ? 2 * y - cpy : y;
|
|
133
|
+
const ex = read() + (isRel ? x : 0);
|
|
134
|
+
const ey = read() + (isRel ? y : 0);
|
|
135
|
+
current?.quadraticCurveTo(c1x, c1y, ex, ey);
|
|
136
|
+
cpx = c1x;
|
|
137
|
+
cpy = c1y;
|
|
138
|
+
x = ex;
|
|
139
|
+
y = ey;
|
|
140
|
+
track(x, y);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else if (lower === "a") {
|
|
144
|
+
while (i < tokens.length && typeof tokens[i] === "number") {
|
|
145
|
+
const rx = Math.abs(read());
|
|
146
|
+
const ry = Math.abs(read());
|
|
147
|
+
const phi = read();
|
|
148
|
+
const large = read();
|
|
149
|
+
const sweep = read();
|
|
150
|
+
const ex = read() + (isRel ? x : 0);
|
|
151
|
+
const ey = read() + (isRel ? y : 0);
|
|
152
|
+
const curves = svgArcToCubics(x, y, rx, ry, phi, large, sweep, ex, ey);
|
|
153
|
+
if (!current || curves.length === 0) {
|
|
154
|
+
current?.lineTo(ex, ey);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
for (const [c1x, c1y, c2x, c2y, px, py] of curves) {
|
|
158
|
+
current.bezierCurveTo(c1x, c1y, c2x, c2y, px, py);
|
|
159
|
+
track(c1x, c1y);
|
|
160
|
+
track(c2x, c2y);
|
|
161
|
+
track(px, py);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
x = ex;
|
|
165
|
+
y = ey;
|
|
166
|
+
track(x, y);
|
|
167
|
+
cpx = x;
|
|
168
|
+
cpy = y;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else if (lower === "z") {
|
|
172
|
+
current?.lineTo(startX, startY);
|
|
173
|
+
x = startX;
|
|
174
|
+
y = startY;
|
|
175
|
+
}
|
|
176
|
+
lastCmd = lower;
|
|
177
|
+
}
|
|
178
|
+
if (current)
|
|
179
|
+
out.push(current);
|
|
180
|
+
if (!Number.isFinite(minMax.minX)) {
|
|
181
|
+
minMax.minX = 0;
|
|
182
|
+
minMax.minY = 0;
|
|
183
|
+
minMax.maxX = 0;
|
|
184
|
+
minMax.maxY = 0;
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
shapes: out,
|
|
188
|
+
bbox: minMax,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function tokenizePath(d) {
|
|
192
|
+
const out = [];
|
|
193
|
+
let i = 0;
|
|
194
|
+
while (i < d.length) {
|
|
195
|
+
const c = d[i];
|
|
196
|
+
if (/[a-zA-Z]/.test(c)) {
|
|
197
|
+
out.push(c);
|
|
198
|
+
i++;
|
|
199
|
+
}
|
|
200
|
+
else if (/[\s,]/.test(c)) {
|
|
201
|
+
i++;
|
|
202
|
+
}
|
|
203
|
+
else if (c === "-" || c === "+" || c === "." || /\d/.test(c)) {
|
|
204
|
+
let j = i;
|
|
205
|
+
if (d[j] === "-" || d[j] === "+")
|
|
206
|
+
j++;
|
|
207
|
+
while (j < d.length && /[0-9.]/.test(d[j]))
|
|
208
|
+
j++;
|
|
209
|
+
if (d[j] === "e" || d[j] === "E") {
|
|
210
|
+
j++;
|
|
211
|
+
if (d[j] === "-" || d[j] === "+")
|
|
212
|
+
j++;
|
|
213
|
+
while (j < d.length && /\d/.test(d[j]))
|
|
214
|
+
j++;
|
|
215
|
+
}
|
|
216
|
+
const num = Number(d.slice(i, j));
|
|
217
|
+
if (Number.isFinite(num))
|
|
218
|
+
out.push(num);
|
|
219
|
+
i = j > i ? j : i + 1;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
i++;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
// SVG arc → 三次贝塞尔。端点参数按 SVG 1.1 附录 F.6 转成圆心,再按不超过 90° 分段。
|
|
228
|
+
function svgArcToCubics(x1, y1, rx, ry, phiDeg, large, sweep, x2, y2) {
|
|
229
|
+
if (rx === 0 || ry === 0)
|
|
230
|
+
return [];
|
|
231
|
+
if (x1 === x2 && y1 === y2)
|
|
232
|
+
return [];
|
|
233
|
+
const phi = (phiDeg * Math.PI) / 180;
|
|
234
|
+
const sinphi = Math.sin(phi);
|
|
235
|
+
const cosphi = Math.cos(phi);
|
|
236
|
+
const dx = (x1 - x2) / 2;
|
|
237
|
+
const dy = (y1 - y2) / 2;
|
|
238
|
+
const px = cosphi * dx + sinphi * dy;
|
|
239
|
+
const py = -sinphi * dx + cosphi * dy;
|
|
240
|
+
rx = Math.abs(rx);
|
|
241
|
+
ry = Math.abs(ry);
|
|
242
|
+
const px2 = px * px;
|
|
243
|
+
const py2 = py * py;
|
|
244
|
+
let rx2 = rx * rx;
|
|
245
|
+
let ry2 = ry * ry;
|
|
246
|
+
const lam = px2 / rx2 + py2 / ry2;
|
|
247
|
+
if (lam > 1) {
|
|
248
|
+
const s = Math.sqrt(lam);
|
|
249
|
+
rx *= s;
|
|
250
|
+
ry *= s;
|
|
251
|
+
rx2 = rx * rx;
|
|
252
|
+
ry2 = ry * ry;
|
|
253
|
+
}
|
|
254
|
+
const sign = large === sweep ? -1 : 1;
|
|
255
|
+
let sq = (rx2 * ry2 - rx2 * py2 - ry2 * px2) / (rx2 * py2 + ry2 * px2);
|
|
256
|
+
if (!Number.isFinite(sq) || sq < 0)
|
|
257
|
+
sq = 0;
|
|
258
|
+
const coef = sign * Math.sqrt(sq);
|
|
259
|
+
const cxp = (coef * rx * py) / ry;
|
|
260
|
+
const cyp = (coef * -ry * px) / rx;
|
|
261
|
+
const cx = cosphi * cxp - sinphi * cyp + (x1 + x2) / 2;
|
|
262
|
+
const cy = sinphi * cxp + cosphi * cyp + (y1 + y2) / 2;
|
|
263
|
+
const theta1 = vectorAngle(1, 0, (px - cxp) / rx, (py - cyp) / ry);
|
|
264
|
+
let dtheta = vectorAngle((px - cxp) / rx, (py - cyp) / ry, (-px - cxp) / rx, (-py - cyp) / ry);
|
|
265
|
+
if (!sweep && dtheta > 0)
|
|
266
|
+
dtheta -= Math.PI * 2;
|
|
267
|
+
if (sweep && dtheta < 0)
|
|
268
|
+
dtheta += Math.PI * 2;
|
|
269
|
+
const segs = Math.max(1, Math.ceil(Math.abs(dtheta) / (Math.PI / 2)));
|
|
270
|
+
const delta = dtheta / segs;
|
|
271
|
+
const t = (4 / 3) * Math.tan(delta / 4);
|
|
272
|
+
const out = [];
|
|
273
|
+
const map = (lx, ly) => [
|
|
274
|
+
cosphi * lx - sinphi * ly + cx,
|
|
275
|
+
sinphi * lx + cosphi * ly + cy,
|
|
276
|
+
];
|
|
277
|
+
for (let i = 0; i < segs; i++) {
|
|
278
|
+
const th = theta1 + i * delta;
|
|
279
|
+
const th2 = th + delta;
|
|
280
|
+
const cosTh = Math.cos(th);
|
|
281
|
+
const sinTh = Math.sin(th);
|
|
282
|
+
const cosTh2 = Math.cos(th2);
|
|
283
|
+
const sinTh2 = Math.sin(th2);
|
|
284
|
+
const p1 = map(rx * (cosTh - t * sinTh), ry * (sinTh + t * cosTh));
|
|
285
|
+
const p2 = map(rx * (cosTh2 + t * sinTh2), ry * (sinTh2 - t * cosTh2));
|
|
286
|
+
const end = map(rx * cosTh2, ry * sinTh2);
|
|
287
|
+
out.push([p1[0], p1[1], p2[0], p2[1], end[0], end[1]]);
|
|
288
|
+
}
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
function vectorAngle(ux, uy, vx, vy) {
|
|
292
|
+
const dot = ux * vx + uy * vy;
|
|
293
|
+
const det = ux * vy - uy * vx;
|
|
294
|
+
return Math.atan2(det, dot);
|
|
295
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// PowerPoint 效果层 —— preset 数值表与默认值(唯一来源)
|
|
3
|
+
//
|
|
4
|
+
// ECMA-376 只定义 preset 的**名称**(camera/@prst、sp3d/@prstMaterial、lightRig/@rig),
|
|
5
|
+
// **不**定义数值。下面的相机角度 / fov / 材质参数都是**逆向 PowerPoint / WPS 行为**
|
|
6
|
+
// 得到的 best-effort 值,并以 samples/7.pptx slide2 棋盘(perspectiveRelaxedModerately
|
|
7
|
+
// + fov 55° + 默认 bevel + threePt/dir=t)做视觉校验基准。
|
|
8
|
+
//
|
|
9
|
+
// 这里是整套 3D 的「PowerPoint 知识」集中地:parser 只忠实解析名称,渲染器只消费
|
|
10
|
+
// resolve.ts 解析出来的具体参数。改 preset 视觉只动这一份文件。
|
|
11
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
12
|
+
// 相机方位/符号约定与几何/UV 的协调见 resolve.ts + camera.ts 的注释。
|
|
13
|
+
export const CAMERA_PRESETS = {
|
|
14
|
+
// ── legacy oblique(窄 fov 斜投影)─────────────────────────────────────────
|
|
15
|
+
legacyObliqueTopLeft: { lat: -20, lon: -30, rev: 0, fov: 25 },
|
|
16
|
+
legacyObliqueTop: { lat: -20, lon: 0, rev: 0, fov: 25 },
|
|
17
|
+
legacyObliqueTopRight: { lat: -20, lon: 30, rev: 0, fov: 25 },
|
|
18
|
+
legacyObliqueLeft: { lat: 0, lon: -30, rev: 0, fov: 25 },
|
|
19
|
+
legacyObliqueFront: { lat: 0, lon: 0, rev: 0, fov: 25 },
|
|
20
|
+
legacyObliqueRight: { lat: 0, lon: 30, rev: 0, fov: 25 },
|
|
21
|
+
legacyObliqueBottomLeft: { lat: 20, lon: -30, rev: 0, fov: 25 },
|
|
22
|
+
legacyObliqueBottom: { lat: 20, lon: 0, rev: 0, fov: 25 },
|
|
23
|
+
legacyObliqueBottomRight: { lat: 20, lon: 30, rev: 0, fov: 25 },
|
|
24
|
+
// ── legacy perspective(中等 fov 透视)─────────────────────────────────────
|
|
25
|
+
legacyPerspectiveTopLeft: { lat: -20, lon: -30, rev: 0, fov: 40 },
|
|
26
|
+
legacyPerspectiveTop: { lat: -20, lon: 0, rev: 0, fov: 40 },
|
|
27
|
+
legacyPerspectiveTopRight: { lat: -20, lon: 30, rev: 0, fov: 40 },
|
|
28
|
+
legacyPerspectiveLeft: { lat: 0, lon: -30, rev: 0, fov: 40 },
|
|
29
|
+
legacyPerspectiveFront: { lat: 0, lon: 0, rev: 0, fov: 40 },
|
|
30
|
+
legacyPerspectiveRight: { lat: 0, lon: 30, rev: 0, fov: 40 },
|
|
31
|
+
legacyPerspectiveBottomLeft: { lat: 20, lon: -30, rev: 0, fov: 40 },
|
|
32
|
+
legacyPerspectiveBottom: { lat: 20, lon: 0, rev: 0, fov: 40 },
|
|
33
|
+
legacyPerspectiveBottomRight: { lat: 20, lon: 30, rev: 0, fov: 40 },
|
|
34
|
+
// ── orthographic / isometric(正交)────────────────────────────────────────
|
|
35
|
+
orthographicFront: { lat: 0, lon: 0, rev: 0, fov: 0.01 },
|
|
36
|
+
isometricTopUp: { lat: -35, lon: 45, rev: 0, fov: 0.01 },
|
|
37
|
+
isometricTopDown: { lat: 35, lon: 45, rev: 0, fov: 0.01 },
|
|
38
|
+
isometricBottomUp: { lat: -35, lon: -135, rev: 0, fov: 0.01 },
|
|
39
|
+
isometricBottomDown: { lat: 35, lon: -135, rev: 0, fov: 0.01 },
|
|
40
|
+
isometricLeftUp: { lat: -35, lon: -45, rev: 0, fov: 0.01 },
|
|
41
|
+
isometricLeftDown: { lat: 35, lon: -45, rev: 0, fov: 0.01 },
|
|
42
|
+
isometricRightUp: { lat: -35, lon: 135, rev: 0, fov: 0.01 },
|
|
43
|
+
isometricRightDown: { lat: 35, lon: 135, rev: 0, fov: 0.01 },
|
|
44
|
+
isometricOffAxis1Left: { lat: -25, lon: -50, rev: 0, fov: 0.01 },
|
|
45
|
+
isometricOffAxis1Right: { lat: -25, lon: 50, rev: 0, fov: 0.01 },
|
|
46
|
+
isometricOffAxis1Top: { lat: -45, lon: 0, rev: 0, fov: 0.01 },
|
|
47
|
+
isometricOffAxis2Left: { lat: 25, lon: -50, rev: 0, fov: 0.01 },
|
|
48
|
+
isometricOffAxis2Right: { lat: 25, lon: 50, rev: 0, fov: 0.01 },
|
|
49
|
+
isometricOffAxis2Top: { lat: 45, lon: 0, rev: 0, fov: 0.01 },
|
|
50
|
+
isometricOffAxis3Bottom: { lat: 0, lon: 45, rev: 0, fov: 0.01 },
|
|
51
|
+
isometricOffAxis3Left: { lat: 0, lon: -45, rev: 0, fov: 0.01 },
|
|
52
|
+
isometricOffAxis3Right: { lat: 0, lon: 45, rev: 0, fov: 0.01 },
|
|
53
|
+
isometricOffAxis3Top: { lat: -45, lon: 0, rev: 0, fov: 0.01 },
|
|
54
|
+
isometricOffAxis4Bottom: { lat: 30, lon: -45, rev: 0, fov: 0.01 },
|
|
55
|
+
isometricOffAxis4Left: { lat: 0, lon: -45, rev: 0, fov: 0.01 },
|
|
56
|
+
isometricOffAxis4Right: { lat: 0, lon: 45, rev: 0, fov: 0.01 },
|
|
57
|
+
isometricOffAxis4Top: { lat: -30, lon: 45, rev: 0, fov: 0.01 },
|
|
58
|
+
// ── oblique(正交斜投影)────────────────────────────────────────────────────
|
|
59
|
+
obliqueTopLeft: { lat: -20, lon: -30, rev: 0, fov: 0.01 },
|
|
60
|
+
obliqueTop: { lat: -20, lon: 0, rev: 0, fov: 0.01 },
|
|
61
|
+
obliqueTopRight: { lat: -20, lon: 30, rev: 0, fov: 0.01 },
|
|
62
|
+
obliqueLeft: { lat: 0, lon: -30, rev: 0, fov: 0.01 },
|
|
63
|
+
obliqueRight: { lat: 0, lon: 30, rev: 0, fov: 0.01 },
|
|
64
|
+
obliqueBottomLeft: { lat: 20, lon: -30, rev: 0, fov: 0.01 },
|
|
65
|
+
obliqueBottom: { lat: 20, lon: 0, rev: 0, fov: 0.01 },
|
|
66
|
+
obliqueBottomRight: { lat: 20, lon: 30, rev: 0, fov: 0.01 },
|
|
67
|
+
// ── perspective(透视)──────────────────────────────────────────────────────
|
|
68
|
+
perspectiveFront: { lat: 0, lon: 0, rev: 0, fov: 35 },
|
|
69
|
+
perspectiveLeft: { lat: 0, lon: -25, rev: 0, fov: 35 },
|
|
70
|
+
perspectiveRight: { lat: 0, lon: 25, rev: 0, fov: 35 },
|
|
71
|
+
perspectiveAbove: { lat: -20, lon: 0, rev: 0, fov: 35 },
|
|
72
|
+
perspectiveBelow: { lat: 20, lon: 0, rev: 0, fov: 35 },
|
|
73
|
+
perspectiveAboveLeftFacing: { lat: -20, lon: -25, rev: 0, fov: 35 },
|
|
74
|
+
perspectiveAboveRightFacing: { lat: -20, lon: 25, rev: 0, fov: 35 },
|
|
75
|
+
perspectiveContrastingLeftFacing: { lat: -10, lon: -50, rev: 0, fov: 60 },
|
|
76
|
+
perspectiveContrastingRightFacing: { lat: -10, lon: 50, rev: 0, fov: 60 },
|
|
77
|
+
perspectiveHeroicLeftFacing: { lat: -45, lon: -45, rev: 0, fov: 60 },
|
|
78
|
+
perspectiveHeroicRightFacing: { lat: -45, lon: 45, rev: 0, fov: 60 },
|
|
79
|
+
perspectiveHeroicExtremeLeftFacing: { lat: -55, lon: -55, rev: 0, fov: 80 },
|
|
80
|
+
perspectiveHeroicExtremeRightFacing: { lat: -55, lon: 55, rev: 0, fov: 80 },
|
|
81
|
+
// perspectiveRelaxed / perspectiveRelaxedModerately:PowerPoint 3D 旋转库里的
|
|
82
|
+
// "透视:宽松 / 适度宽松" —— 真·透视相机(不是正交!),相机在物体上方俯视,使顶边
|
|
83
|
+
// 后退、底边前凸,产生"桌面式"透视倾斜。两个关键:
|
|
84
|
+
// 1. 必须用 PerspectiveCamera:正交投影下倾斜只按 cos(angle) 缩放(15° 仅 3%,看不出),
|
|
85
|
+
// 透视收缩才让倾斜可见。
|
|
86
|
+
// 2. "relaxed" = 长焦镜头(窄 fov),透视轻柔、畸变小;配中等俯角形成柔和的梯形。
|
|
87
|
+
// PowerPoint 写进 OOXML 的 `@fov`(棋盘 fov=55°)不是字面相机视角,PPT 内部当
|
|
88
|
+
// zoom/参考用 —— 真正落地是窄 fov + 适度俯视,所以这两个 preset 锁定窄 fov。
|
|
89
|
+
// - lat:俯视下倾角,负值=相机在上方("Above" 语义,见 camera.ts eye.y 约定)。
|
|
90
|
+
perspectiveRelaxed: { lat: -13, lon: 0, rev: 0, fov: 22 },
|
|
91
|
+
perspectiveRelaxedModerately: { lat: -18, lon: 0, rev: 0, fov: 28 },
|
|
92
|
+
};
|
|
93
|
+
// "perspectiveRelaxed*" 锁定预设窄 fov,忽略 OOXML 里偏宽的显式 `@fov`(见上注释)。
|
|
94
|
+
export const FOV_LOCKED_PRESETS = new Set([
|
|
95
|
+
"perspectiveRelaxed",
|
|
96
|
+
"perspectiveRelaxedModerately",
|
|
97
|
+
]);
|
|
98
|
+
// ── 默认值(OOXML 缺省时的 PowerPoint 默认)───────────────────────────────────
|
|
99
|
+
/**
|
|
100
|
+
* ECMA-376 §20.1.5.4:`<a:bevelT/>` / `<a:bevelB/>` 缺省 w/h = 76200 EMU。
|
|
101
|
+
* 76200 ÷ 9525 EMU/px = 8 px。parser 把缺省留 undefined,这里补默认。
|
|
102
|
+
*/
|
|
103
|
+
export const DEFAULT_BEVEL_PX = 8;
|
|
104
|
+
/** ECMA-376 §20.1.5.12 sp3d/@prstMaterial 默认 = warmMatte。 */
|
|
105
|
+
export const DEFAULT_MATERIAL = "warmMatte";
|
|
106
|
+
/** ECMA-376 §20.1.5.9 lightRig 缺省(无 scene3d 时)我们用 threePt 兜底。 */
|
|
107
|
+
export const DEFAULT_LIGHT_RIG = "threePt";
|
|
108
|
+
/** PerspectiveCamera 缺省 fov(无 preset 也无显式 fov 时)。 */
|
|
109
|
+
export const DEFAULT_FOV = 35;
|