@nayan-ui/engine 0.0.0-stage → 1.0.0-beta.3
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/LICENSE +21 -0
- package/NayanEngine.podspec +28 -0
- package/README.md +210 -2
- package/lib/module/assets/AssetBytesCache.js +76 -0
- package/lib/module/assets/AssetCache.js +19 -0
- package/lib/module/assets/AssetLoader.js +52 -0
- package/lib/module/assets/CacheGeneration.js +7 -0
- package/lib/module/assets/IndexedDbCache.web.js +149 -0
- package/lib/module/assets/ProceduralCache.js +222 -0
- package/lib/module/assets/font.js +136 -0
- package/lib/module/assets/loft.js +142 -0
- package/lib/module/assets/model.js +74 -0
- package/lib/module/assets/modelling.js +254 -0
- package/lib/module/assets/shape.js +926 -0
- package/lib/module/assets/source.js +58 -0
- package/lib/module/assets/source.web.js +22 -0
- package/lib/module/assets/storage.js +4 -0
- package/lib/module/assets/storage.shared.js +36 -0
- package/lib/module/assets/storage.web.js +37 -0
- package/lib/module/assets/surface.js +47 -0
- package/lib/module/assets/texture.js +66 -0
- package/lib/module/drawing/DrawingCanvas.js +38 -0
- package/lib/module/drawing/DrawingCanvas.web.js +206 -0
- package/lib/module/drawing/DrawingDocument.js +256 -0
- package/lib/module/drawing/types.js +4 -0
- package/lib/module/globals.d.ts +12 -0
- package/lib/module/index.js +36 -0
- package/lib/module/input/Joystick.js +84 -0
- package/lib/module/input/Joystick.web.js +93 -0
- package/lib/module/input/keyboard.js +250 -0
- package/lib/module/media/audio-worklet.js +34 -0
- package/lib/module/media/audio.js +94 -0
- package/lib/module/media/haptics.js +82 -0
- package/lib/module/media/output.web.js +66 -0
- package/lib/module/native/BrowserEngine.js +551 -0
- package/lib/module/native/EngineBridge.js +4 -0
- package/lib/module/native/NativeNayanEngine.js +7 -0
- package/lib/module/native/NativeNayanEngine.web.js +62 -0
- package/lib/module/native/prepare.js +7 -0
- package/lib/module/native/prepare.web.js +17 -0
- package/lib/module/native/spec/NativeNayanEngine.js +17 -0
- package/lib/module/package.json +1 -0
- package/lib/module/render/GameView.js +249 -0
- package/lib/module/render/camera.js +34 -0
- package/lib/module/render/host.js +39 -0
- package/lib/module/render/host.web.js +104 -0
- package/lib/module/render/meshes.js +159 -0
- package/lib/module/render/quality.js +112 -0
- package/lib/module/render/responder.js +4 -0
- package/lib/module/render/responder.web.js +97 -0
- package/lib/module/render/touch.js +185 -0
- package/lib/module/types.js +30 -0
- package/lib/module/world/Scoreboard.js +143 -0
- package/lib/module/world/TransformBatch.js +126 -0
- package/lib/module/world/World.js +690 -0
- package/lib/module/world/desc.js +186 -0
- package/lib/module/world/effects.js +451 -0
- package/lib/module/world/quat.js +86 -0
- package/lib/module/world/types.js +4 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/assets/AssetBytesCache.d.ts +7 -0
- package/lib/typescript/src/assets/AssetCache.d.ts +9 -0
- package/lib/typescript/src/assets/AssetLoader.d.ts +11 -0
- package/lib/typescript/src/assets/CacheGeneration.d.ts +3 -0
- package/lib/typescript/src/assets/IndexedDbCache.web.d.ts +20 -0
- package/lib/typescript/src/assets/ProceduralCache.d.ts +33 -0
- package/lib/typescript/src/assets/font.d.ts +53 -0
- package/lib/typescript/src/assets/loft.d.ts +31 -0
- package/lib/typescript/src/assets/model.d.ts +48 -0
- package/lib/typescript/src/assets/modelling.d.ts +157 -0
- package/lib/typescript/src/assets/shape.d.ts +203 -0
- package/lib/typescript/src/assets/source.d.ts +10 -0
- package/lib/typescript/src/assets/source.web.d.ts +8 -0
- package/lib/typescript/src/assets/storage.d.ts +2 -0
- package/lib/typescript/src/assets/storage.shared.d.ts +7 -0
- package/lib/typescript/src/assets/storage.web.d.ts +7 -0
- package/lib/typescript/src/assets/surface.d.ts +17 -0
- package/lib/typescript/src/assets/texture.d.ts +30 -0
- package/lib/typescript/src/drawing/DrawingCanvas.d.ts +5 -0
- package/lib/typescript/src/drawing/DrawingCanvas.web.d.ts +4 -0
- package/lib/typescript/src/drawing/DrawingDocument.d.ts +210 -0
- package/lib/typescript/src/drawing/types.d.ts +12 -0
- package/lib/typescript/src/index.d.ts +42 -0
- package/lib/typescript/src/input/Joystick.d.ts +20 -0
- package/lib/typescript/src/input/Joystick.web.d.ts +13 -0
- package/lib/typescript/src/input/keyboard.d.ts +17 -0
- package/lib/typescript/src/media/audio.d.ts +46 -0
- package/lib/typescript/src/media/haptics.d.ts +22 -0
- package/lib/typescript/src/media/output.web.d.ts +4 -0
- package/lib/typescript/src/native/BrowserEngine.d.ts +8 -0
- package/lib/typescript/src/native/EngineBridge.d.ts +22 -0
- package/lib/typescript/src/native/NativeNayanEngine.d.ts +5 -0
- package/lib/typescript/src/native/NativeNayanEngine.web.d.ts +10 -0
- package/lib/typescript/src/native/prepare.d.ts +3 -0
- package/lib/typescript/src/native/prepare.web.d.ts +2 -0
- package/lib/typescript/src/native/spec/NativeNayanEngine.d.ts +182 -0
- package/lib/typescript/src/render/GameView.d.ts +95 -0
- package/lib/typescript/src/render/camera.d.ts +6 -0
- package/lib/typescript/src/render/host.d.ts +29 -0
- package/lib/typescript/src/render/host.web.d.ts +16 -0
- package/lib/typescript/src/render/meshes.d.ts +45 -0
- package/lib/typescript/src/render/quality.d.ts +46 -0
- package/lib/typescript/src/render/responder.d.ts +2 -0
- package/lib/typescript/src/render/responder.web.d.ts +40 -0
- package/lib/typescript/src/render/touch.d.ts +26 -0
- package/lib/typescript/src/types.d.ts +72 -0
- package/lib/typescript/src/world/Scoreboard.d.ts +65 -0
- package/lib/typescript/src/world/TransformBatch.d.ts +39 -0
- package/lib/typescript/src/world/World.d.ts +199 -0
- package/lib/typescript/src/world/desc.d.ts +17 -0
- package/lib/typescript/src/world/effects.d.ts +156 -0
- package/lib/typescript/src/world/quat.d.ts +31 -0
- package/lib/typescript/src/world/types.d.ts +233 -0
- package/lib/web/audio-worklet.js +29 -0
- package/lib/web/engine_core_bg.wasm +0 -0
- package/lib/web/index.js +7974 -0
- package/lib/web/types/src/assets/AssetBytesCache.d.ts +6 -0
- package/lib/web/types/src/assets/AssetCache.d.ts +8 -0
- package/lib/web/types/src/assets/AssetLoader.d.ts +10 -0
- package/lib/web/types/src/assets/CacheGeneration.d.ts +2 -0
- package/lib/web/types/src/assets/IndexedDbCache.d.ts +19 -0
- package/lib/web/types/src/assets/ProceduralCache.d.ts +32 -0
- package/lib/web/types/src/assets/font.d.ts +52 -0
- package/lib/web/types/src/assets/loft.d.ts +30 -0
- package/lib/web/types/src/assets/model.d.ts +47 -0
- package/lib/web/types/src/assets/modelling.d.ts +156 -0
- package/lib/web/types/src/assets/shape.d.ts +202 -0
- package/lib/web/types/src/assets/source.d.ts +7 -0
- package/lib/web/types/src/assets/storage.d.ts +6 -0
- package/lib/web/types/src/assets/storage.shared.d.ts +6 -0
- package/lib/web/types/src/assets/surface.d.ts +16 -0
- package/lib/web/types/src/assets/texture.d.ts +29 -0
- package/lib/web/types/src/drawing/DrawingCanvas.d.ts +3 -0
- package/lib/web/types/src/drawing/DrawingDocument.d.ts +209 -0
- package/lib/web/types/src/drawing/types.d.ts +11 -0
- package/lib/web/types/src/index.d.ts +41 -0
- package/lib/web/types/src/input/Joystick.d.ts +12 -0
- package/lib/web/types/src/input/keyboard.d.ts +16 -0
- package/lib/web/types/src/media/audio.d.ts +45 -0
- package/lib/web/types/src/media/haptics.d.ts +21 -0
- package/lib/web/types/src/media/output.d.ts +3 -0
- package/lib/web/types/src/native/BrowserEngine.d.ts +7 -0
- package/lib/web/types/src/native/EngineBridge.d.ts +21 -0
- package/lib/web/types/src/native/NativeNayanEngine.d.ts +9 -0
- package/lib/web/types/src/native/prepare.d.ts +1 -0
- package/lib/web/types/src/native/spec/NativeNayanEngine.d.ts +181 -0
- package/lib/web/types/src/render/GameView.d.ts +94 -0
- package/lib/web/types/src/render/camera.d.ts +5 -0
- package/lib/web/types/src/render/host.d.ts +15 -0
- package/lib/web/types/src/render/meshes.d.ts +44 -0
- package/lib/web/types/src/render/quality.d.ts +45 -0
- package/lib/web/types/src/render/responder.d.ts +39 -0
- package/lib/web/types/src/render/touch.d.ts +25 -0
- package/lib/web/types/src/types.d.ts +71 -0
- package/lib/web/types/src/world/Scoreboard.d.ts +64 -0
- package/lib/web/types/src/world/TransformBatch.d.ts +38 -0
- package/lib/web/types/src/world/World.d.ts +198 -0
- package/lib/web/types/src/world/desc.d.ts +16 -0
- package/lib/web/types/src/world/effects.d.ts +155 -0
- package/lib/web/types/src/world/quat.d.ts +30 -0
- package/lib/web/types/src/world/types.d.ts +232 -0
- package/native/android/CMakeLists.txt +34 -0
- package/native/android/NayanEngineSpec.h +15 -0
- package/native/android/build.gradle +26 -0
- package/native/android/codegen/CMakeLists.txt +3 -0
- package/native/android/proguard-rules.pro +2 -0
- package/native/android/src/main/java/com/nayanui/engine/EngineSurfaceView.java +234 -0
- package/native/android/src/main/java/com/nayanui/engine/EngineSurfaceViewManager.java +45 -0
- package/native/android/src/main/java/com/nayanui/engine/NayanEnginePackage.java +24 -0
- package/native/core/build/EngineCore.xcframework/Info.plist +43 -0
- package/native/core/build/EngineCore.xcframework/ios-arm64/libengine_core.a +0 -0
- package/native/core/build/EngineCore.xcframework/ios-arm64-simulator/libengine_core.a +0 -0
- package/native/core/build/android/arm64-v8a/libengine_core.a +0 -0
- package/native/core/build/android/armeabi-v7a/libengine_core.a +0 -0
- package/native/core/build/android/x86/libengine_core.a +0 -0
- package/native/core/build/android/x86_64/libengine_core.a +0 -0
- package/native/core/include/drawing-licenses.txt +58 -0
- package/native/core/include/engine_core.h +322 -0
- package/native/cpp/NayanEngineModule.cpp +827 -0
- package/native/cpp/NayanEngineModule.h +144 -0
- package/native/cpp/generated/NayanEngineSpecJSI.cpp +2 -0
- package/native/cpp/generated/NayanEngineSpecJSI.h +921 -0
- package/native/ios/NayanEngineProvider.h +5 -0
- package/native/ios/NayanEngineProvider.mm +21 -0
- package/native/ios/NayanEngineSurface.mm +223 -0
- package/native/scripts/check-package.mjs +39 -0
- package/package.json +162 -4
- package/react-native.config.js +18 -0
- package/src/native/spec/NativeNayanEngine.ts +226 -0
|
@@ -0,0 +1,926 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Custom shapes: 2D vector paths, and solids built from them (extrusions, lathes, tubes, merged
|
|
4
|
+
// parts) that draw, collide and pick like loaded models.
|
|
5
|
+
import NativeEngine from '../native/NativeNayanEngine';
|
|
6
|
+
import { Builder, meshInfo, registerMesh, VERTEX_FLOATS } from "../render/meshes.js";
|
|
7
|
+
import { SHAPE_MESH } from "../types.js";
|
|
8
|
+
import { nativeGeometry } from "./model.js";
|
|
9
|
+
const TAU = Math.PI * 2;
|
|
10
|
+
/** Line segments per curve, arc or circle quarter when a path is flattened. */
|
|
11
|
+
const CURVE_STEPS = 12;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A 2D outline, built like an SVG path: x right, y down. Solids made from it
|
|
15
|
+
* (`extrude`, `lathe`) are flipped upright, so a path drawn the way it looks on screen stands the
|
|
16
|
+
* same way in the world. Coordinates are in world units: a 1 x 1 path makes a 1 unit wide shape.
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* const heart = Path.fromSVG("M0.5 0.9 C0 0.5 0 0.1 0.25 0.1 C0.4 0.1 0.5 0.25 0.5 0.3 C0.5 0.25 0.6 0.1 0.75 0.1 C1 0.1 1 0.5 0.5 0.9 Z");
|
|
20
|
+
* const star = new Path().addStar(0, 0, 0.5, 0.22, 5);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export class Path {
|
|
24
|
+
contours = [];
|
|
25
|
+
start = [0, 0];
|
|
26
|
+
last = [0, 0];
|
|
27
|
+
get current() {
|
|
28
|
+
let c = this.contours[this.contours.length - 1];
|
|
29
|
+
if (!c || c.closed) {
|
|
30
|
+
c = {
|
|
31
|
+
points: [[...this.last]],
|
|
32
|
+
closed: false
|
|
33
|
+
};
|
|
34
|
+
this.start = [...this.last];
|
|
35
|
+
this.contours.push(c);
|
|
36
|
+
}
|
|
37
|
+
return c;
|
|
38
|
+
}
|
|
39
|
+
add(x, y) {
|
|
40
|
+
this.current.points.push([x, y]);
|
|
41
|
+
this.last = [x, y];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Starts a new contour at (x, y). */
|
|
45
|
+
moveTo(x, y) {
|
|
46
|
+
const c = this.contours[this.contours.length - 1];
|
|
47
|
+
if (c && !c.closed && c.points.length === 1) this.contours.pop(); // a moveTo replaces a lone point
|
|
48
|
+
this.contours.push({
|
|
49
|
+
points: [[x, y]],
|
|
50
|
+
closed: false
|
|
51
|
+
});
|
|
52
|
+
this.start = [x, y];
|
|
53
|
+
this.last = [x, y];
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
lineTo(x, y) {
|
|
57
|
+
this.add(x, y);
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A quadratic Bézier through control point (cx, cy) to (x, y). */
|
|
62
|
+
quadraticTo(cx, cy, x, y) {
|
|
63
|
+
const [x0, y0] = this.last;
|
|
64
|
+
for (let i = 1; i <= CURVE_STEPS; i++) {
|
|
65
|
+
const t = i / CURVE_STEPS;
|
|
66
|
+
const u = 1 - t;
|
|
67
|
+
this.add(u * u * x0 + 2 * u * t * cx + t * t * x, u * u * y0 + 2 * u * t * cy + t * t * y);
|
|
68
|
+
}
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A cubic Bézier through control points (c1x, c1y) and (c2x, c2y) to (x, y). */
|
|
73
|
+
cubicTo(c1x, c1y, c2x, c2y, x, y) {
|
|
74
|
+
const [x0, y0] = this.last;
|
|
75
|
+
for (let i = 1; i <= CURVE_STEPS; i++) {
|
|
76
|
+
const t = i / CURVE_STEPS;
|
|
77
|
+
const u = 1 - t;
|
|
78
|
+
const a = u * u * u;
|
|
79
|
+
const b = 3 * u * u * t;
|
|
80
|
+
const c = 3 * u * t * t;
|
|
81
|
+
const d = t * t * t;
|
|
82
|
+
this.add(a * x0 + b * c1x + c * c2x + d * x, a * y0 + b * c1y + c * c2y + d * y);
|
|
83
|
+
}
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* An elliptical arc to (x, y), as in SVG's `A` command: radii rx, ry, the ellipse turned `rotation`
|
|
89
|
+
* radians, and the flags choosing which of the four possible arcs.
|
|
90
|
+
*/
|
|
91
|
+
arcTo(rx, ry, rotation, largeArc, sweep, x, y) {
|
|
92
|
+
const [x0, y0] = this.last;
|
|
93
|
+
rx = Math.abs(rx);
|
|
94
|
+
ry = Math.abs(ry);
|
|
95
|
+
if (rx < 1e-9 || ry < 1e-9 || x0 === x && y0 === y) return this.lineTo(x, y);
|
|
96
|
+
// Endpoint to center parameterization (SVG implementation notes, F.6.5).
|
|
97
|
+
const cos = Math.cos(rotation);
|
|
98
|
+
const sin = Math.sin(rotation);
|
|
99
|
+
const dx = (x0 - x) / 2;
|
|
100
|
+
const dy = (y0 - y) / 2;
|
|
101
|
+
const x1 = cos * dx + sin * dy;
|
|
102
|
+
const y1 = -sin * dx + cos * dy;
|
|
103
|
+
const grow = x1 * x1 / (rx * rx) + y1 * y1 / (ry * ry);
|
|
104
|
+
if (grow > 1) {
|
|
105
|
+
rx *= Math.sqrt(grow);
|
|
106
|
+
ry *= Math.sqrt(grow);
|
|
107
|
+
}
|
|
108
|
+
const num = rx * rx * ry * ry - rx * rx * y1 * y1 - ry * ry * x1 * x1;
|
|
109
|
+
const den = rx * rx * y1 * y1 + ry * ry * x1 * x1;
|
|
110
|
+
const k = (largeArc === sweep ? -1 : 1) * Math.sqrt(Math.max(0, num / den));
|
|
111
|
+
const cx1 = k * rx * y1 / ry;
|
|
112
|
+
const cy1 = -k * ry * x1 / rx;
|
|
113
|
+
const cx = cos * cx1 - sin * cy1 + (x0 + x) / 2;
|
|
114
|
+
const cy = sin * cx1 + cos * cy1 + (y0 + y) / 2;
|
|
115
|
+
const angle = (ux, uy, vx, vy) => Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy);
|
|
116
|
+
const start = angle(1, 0, (x1 - cx1) / rx, (y1 - cy1) / ry);
|
|
117
|
+
let delta = angle((x1 - cx1) / rx, (y1 - cy1) / ry, (-x1 - cx1) / rx, (-y1 - cy1) / ry);
|
|
118
|
+
if (!sweep && delta > 0) delta -= TAU;
|
|
119
|
+
if (sweep && delta < 0) delta += TAU;
|
|
120
|
+
const steps = Math.max(2, Math.ceil(Math.abs(delta) / (Math.PI / 2) * CURVE_STEPS));
|
|
121
|
+
for (let i = 1; i <= steps; i++) {
|
|
122
|
+
const t = start + delta * i / steps;
|
|
123
|
+
const ex = rx * Math.cos(t);
|
|
124
|
+
const ey = ry * Math.sin(t);
|
|
125
|
+
if (i === steps) this.add(x, y);else this.add(cos * ex - sin * ey + cx, sin * ex + cos * ey + cy);
|
|
126
|
+
}
|
|
127
|
+
return this;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Closes the current contour back to its first point. */
|
|
131
|
+
close() {
|
|
132
|
+
const c = this.contours[this.contours.length - 1];
|
|
133
|
+
if (c && !c.closed) {
|
|
134
|
+
c.closed = true;
|
|
135
|
+
this.last = [...this.start];
|
|
136
|
+
}
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A closed polygon through `points`. */
|
|
141
|
+
addPolygon(points, close = true) {
|
|
142
|
+
if (!points.length) return this;
|
|
143
|
+
this.moveTo(points[0][0], points[0][1]);
|
|
144
|
+
for (let i = 1; i < points.length; i++) this.lineTo(points[i][0], points[i][1]);
|
|
145
|
+
return close ? this.close() : this;
|
|
146
|
+
}
|
|
147
|
+
addRect(x, y, width, height) {
|
|
148
|
+
return this.addPolygon([[x, y], [x + width, y], [x + width, y + height], [x, y + height]]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** A rectangle with corners rounded by `radius` (or [rx, ry]). */
|
|
152
|
+
addRoundedRect(x, y, width, height, radius) {
|
|
153
|
+
let [rx, ry] = typeof radius === 'number' ? [radius, radius] : radius;
|
|
154
|
+
rx = Math.min(Math.abs(rx), width / 2);
|
|
155
|
+
ry = Math.min(Math.abs(ry), height / 2);
|
|
156
|
+
if (rx < 1e-9 || ry < 1e-9) return this.addRect(x, y, width, height);
|
|
157
|
+
return this.moveTo(x + rx, y).lineTo(x + width - rx, y).arcTo(rx, ry, 0, false, true, x + width, y + ry).lineTo(x + width, y + height - ry).arcTo(rx, ry, 0, false, true, x + width - rx, y + height).lineTo(x + rx, y + height).arcTo(rx, ry, 0, false, true, x, y + height - ry).lineTo(x, y + ry).arcTo(rx, ry, 0, false, true, x + rx, y).close();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** An ellipse filling the box. */
|
|
161
|
+
addOval(x, y, width, height) {
|
|
162
|
+
const rx = width / 2;
|
|
163
|
+
const ry = height / 2;
|
|
164
|
+
const cx = x + rx;
|
|
165
|
+
const cy = y + ry;
|
|
166
|
+
const steps = CURVE_STEPS * 4;
|
|
167
|
+
const points = [];
|
|
168
|
+
for (let i = 0; i < steps; i++) {
|
|
169
|
+
const t = i / steps * TAU;
|
|
170
|
+
points.push([cx + Math.cos(t) * rx, cy + Math.sin(t) * ry]);
|
|
171
|
+
}
|
|
172
|
+
return this.addPolygon(points);
|
|
173
|
+
}
|
|
174
|
+
addCircle(cx, cy, radius) {
|
|
175
|
+
return this.addOval(cx - radius, cy - radius, radius * 2, radius * 2);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** A star with `points` tips at `outer` radius and notches at `inner`, the first tip pointing up. */
|
|
179
|
+
addStar(cx, cy, outer, inner, points = 5) {
|
|
180
|
+
const corners = [];
|
|
181
|
+
for (let i = 0; i < points * 2; i++) {
|
|
182
|
+
const r = i % 2 ? inner : outer;
|
|
183
|
+
const t = -Math.PI / 2 + i / (points * 2) * TAU;
|
|
184
|
+
corners.push([cx + Math.cos(t) * r, cy + Math.sin(t) * r]);
|
|
185
|
+
}
|
|
186
|
+
return this.addPolygon(corners);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** A regular polygon with `sides` corners, the first pointing up. */
|
|
190
|
+
addRegularPolygon(cx, cy, radius, sides) {
|
|
191
|
+
const corners = [];
|
|
192
|
+
for (let i = 0; i < sides; i++) {
|
|
193
|
+
const t = -Math.PI / 2 + i / sides * TAU;
|
|
194
|
+
corners.push([cx + Math.cos(t) * radius, cy + Math.sin(t) * radius]);
|
|
195
|
+
}
|
|
196
|
+
return this.addPolygon(corners);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Parses SVG path data (`d`): M L H V C S Q T A Z, absolute and relative. Paste paths from Figma,
|
|
201
|
+
* Illustrator or any icon set (scale them with `fit` on the shape, or `transform` here).
|
|
202
|
+
*/
|
|
203
|
+
static fromSVG(d) {
|
|
204
|
+
const path = new Path();
|
|
205
|
+
const token = /[a-zA-Z]|[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g;
|
|
206
|
+
if (d.replace(token, '').replace(/[\s,]/g, '')) throw new Error('Path.fromSVG: invalid character in path data');
|
|
207
|
+
const tokens = d.match(token) ?? [];
|
|
208
|
+
let i = 0;
|
|
209
|
+
let command = '';
|
|
210
|
+
let [x, y] = [0, 0];
|
|
211
|
+
let control = null; // the last curve's second control point, for S and T
|
|
212
|
+
let lastType = '';
|
|
213
|
+
const num = () => {
|
|
214
|
+
const t = tokens[i++];
|
|
215
|
+
if (t === undefined || /^[a-zA-Z]$/.test(t) || !Number.isFinite(Number(t))) throw new Error(`Path.fromSVG: expected a number near "${t ?? 'the end'}"`);
|
|
216
|
+
return parseFloat(t);
|
|
217
|
+
};
|
|
218
|
+
const flag = () => num() !== 0;
|
|
219
|
+
while (i < tokens.length) {
|
|
220
|
+
if (/^[a-zA-Z]$/.test(tokens[i])) command = tokens[i++];else if (!command) throw new Error('Path.fromSVG: the path must start with a command');
|
|
221
|
+
const rel = command === command.toLowerCase();
|
|
222
|
+
const ox = rel ? x : 0;
|
|
223
|
+
const oy = rel ? y : 0;
|
|
224
|
+
switch (command.toUpperCase()) {
|
|
225
|
+
case 'M':
|
|
226
|
+
x = ox + num();
|
|
227
|
+
y = oy + num();
|
|
228
|
+
path.moveTo(x, y);
|
|
229
|
+
command = rel ? 'l' : 'L'; // further pairs are lines
|
|
230
|
+
control = null;
|
|
231
|
+
break;
|
|
232
|
+
case 'L':
|
|
233
|
+
x = ox + num();
|
|
234
|
+
y = oy + num();
|
|
235
|
+
path.lineTo(x, y);
|
|
236
|
+
control = null;
|
|
237
|
+
break;
|
|
238
|
+
case 'H':
|
|
239
|
+
x = ox + num();
|
|
240
|
+
path.lineTo(x, y);
|
|
241
|
+
control = null;
|
|
242
|
+
break;
|
|
243
|
+
case 'V':
|
|
244
|
+
y = oy + num();
|
|
245
|
+
path.lineTo(x, y);
|
|
246
|
+
control = null;
|
|
247
|
+
break;
|
|
248
|
+
case 'C':
|
|
249
|
+
{
|
|
250
|
+
const c1 = [ox + num(), oy + num()];
|
|
251
|
+
const c2 = [ox + num(), oy + num()];
|
|
252
|
+
x = ox + num();
|
|
253
|
+
y = oy + num();
|
|
254
|
+
path.cubicTo(c1[0], c1[1], c2[0], c2[1], x, y);
|
|
255
|
+
control = c2;
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
case 'S':
|
|
259
|
+
{
|
|
260
|
+
const c1 = control && 'CS'.includes(lastType) ? [2 * x - control[0], 2 * y - control[1]] : [x, y];
|
|
261
|
+
const c2 = [ox + num(), oy + num()];
|
|
262
|
+
x = ox + num();
|
|
263
|
+
y = oy + num();
|
|
264
|
+
path.cubicTo(c1[0], c1[1], c2[0], c2[1], x, y);
|
|
265
|
+
control = c2;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
case 'Q':
|
|
269
|
+
{
|
|
270
|
+
const c = [ox + num(), oy + num()];
|
|
271
|
+
x = ox + num();
|
|
272
|
+
y = oy + num();
|
|
273
|
+
path.quadraticTo(c[0], c[1], x, y);
|
|
274
|
+
control = c;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
case 'T':
|
|
278
|
+
{
|
|
279
|
+
const c = control && 'QT'.includes(lastType) ? [2 * x - control[0], 2 * y - control[1]] : [x, y];
|
|
280
|
+
x = ox + num();
|
|
281
|
+
y = oy + num();
|
|
282
|
+
path.quadraticTo(c[0], c[1], x, y);
|
|
283
|
+
control = c;
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
case 'A':
|
|
287
|
+
{
|
|
288
|
+
const rx = num();
|
|
289
|
+
const ry = num();
|
|
290
|
+
const rotation = num() * Math.PI / 180;
|
|
291
|
+
const large = flag();
|
|
292
|
+
const sweep = flag();
|
|
293
|
+
x = ox + num();
|
|
294
|
+
y = oy + num();
|
|
295
|
+
path.arcTo(rx, ry, rotation, large, sweep, x, y);
|
|
296
|
+
control = null;
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
case 'Z':
|
|
300
|
+
path.close();
|
|
301
|
+
[x, y] = path.last;
|
|
302
|
+
control = null;
|
|
303
|
+
break;
|
|
304
|
+
default:
|
|
305
|
+
throw new Error(`Path.fromSVG: unknown command "${command}"`);
|
|
306
|
+
}
|
|
307
|
+
lastType = command.toUpperCase();
|
|
308
|
+
// Z takes no arguments. A following number must fail instead of repeating Z forever.
|
|
309
|
+
if (lastType === 'Z') command = '';
|
|
310
|
+
}
|
|
311
|
+
return path;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** A copy moved by (dx, dy) and scaled by sx, sy (about the origin, before moving). */
|
|
315
|
+
transform(sx, sy = sx, dx = 0, dy = 0) {
|
|
316
|
+
const out = new Path();
|
|
317
|
+
out.contours = this.contours.map(c => ({
|
|
318
|
+
closed: c.closed,
|
|
319
|
+
points: c.points.map(([x, y]) => [x * sx + dx, y * sy + dy])
|
|
320
|
+
}));
|
|
321
|
+
out.start = [this.start[0] * sx + dx, this.start[1] * sy + dy];
|
|
322
|
+
out.last = [this.last[0] * sx + dx, this.last[1] * sy + dy];
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** The flattened contours (curves turned into points), y down as drawn. */
|
|
327
|
+
toContours() {
|
|
328
|
+
return this.contours.filter(c => c.points.length > 1).map(c => c.points.map(p => [p[0], p[1]]));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** [minX, minY, maxX, maxY], or undefined for an empty path. */
|
|
332
|
+
bounds() {
|
|
333
|
+
let minX = Infinity,
|
|
334
|
+
minY = Infinity,
|
|
335
|
+
maxX = -Infinity,
|
|
336
|
+
maxY = -Infinity;
|
|
337
|
+
let found = false;
|
|
338
|
+
for (const contour of this.contours) for (const [x, y] of contour.points) {
|
|
339
|
+
found = true;
|
|
340
|
+
minX = Math.min(minX, x);
|
|
341
|
+
minY = Math.min(minY, y);
|
|
342
|
+
maxX = Math.max(maxX, x);
|
|
343
|
+
maxY = Math.max(maxY, y);
|
|
344
|
+
}
|
|
345
|
+
return found ? [minX, minY, maxX, maxY] : undefined;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ── Geometry ─────────────────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Shape geometry, before it's registered: 11 floats per vertex (position, normal, color, uv) and
|
|
353
|
+
* triangle indices. Builders return it; `merge` combines it; `createShape` turns it into a mesh.
|
|
354
|
+
*/
|
|
355
|
+
|
|
356
|
+
const native = () => {
|
|
357
|
+
if (!NativeEngine) throw new Error('shapes: the native core is not linked into this build');
|
|
358
|
+
if (typeof NativeEngine.meshCreate !== 'function') throw new Error("shapes: this app's native core predates custom shapes: rebuild it");
|
|
359
|
+
return NativeEngine;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
/** Sets every vertex's color (vertex colors multiply the entity's color). */
|
|
363
|
+
function paint(g, color) {
|
|
364
|
+
if (!color) return g;
|
|
365
|
+
for (let v = 0; v < g.vertices.length; v += VERTEX_FLOATS) {
|
|
366
|
+
g.vertices[v + 6] = color[0];
|
|
367
|
+
g.vertices[v + 7] = color[1];
|
|
368
|
+
g.vertices[v + 8] = color[2];
|
|
369
|
+
}
|
|
370
|
+
return g;
|
|
371
|
+
}
|
|
372
|
+
const fromBuilder = b => ({
|
|
373
|
+
vertices: new Float32Array(b.vertices),
|
|
374
|
+
indices: new Uint32Array(b.indices)
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
/** A built-in shape's geometry (unit-sized, centered), to `merge` with others. */
|
|
378
|
+
export function shapeGeometry(shape, options = {}) {
|
|
379
|
+
const info = meshInfo(SHAPE_MESH[shape]);
|
|
380
|
+
if (!info) throw new Error(`shapeGeometry: no geometry for "${shape}" (await warmUpEngine() first on the web)`);
|
|
381
|
+
return paint({
|
|
382
|
+
vertices: info.geometry.vertices.slice(),
|
|
383
|
+
indices: info.geometry.indices.slice()
|
|
384
|
+
}, options.color);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* A 2D path filled and pushed into a solid, facing +Z: badges, coins, leaves, hearts, stars, logos,
|
|
388
|
+
* fruit slices. Holes follow the fill rule. Textures map across the front face.
|
|
389
|
+
*/
|
|
390
|
+
export function extrude(path, options = {}) {
|
|
391
|
+
const contours = path.toContours();
|
|
392
|
+
const counts = contours.map(c => c.length);
|
|
393
|
+
const points = new Float32Array(counts.reduce((a, b) => a + b, 0) * 2);
|
|
394
|
+
let k = 0;
|
|
395
|
+
for (const c of contours) {
|
|
396
|
+
for (const [x, y] of c) {
|
|
397
|
+
points[k++] = x;
|
|
398
|
+
points[k++] = -y; // y down in the path, up in the world
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const depth = options.depth ?? 0.2;
|
|
402
|
+
const vertices = new Float32Array(native().pathExtrude(points.buffer, counts, depth, options.fillRule === 'evenodd'));
|
|
403
|
+
if (!vertices.length) throw new Error('extrude: the path encloses no area');
|
|
404
|
+
const indices = new Uint32Array(vertices.length / VERTEX_FLOATS);
|
|
405
|
+
for (let i = 0; i < indices.length; i++) indices[i] = i;
|
|
406
|
+
return paint({
|
|
407
|
+
vertices,
|
|
408
|
+
indices
|
|
409
|
+
}, options.color);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Where a color function is asked about: `u` around (0..1), `v` along (0..1), and the surface there. */
|
|
413
|
+
|
|
414
|
+
/** A flat color, or one per point of the surface: blushes, stripes, speckles, spots. */
|
|
415
|
+
|
|
416
|
+
/** A flat color, or one per point of a round face: `r` from its center (0) to its rim (1), `angle` around it. */
|
|
417
|
+
|
|
418
|
+
/** Colors each vertex by `fn` of its surface point (u, v read from the texture coordinates). */
|
|
419
|
+
function paintBy(g, color) {
|
|
420
|
+
if (typeof color !== 'function') return paint(g, color);
|
|
421
|
+
const v = g.vertices;
|
|
422
|
+
for (let i = 0; i < v.length; i += VERTEX_FLOATS) {
|
|
423
|
+
const c = color({
|
|
424
|
+
u: v[i + 9],
|
|
425
|
+
v: v[i + 10],
|
|
426
|
+
position: [v[i], v[i + 1], v[i + 2]],
|
|
427
|
+
normal: [v[i + 3], v[i + 4], v[i + 5]]
|
|
428
|
+
});
|
|
429
|
+
v[i + 6] = c[0];
|
|
430
|
+
v[i + 7] = c[1];
|
|
431
|
+
v[i + 8] = c[2];
|
|
432
|
+
}
|
|
433
|
+
return g;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Recolors geometry point by point (in place, and returned): two-tone leaves, painted tips, stripes on
|
|
438
|
+
* an extrusion. `u` and `v` are the texture coordinates there.
|
|
439
|
+
*/
|
|
440
|
+
export function recolor(geometry, color) {
|
|
441
|
+
return paintBy(geometry, color);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* A round face: a polar grid of `rings` x `segments` around `center` in the plane spanned by unit
|
|
446
|
+
* vectors `x` and `y` (radius `radius(angle)` toward the rim), facing `normal`, swept `sweep` radians.
|
|
447
|
+
*/
|
|
448
|
+
function roundFace(center, x, y, normal, radius, sweep, segments, rings, color) {
|
|
449
|
+
const b = new Builder();
|
|
450
|
+
b.grid(rings, segments, (u, v) => {
|
|
451
|
+
const a = u * sweep;
|
|
452
|
+
const r = radius(u) * v;
|
|
453
|
+
const [c, s] = [Math.cos(a) * r, Math.sin(a) * r];
|
|
454
|
+
return [[center[0] + x[0] * c + y[0] * s, center[1] + x[1] * c + y[1] * s, center[2] + x[2] * c + y[2] * s], normal];
|
|
455
|
+
});
|
|
456
|
+
const g = facingOutward(fromBuilder(b));
|
|
457
|
+
const vs = g.vertices;
|
|
458
|
+
for (let i = 0; i < vs.length; i += VERTEX_FLOATS) {
|
|
459
|
+
const a = vs[i + 9] * sweep;
|
|
460
|
+
const r = vs[i + 10];
|
|
461
|
+
// Texture coordinates across the face, like a disc's.
|
|
462
|
+
vs[i + 9] = 0.5 + Math.cos(a) * r / 2;
|
|
463
|
+
vs[i + 10] = 0.5 + Math.sin(a) * r / 2;
|
|
464
|
+
if (typeof color === 'function') {
|
|
465
|
+
const c = color(r, a);
|
|
466
|
+
vs[i + 6] = c[0];
|
|
467
|
+
vs[i + 7] = c[1];
|
|
468
|
+
vs[i + 8] = c[2];
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return typeof color === 'function' ? g : paint(g, color);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Whether a typed array covers its whole ArrayBuffer (so the native passes can take the buffer). */
|
|
475
|
+
const whole = a => a.byteOffset === 0 && a.byteLength === a.buffer.byteLength;
|
|
476
|
+
|
|
477
|
+
/** The Rust geometry passes, if this app's native core has them and the arrays can be handed over. */
|
|
478
|
+
const nativeGeometry3 = g => NativeEngine && typeof NativeEngine.geometryPlace === 'function' && whole(g.vertices) && whole(g.indices) ? NativeEngine : null;
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Recomputes corner-angle-weighted normals in place, welding UV seams and poles at 1e-5 units.
|
|
482
|
+
* Uses Rust when available. Call on a sculpted surface before merging caps or other hard edges:
|
|
483
|
+
* coincident vertices share a normal. Degenerate faces leave the original normals intact.
|
|
484
|
+
*/
|
|
485
|
+
export function smoothNormals(g) {
|
|
486
|
+
const n = nativeGeometry3(g);
|
|
487
|
+
if (n) {
|
|
488
|
+
n.geometrySmoothNormals(g.vertices.buffer, g.indices.buffer);
|
|
489
|
+
return g;
|
|
490
|
+
}
|
|
491
|
+
const v = g.vertices;
|
|
492
|
+
const key = i => `${Math.round(v[i * VERTEX_FLOATS] * 1e5)},${Math.round(v[i * VERTEX_FLOATS + 1] * 1e5)},${Math.round(v[i * VERTEX_FLOATS + 2] * 1e5)}`;
|
|
493
|
+
const sums = new Map();
|
|
494
|
+
const keys = [];
|
|
495
|
+
for (let i = 0; i < v.length / VERTEX_FLOATS; i++) keys.push(key(i));
|
|
496
|
+
const idx = g.indices;
|
|
497
|
+
for (let t = 0; t + 2 < idx.length; t += 3) {
|
|
498
|
+
if (idx[t] >= keys.length || idx[t + 1] >= keys.length || idx[t + 2] >= keys.length) continue;
|
|
499
|
+
const [a, b, c] = [idx[t] * VERTEX_FLOATS, idx[t + 1] * VERTEX_FLOATS, idx[t + 2] * VERTEX_FLOATS];
|
|
500
|
+
const e1 = [v[b] - v[a], v[b + 1] - v[a + 1], v[b + 2] - v[a + 2]];
|
|
501
|
+
const e2 = [v[c] - v[a], v[c + 1] - v[a + 1], v[c + 2] - v[a + 2]];
|
|
502
|
+
const f = [e1[1] * e2[2] - e1[2] * e2[1], e1[2] * e2[0] - e1[0] * e2[2], e1[0] * e2[1] - e1[1] * e2[0]];
|
|
503
|
+
const length = Math.hypot(f[0], f[1], f[2]);
|
|
504
|
+
if (length === 0 || !Number.isFinite(length)) continue;
|
|
505
|
+
for (let corner = 0; corner < 3; corner++) {
|
|
506
|
+
const i = idx[t + corner];
|
|
507
|
+
const next = idx[t + (corner + 1) % 3] * VERTEX_FLOATS;
|
|
508
|
+
const previous = idx[t + (corner + 2) % 3] * VERTEX_FLOATS;
|
|
509
|
+
let dot = 0;
|
|
510
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
511
|
+
const origin = v[i * VERTEX_FLOATS + axis];
|
|
512
|
+
dot += (v[next + axis] - origin) * (v[previous + axis] - origin);
|
|
513
|
+
}
|
|
514
|
+
const weight = Math.atan2(length, dot) / length;
|
|
515
|
+
const k = keys[i];
|
|
516
|
+
const sum = sums.get(k) ?? [0, 0, 0];
|
|
517
|
+
sum[0] += f[0] * weight;
|
|
518
|
+
sum[1] += f[1] * weight;
|
|
519
|
+
sum[2] += f[2] * weight;
|
|
520
|
+
sums.set(k, sum);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
for (let i = 0; i < keys.length; i++) {
|
|
524
|
+
const n = sums.get(keys[i]);
|
|
525
|
+
const len = n ? Math.hypot(n[0], n[1], n[2]) : 0;
|
|
526
|
+
if (len > 1e-12) v.set([n[0] / len, n[1] / len, n[2] / len], i * VERTEX_FLOATS + 3);
|
|
527
|
+
}
|
|
528
|
+
return g;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Spins a profile around the vertical axis, like a potter's wheel: fruit (apples, oranges, lemons,
|
|
533
|
+
* pears, cherries), vases, bottles, bowls, chess pieces, spinning tops. The profile is a `Path`
|
|
534
|
+
* (x = distance from the axis, y down, as drawn) or [radius, height] points (y up), top to bottom.
|
|
535
|
+
* Open ends away from the axis are capped. With `sweep` < TAU the cut faces are closed too.
|
|
536
|
+
*/
|
|
537
|
+
export function lathe(profile, options = {}) {
|
|
538
|
+
let points = profile instanceof Path ? (profile.toContours()[0] ?? []).map(([x, y]) => [Math.abs(x), -y]) : profile.map(([r, y]) => [Math.abs(r), y]);
|
|
539
|
+
points = points.filter((p, i) => i === 0 || Math.hypot(p[0] - points[i - 1][0], p[1] - points[i - 1][1]) > 1e-9);
|
|
540
|
+
if (points.length < 2) throw new Error('lathe: the profile needs at least two points');
|
|
541
|
+
if (points[0][1] < points[points.length - 1][1]) points.reverse(); // top to bottom
|
|
542
|
+
const segments = Math.max(3, Math.round(options.segments ?? 32));
|
|
543
|
+
const sweep = Math.min(TAU, Math.max(1e-3, options.sweep ?? TAU));
|
|
544
|
+
const full = sweep >= TAU - 1e-6;
|
|
545
|
+
const last = points.length - 1;
|
|
546
|
+
|
|
547
|
+
// Profile normals in the (radius, height) plane: the average of the neighbouring edges', turned outward.
|
|
548
|
+
const edge = i => {
|
|
549
|
+
const a = points[Math.max(0, i)];
|
|
550
|
+
const b = points[Math.min(last, i + 1)];
|
|
551
|
+
const len = Math.hypot(b[0] - a[0], b[1] - a[1]) || 1;
|
|
552
|
+
return [(a[1] - b[1]) / len, (b[0] - a[0]) / len]; // going down the profile, outward is to the left
|
|
553
|
+
};
|
|
554
|
+
const normals = points.map(([r], i) => {
|
|
555
|
+
// On the axis the surface is a pole: straight up at the top, down at the bottom.
|
|
556
|
+
if (r < 1e-6 && (i === 0 || i === last)) return [0, i === 0 ? 1 : -1];
|
|
557
|
+
const e1 = edge(i - 1);
|
|
558
|
+
const e2 = edge(i);
|
|
559
|
+
const n = i === 0 ? e2 : i === last ? e1 : [e1[0] + e2[0], e1[1] + e2[1]];
|
|
560
|
+
const len = Math.hypot(n[0], n[1]) || 1;
|
|
561
|
+
return [n[0] / len, n[1] / len];
|
|
562
|
+
});
|
|
563
|
+
const b = new Builder();
|
|
564
|
+
const displace = options.displace;
|
|
565
|
+
// a = u * sweep around the axis; the same orientation as the built-in sphere, so faces point outward.
|
|
566
|
+
b.grid(last, segments, (u, v) => {
|
|
567
|
+
const i = Math.round(v * last);
|
|
568
|
+
const a = u * sweep;
|
|
569
|
+
const [r, y] = points[i];
|
|
570
|
+
const [nr, ny] = normals[i];
|
|
571
|
+
const n = [nr * Math.cos(a), ny, -nr * Math.sin(a)];
|
|
572
|
+
// The seam and the poles must move together, or the surface tears.
|
|
573
|
+
const d = displace ? displace(r < 1e-6 || full && u === 1 ? 0 : u, v) : 0;
|
|
574
|
+
return [[r * Math.cos(a) + n[0] * d, y + n[1] * d, -r * Math.sin(a) + n[2] * d], n];
|
|
575
|
+
});
|
|
576
|
+
const parts = [{
|
|
577
|
+
geometry: paintBy(displace ? smoothNormals(fromBuilder(b)) : fromBuilder(b), options.color)
|
|
578
|
+
}];
|
|
579
|
+
|
|
580
|
+
// Ends away from the axis: flat round faces (part of a circle when the sweep is partial).
|
|
581
|
+
const capColor = options.capColor ?? (typeof options.color === 'function' ? undefined : options.color);
|
|
582
|
+
const rings = Math.max(1, Math.round(options.capRings ?? (typeof capColor === 'function' ? 12 : 1)));
|
|
583
|
+
for (const [i, facing] of [[0, 1], [last, -1]]) {
|
|
584
|
+
const [r, y] = points[i];
|
|
585
|
+
if (r < 1e-6) continue;
|
|
586
|
+
parts.push({
|
|
587
|
+
geometry: roundFace([0, y, 0], [1, 0, 0], [0, 0, -1], [0, facing, 0], () => r, sweep, segments, rings, capColor)
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
if (full) return parts.length > 1 ? merge(parts) : parts[0].geometry;
|
|
591
|
+
|
|
592
|
+
// The cut: the region between the profile and the axis, laid into the planes at a = 0 and a = sweep.
|
|
593
|
+
// A profile whose height only falls gets a grid (so a color function has points to work with);
|
|
594
|
+
// one that turns back (a dimple) is filled exactly by the core.
|
|
595
|
+
const cutColor = options.cutColor ?? (typeof options.color === 'function' ? undefined : options.color);
|
|
596
|
+
const falling = points.every((p, i) => i === 0 || p[1] <= points[i - 1][1] + 1e-9);
|
|
597
|
+
let face;
|
|
598
|
+
if (falling) {
|
|
599
|
+
const g = new Builder();
|
|
600
|
+
const steps = Math.max(1, Math.round(options.cutSteps ?? (typeof cutColor === 'function' ? 12 : 1)));
|
|
601
|
+
g.grid(last, steps, (u, v) => {
|
|
602
|
+
const [r, y] = points[Math.round(v * last)];
|
|
603
|
+
return [[u * r, y, 0], [0, 0, 1]];
|
|
604
|
+
});
|
|
605
|
+
face = facingOutward(fromBuilder(g));
|
|
606
|
+
} else {
|
|
607
|
+
face = extrude(new Path().addPolygon([[0, -points[0][1]], ...points.map(([r, y]) => [r, -y]), [0, -points[last][1]]]), {
|
|
608
|
+
depth: 0
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
if (typeof cutColor === 'function') {
|
|
612
|
+
const widest = Math.max(...points.map(p => p[0]), 1e-6);
|
|
613
|
+
const skinAt = y => {
|
|
614
|
+
// The profile's radius at height y (for r from the axis to the skin).
|
|
615
|
+
let best = 0;
|
|
616
|
+
for (let i = 0; i < last; i++) {
|
|
617
|
+
const [r0, y0] = points[i];
|
|
618
|
+
const [r1, y1] = points[i + 1];
|
|
619
|
+
if ((y - y0) * (y - y1) <= 0 && y0 !== y1) best = Math.max(best, r0 + (y - y0) / (y1 - y0) * (r1 - r0));
|
|
620
|
+
}
|
|
621
|
+
return best || widest;
|
|
622
|
+
};
|
|
623
|
+
const fv = face.vertices;
|
|
624
|
+
for (let i = 0; i < fv.length; i += VERTEX_FLOATS) {
|
|
625
|
+
const c = cutColor(Math.min(1, fv[i] / skinAt(fv[i + 1])), fv[i + 1]);
|
|
626
|
+
fv.set(c.slice(0, 3), i + 6);
|
|
627
|
+
}
|
|
628
|
+
} else {
|
|
629
|
+
paint(face, cutColor);
|
|
630
|
+
}
|
|
631
|
+
const cut = (angle, flip) => {
|
|
632
|
+
const g = {
|
|
633
|
+
vertices: face.vertices.slice(),
|
|
634
|
+
indices: face.indices.slice()
|
|
635
|
+
};
|
|
636
|
+
const [c, s] = [Math.cos(angle), Math.sin(angle)];
|
|
637
|
+
const n = angle === 0 ? [0, 0, 1] : [-s, 0, -c];
|
|
638
|
+
for (let v = 0; v < g.vertices.length; v += VERTEX_FLOATS) {
|
|
639
|
+
const r = g.vertices[v];
|
|
640
|
+
g.vertices[v] = r * c;
|
|
641
|
+
g.vertices[v + 2] = -r * s;
|
|
642
|
+
g.vertices.set(n, v + 3);
|
|
643
|
+
}
|
|
644
|
+
if (flip) for (let t = 0; t < g.indices.length; t += 3) [g.indices[t + 1], g.indices[t + 2]] = [g.indices[t + 2], g.indices[t + 1]];
|
|
645
|
+
return g;
|
|
646
|
+
};
|
|
647
|
+
return merge([...parts, {
|
|
648
|
+
geometry: cut(0, false)
|
|
649
|
+
}, {
|
|
650
|
+
geometry: cut(sweep, true)
|
|
651
|
+
}]);
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* A tube along a smooth curve through `points`: bananas, stems, chillies, worms, snakes, ropes,
|
|
655
|
+
* pipes, tentacles, candy canes.
|
|
656
|
+
*/
|
|
657
|
+
export function tube(points, options = {}) {
|
|
658
|
+
if (points.some(p => p.some(v => !Number.isFinite(v)))) throw new Error('tube: points must be finite');
|
|
659
|
+
const distance = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
660
|
+
points = points.filter((p, i) => i === 0 || distance(p, points[i - 1]) > 1e-9);
|
|
661
|
+
const closed = options.closed ?? false;
|
|
662
|
+
if (closed && points.length > 1 && distance(points[0], points[points.length - 1]) < 1e-9) points = points.slice(0, -1);
|
|
663
|
+
if (points.length < (closed ? 3 : 2)) throw new Error(`tube: needs at least ${closed ? 'three' : 'two'} distinct points`);
|
|
664
|
+
const radius = typeof options.radius === 'function' ? options.radius : () => options.radius ?? 0.1;
|
|
665
|
+
const shape = options.shape ?? (() => 1);
|
|
666
|
+
const sides = Math.max(3, Math.round(options.sides ?? 12));
|
|
667
|
+
const steps = Math.max(1, Math.round(options.smoothness ?? 8));
|
|
668
|
+
|
|
669
|
+
// Catmull-Rom through the points.
|
|
670
|
+
const p = i => points[closed ? (i + points.length) % points.length : Math.max(0, Math.min(points.length - 1, i))];
|
|
671
|
+
const curve = [];
|
|
672
|
+
for (let i = 0; i < points.length - (closed ? 0 : 1); i++) {
|
|
673
|
+
const [a, b, c, d] = [p(i - 1), p(i), p(i + 1), p(i + 2)];
|
|
674
|
+
for (let s = 0; s < steps; s++) {
|
|
675
|
+
const t = s / steps;
|
|
676
|
+
const t2 = t * t;
|
|
677
|
+
const t3 = t2 * t;
|
|
678
|
+
const coordinate = k => 0.5 * (2 * b[k] + (-a[k] + c[k]) * t + (2 * a[k] - 5 * b[k] + 4 * c[k] - d[k]) * t2 + (-a[k] + 3 * b[k] - 3 * c[k] + d[k]) * t3);
|
|
679
|
+
curve.push([coordinate(0), coordinate(1), coordinate(2)]);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
curve.push(p(closed ? 0 : points.length - 1));
|
|
683
|
+
const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
684
|
+
const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
685
|
+
const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
686
|
+
const norm = a => {
|
|
687
|
+
const l = Math.hypot(a[0], a[1], a[2]) || 1;
|
|
688
|
+
return [a[0] / l, a[1] / l, a[2] / l];
|
|
689
|
+
};
|
|
690
|
+
const last = curve.length - 1;
|
|
691
|
+
const tangents = curve.map((_, i) => {
|
|
692
|
+
let delta = sub(curve[closed && i === last ? 1 : Math.min(last, i + 1)], curve[closed && i === 0 ? last - 1 : Math.max(0, i - 1)]);
|
|
693
|
+
// A reversing curve has no central derivative. Use its incoming/outgoing segment instead.
|
|
694
|
+
if (Math.hypot(...delta) < 1e-9) delta = i > 0 ? sub(curve[i], curve[i - 1]) : sub(curve[1], curve[0]);
|
|
695
|
+
return norm(delta);
|
|
696
|
+
});
|
|
697
|
+
if (closed) tangents[last] = tangents[0];
|
|
698
|
+
// Minimal rotations between tangents: projecting the previous normal onto the next plane
|
|
699
|
+
// collapses at right-angle bends and adds unwanted twist to curls.
|
|
700
|
+
const t0 = tangents[0];
|
|
701
|
+
let normal = norm(cross(t0, Math.abs(t0[1]) < 0.9 ? [0, 1, 0] : [1, 0, 0]));
|
|
702
|
+
let previous = t0;
|
|
703
|
+
const frames = [];
|
|
704
|
+
for (const t of tangents) {
|
|
705
|
+
const axis = cross(previous, t);
|
|
706
|
+
const cosine = Math.max(-1, Math.min(1, dot(previous, t)));
|
|
707
|
+
if (cosine > -1 + 1e-8) {
|
|
708
|
+
const turn = cross(axis, normal);
|
|
709
|
+
const second = cross(axis, turn);
|
|
710
|
+
normal = [normal[0] + turn[0] + second[0] / (1 + cosine), normal[1] + turn[1] + second[1] / (1 + cosine), normal[2] + turn[2] + second[2] / (1 + cosine)];
|
|
711
|
+
}
|
|
712
|
+
// At a reversal, a half turn about the normal retains it and reverses the binormal.
|
|
713
|
+
const projection = dot(normal, t);
|
|
714
|
+
normal = norm(sub(normal, [t[0] * projection, t[1] * projection, t[2] * projection]));
|
|
715
|
+
frames.push([normal, cross(t, normal)]);
|
|
716
|
+
previous = t;
|
|
717
|
+
}
|
|
718
|
+
if (closed) {
|
|
719
|
+
const end = frames[last][0],
|
|
720
|
+
start = frames[0][0];
|
|
721
|
+
const correction = Math.atan2(dot(t0, cross(end, start)), dot(end, start));
|
|
722
|
+
const lengths = [0];
|
|
723
|
+
for (let i = 1; i <= last; i++) lengths.push(lengths[i - 1] + distance(curve[i], curve[i - 1]));
|
|
724
|
+
for (let i = 1; i <= last; i++) {
|
|
725
|
+
const angle = correction * lengths[i] / lengths[last];
|
|
726
|
+
const [n, bn] = frames[i];
|
|
727
|
+
const c = Math.cos(angle),
|
|
728
|
+
s = Math.sin(angle);
|
|
729
|
+
const corrected = [n[0] * c + bn[0] * s, n[1] * c + bn[1] * s, n[2] * c + bn[2] * s];
|
|
730
|
+
frames[i] = [corrected, cross(tangents[i], corrected)];
|
|
731
|
+
}
|
|
732
|
+
frames[last] = frames[0];
|
|
733
|
+
}
|
|
734
|
+
const b = new Builder();
|
|
735
|
+
const ring = curve.map((c, i) => {
|
|
736
|
+
const r = Math.max(0, radius(closed && i === last ? 0 : i / last));
|
|
737
|
+
const [n, bn] = frames[i];
|
|
738
|
+
const first = b.vertices.length / VERTEX_FLOATS;
|
|
739
|
+
for (let j = 0; j <= sides; j++) {
|
|
740
|
+
const a = (j === sides ? 0 : j / sides) * TAU;
|
|
741
|
+
const cosine = Math.cos(a),
|
|
742
|
+
sine = Math.sin(a);
|
|
743
|
+
const dir = [cosine * n[0] + sine * bn[0], cosine * n[1] + sine * bn[1], cosine * n[2] + sine * bn[2]];
|
|
744
|
+
const rj = r * shape(j === sides ? 0 : j / sides);
|
|
745
|
+
b.vertex([c[0] + dir[0] * rj, c[1] + dir[1] * rj, c[2] + dir[2] * rj], dir, j / sides, i / (curve.length - 1));
|
|
746
|
+
}
|
|
747
|
+
return {
|
|
748
|
+
first,
|
|
749
|
+
r
|
|
750
|
+
};
|
|
751
|
+
});
|
|
752
|
+
for (let i = 0; i < ring.length - 1; i++) {
|
|
753
|
+
for (let j = 0; j < sides; j++) {
|
|
754
|
+
const a = ring[i].first + j;
|
|
755
|
+
const c = ring[i + 1].first + j;
|
|
756
|
+
b.indices.push(a, a + 1, c, a + 1, c + 1, c);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
let side = facingOutward(fromBuilder(b));
|
|
760
|
+
// Taper changes the longitudinal component too; radial normals only describe a cylinder.
|
|
761
|
+
if (options.shape || typeof options.radius === 'function') side = smoothNormals(side);
|
|
762
|
+
const parts = [{
|
|
763
|
+
geometry: paintBy(side, options.color)
|
|
764
|
+
}];
|
|
765
|
+
if (!closed && (options.caps ?? true)) {
|
|
766
|
+
const capColor = options.capColor ?? (typeof options.color === 'function' ? undefined : options.color);
|
|
767
|
+
const rings = typeof capColor === 'function' ? 8 : 1;
|
|
768
|
+
for (const [i, facing] of [[0, -1], [ring.length - 1, 1]]) {
|
|
769
|
+
const {
|
|
770
|
+
r
|
|
771
|
+
} = ring[i];
|
|
772
|
+
if (r < 1e-6) continue;
|
|
773
|
+
const t = tangents[i];
|
|
774
|
+
const [fn, fb] = frames[i];
|
|
775
|
+
parts.push({
|
|
776
|
+
geometry: roundFace(curve[i], fn, fb, [t[0] * facing, t[1] * facing, t[2] * facing], u => r * shape(u === 1 ? 0 : u), TAU, sides, rings, capColor)
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return parts.length > 1 ? merge(parts) : parts[0].geometry;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** Flips triangles whose winding disagrees with their vertex normals (so back-face culling keeps them). */
|
|
784
|
+
function facingOutward(g) {
|
|
785
|
+
const native = nativeGeometry3(g);
|
|
786
|
+
if (native) {
|
|
787
|
+
native.geometryFaceOutward(g.vertices.buffer, g.indices.buffer);
|
|
788
|
+
return g;
|
|
789
|
+
}
|
|
790
|
+
const v = g.vertices;
|
|
791
|
+
const at = (i, k) => v[g.indices[i] * VERTEX_FLOATS + k];
|
|
792
|
+
for (let t = 0; t < g.indices.length; t += 3) {
|
|
793
|
+
const e1 = [0, 1, 2].map(k => at(t + 1, k) - at(t, k));
|
|
794
|
+
const e2 = [0, 1, 2].map(k => at(t + 2, k) - at(t, k));
|
|
795
|
+
const face = [e1[1] * e2[2] - e1[2] * e2[1], e1[2] * e2[0] - e1[0] * e2[2], e1[0] * e2[1] - e1[1] * e2[0]];
|
|
796
|
+
const n = [0, 1, 2].map(k => at(t, k + 3) + at(t + 1, k + 3) + at(t + 2, k + 3));
|
|
797
|
+
if (face[0] * n[0] + face[1] * n[1] + face[2] * n[2] < 0) {
|
|
798
|
+
[g.indices[t + 1], g.indices[t + 2]] = [g.indices[t + 2], g.indices[t + 1]];
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return g;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Bakes parts into one geometry, each scaled, rotated, moved and colored. One mesh draws in one
|
|
805
|
+
* instanced call and moves, spins and slices as one entity.
|
|
806
|
+
*/
|
|
807
|
+
export function merge(parts) {
|
|
808
|
+
const vertexCount = parts.reduce((n, p) => n + p.geometry.vertices.length, 0);
|
|
809
|
+
const indexCount = parts.reduce((n, p) => n + p.geometry.indices.length, 0);
|
|
810
|
+
const vertices = new Float32Array(vertexCount);
|
|
811
|
+
const indices = new Uint32Array(indexCount);
|
|
812
|
+
let vo = 0;
|
|
813
|
+
let io = 0;
|
|
814
|
+
for (const part of parts) {
|
|
815
|
+
const [px, py, pz] = part.position ?? [0, 0, 0];
|
|
816
|
+
const s = part.scale ?? 1;
|
|
817
|
+
const [sx, sy, sz] = typeof s === 'number' ? [s, s, s] : s;
|
|
818
|
+
const [qx, qy, qz, qw] = part.rotation ?? [0, 0, 0, 1];
|
|
819
|
+
const fast = nativeGeometry3(part.geometry);
|
|
820
|
+
if (fast) {
|
|
821
|
+
const c = part.color ?? [1, 1, 1];
|
|
822
|
+
const placement = [px, py, pz, qx, qy, qz, qw, sx, sy, sz, c[0], c[1], c[2]];
|
|
823
|
+
fast.geometryPlace(part.geometry.vertices.buffer, part.geometry.indices.buffer, placement, vo / VERTEX_FLOATS, vertices.buffer, vo, indices.buffer, io);
|
|
824
|
+
vo += part.geometry.vertices.length;
|
|
825
|
+
io += part.geometry.indices.length;
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
const rotate = (x, y, z) => {
|
|
829
|
+
// v + 2w(q x v) + 2(q x (q x v))
|
|
830
|
+
const tx = 2 * (qy * z - qz * y);
|
|
831
|
+
const ty = 2 * (qz * x - qx * z);
|
|
832
|
+
const tz = 2 * (qx * y - qy * x);
|
|
833
|
+
return [x + qw * tx + (qy * tz - qz * ty), y + qw * ty + (qz * tx - qx * tz), z + qw * tz + (qx * ty - qy * tx)];
|
|
834
|
+
};
|
|
835
|
+
const src = part.geometry.vertices;
|
|
836
|
+
const first = vo / VERTEX_FLOATS;
|
|
837
|
+
for (let v = 0; v < src.length; v += VERTEX_FLOATS) {
|
|
838
|
+
const p = rotate(src[v] * sx, src[v + 1] * sy, src[v + 2] * sz);
|
|
839
|
+
// Normals scale inversely (a squashed sphere's normals tilt toward its flat sides).
|
|
840
|
+
const n = rotate(src[v + 3] / sx, src[v + 4] / sy, src[v + 5] / sz);
|
|
841
|
+
const len = Math.hypot(n[0], n[1], n[2]);
|
|
842
|
+
const o = vo + v;
|
|
843
|
+
vertices[o] = p[0] + px;
|
|
844
|
+
vertices[o + 1] = p[1] + py;
|
|
845
|
+
vertices[o + 2] = p[2] + pz;
|
|
846
|
+
// Zero normals (camera-facing sprites) stay zero.
|
|
847
|
+
vertices[o + 3] = len > 0 ? n[0] / len : 0;
|
|
848
|
+
vertices[o + 4] = len > 0 ? n[1] / len : 0;
|
|
849
|
+
vertices[o + 5] = len > 0 ? n[2] / len : 0;
|
|
850
|
+
const c = part.color ?? [1, 1, 1];
|
|
851
|
+
vertices[o + 6] = src[v + 6] * c[0];
|
|
852
|
+
vertices[o + 7] = src[v + 7] * c[1];
|
|
853
|
+
vertices[o + 8] = src[v + 8] * c[2];
|
|
854
|
+
vertices[o + 9] = src[v + 9];
|
|
855
|
+
vertices[o + 10] = src[v + 10];
|
|
856
|
+
}
|
|
857
|
+
const mirrored = sx * sy * sz < 0; // turns triangles inside out: flip them back
|
|
858
|
+
const si = part.geometry.indices;
|
|
859
|
+
for (let t = 0; t < si.length; t += 3) {
|
|
860
|
+
indices[io + t] = si[t] + first;
|
|
861
|
+
indices[io + t + 1] = si[t + (mirrored ? 2 : 1)] + first;
|
|
862
|
+
indices[io + t + 2] = si[t + (mirrored ? 1 : 2)] + first;
|
|
863
|
+
}
|
|
864
|
+
vo += src.length;
|
|
865
|
+
io += si.length;
|
|
866
|
+
}
|
|
867
|
+
return {
|
|
868
|
+
vertices,
|
|
869
|
+
indices
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Registers geometry as a mesh: pass the result as an entity's `mesh` (or a burst's), with any
|
|
874
|
+
* `texture`, physics and picking. Colliders default to a box holding the shape. Mesh ids are
|
|
875
|
+
* shared by models, fonts and shapes (about 4,080) and live for the whole app. Equal geometry/options
|
|
876
|
+
* automatically reuse preparation and registration; input buffers may still be edited for a later
|
|
877
|
+
* call. Material variants remain independent. Build geometry during scene setup, outside onUpdate.
|
|
878
|
+
*
|
|
879
|
+
* ```ts
|
|
880
|
+
* const orange = createShape(lathe(orangeProfile, { color: [1, 0.55, 0.1] }), { fit: 1, shine: 0.3 });
|
|
881
|
+
* world.spawn({ mesh: orange, position: [0, 2, 0], physics: "dynamic" });
|
|
882
|
+
* ```
|
|
883
|
+
*/
|
|
884
|
+
export function createShape(geometry, options = {}) {
|
|
885
|
+
const core = native();
|
|
886
|
+
if (typeof core.shapePrepare !== 'function') throw new Error("createShape: rebuild the app with the engine's Rust asset preparation");
|
|
887
|
+
const mesh = core.shapePrepare(geometry.vertices.byteOffset === 0 && geometry.vertices.byteLength === geometry.vertices.buffer.byteLength ? geometry.vertices.buffer : geometry.vertices.slice().buffer, geometry.indices.byteOffset === 0 && geometry.indices.byteLength === geometry.indices.buffer.byteLength ? geometry.indices.buffer : geometry.indices.slice().buffer, prepareOptions(options));
|
|
888
|
+
if (mesh < 0) throw new Error(`createShape: ${core.loadError()}`);
|
|
889
|
+
return preparedModel(mesh, options.shine ?? 0);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/** Only small handle metadata stays in JS; native/WASM owns the expensive preparation cache. */
|
|
893
|
+
const preparedModels = new Map();
|
|
894
|
+
export function preparedModel(mesh, shine) {
|
|
895
|
+
const cached = preparedModels.get(mesh);
|
|
896
|
+
if (cached) return cached;
|
|
897
|
+
let geometry;
|
|
898
|
+
registerMesh(mesh, {
|
|
899
|
+
// Only advanced CPU geometry queries materialize a JS/WASM copy; drawing stays in Rust.
|
|
900
|
+
get geometry() {
|
|
901
|
+
return geometry ??= nativeGeometry(mesh);
|
|
902
|
+
},
|
|
903
|
+
shine: Math.max(0, Math.min(1, shine))
|
|
904
|
+
}, false);
|
|
905
|
+
const [x, y, z] = native().modelSize(mesh);
|
|
906
|
+
const model = {
|
|
907
|
+
id: mesh,
|
|
908
|
+
size: [x, y, z]
|
|
909
|
+
};
|
|
910
|
+
preparedModels.set(mesh, model);
|
|
911
|
+
return model;
|
|
912
|
+
}
|
|
913
|
+
export function prepareOptions(options) {
|
|
914
|
+
const opt = options.optimize === true ? {} : options.optimize || null;
|
|
915
|
+
return JSON.stringify({
|
|
916
|
+
center: options.center ?? true,
|
|
917
|
+
fit: options.fit ?? 0,
|
|
918
|
+
shine: Math.max(0, Math.min(1, options.shine ?? 0)),
|
|
919
|
+
optimize: opt && {
|
|
920
|
+
ratio: opt.ratio ?? 1,
|
|
921
|
+
error: opt.error ?? 0.001,
|
|
922
|
+
lockBorders: opt.lockBorders ?? true
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
//# sourceMappingURL=shape.js.map
|