@solidrt/3d 0.0.50 → 0.0.52
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/AGENTS.md +393 -87
- package/README.md +20 -10
- package/demos/README.md +15 -0
- package/demos/assets/icon.svg +23 -0
- package/demos/package.json +9 -0
- package/demos/src/the-third-dimension.tsx +866 -0
- package/demos/tsconfig.json +15 -0
- package/examples/README.md +34 -2
- package/examples/aim.tsx +5 -5
- package/examples/instanced.tsx +158 -0
- package/examples/lit.tsx +66 -0
- package/examples/model.glb +0 -0
- package/examples/model.tsx +51 -0
- package/examples/pick.tsx +5 -5
- package/examples/scene-background.tsx +3 -3
- package/examples/scene-basic.tsx +3 -3
- package/examples/scene-post-effect.tsx +3 -3
- package/examples/scene-views.tsx +95 -0
- package/examples/shadows.tsx +86 -0
- package/examples/sprites.tsx +95 -0
- package/examples/sweep-paths.tsx +11 -27
- package/package.json +5 -3
- package/src/components.tsx +171 -3
- package/src/geometry-gpu.ts +97 -0
- package/src/geometry.ts +413 -162
- package/src/glsl.ts +83 -3
- package/src/gltf.ts +437 -0
- package/src/index.ts +18 -11
- package/src/material.ts +373 -47
- package/src/math.ts +114 -0
- package/src/model-file.ts +122 -0
- package/src/model.ts +105 -0
- package/src/orbit.ts +13 -9
- package/src/order.ts +12 -5
- package/src/profile.ts +4 -8
- package/src/scene.ts +1270 -281
- package/src/sweep.ts +21 -36
- package/src/bvh.ts +0 -258
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// The baked model container (.srtm): ModelData as one file whose payload IS
|
|
2
|
+
// the GPU layout, so loading it is a header parse plus typed-array views -
|
|
3
|
+
// no per-vertex work. Written by tools/model.ts under bun (from parseGltf),
|
|
4
|
+
// read by loadModel on flux; both ends are this pure module.
|
|
5
|
+
//
|
|
6
|
+
// Layout, all little-endian:
|
|
7
|
+
// "SRTM" u32 | version u32 | jsonLength u32 | json (padded to 4) | payload
|
|
8
|
+
// The JSON header describes each block's byte range into the payload; every
|
|
9
|
+
// block starts 4-aligned so Float32Array/Uint32Array views sit on it
|
|
10
|
+
// directly. Images travel as their encoded files (PNG/JPEG bytes).
|
|
11
|
+
|
|
12
|
+
import type { ModelData, ModelMaterial } from "./gltf.ts"
|
|
13
|
+
import { layoutKey, layoutStride, STANDARD_FLOATS } from "./geometry.ts"
|
|
14
|
+
|
|
15
|
+
/** "SRTM" read as a little-endian u32. */
|
|
16
|
+
const MAGIC = 0x4d545253
|
|
17
|
+
const VERSION = 1
|
|
18
|
+
|
|
19
|
+
type Block = { offset: number; bytes: number }
|
|
20
|
+
|
|
21
|
+
type PartHeader = Block & {
|
|
22
|
+
name: string
|
|
23
|
+
material: number
|
|
24
|
+
layout: string
|
|
25
|
+
vertexCount: number
|
|
26
|
+
indexBits: 16 | 32
|
|
27
|
+
index: Block
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type Header = {
|
|
31
|
+
materials: ModelMaterial[]
|
|
32
|
+
images: Block[]
|
|
33
|
+
parts: PartHeader[]
|
|
34
|
+
bounds: number[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Serialize a model into the .srtm container. */
|
|
38
|
+
export function encodeModel(data: ModelData): Uint8Array {
|
|
39
|
+
let blocks: Uint8Array[] = []
|
|
40
|
+
let offset = 0
|
|
41
|
+
let push = (bytes: Uint8Array): Block => {
|
|
42
|
+
let block = { offset, bytes: bytes.byteLength }
|
|
43
|
+
blocks.push(bytes)
|
|
44
|
+
offset += bytes.byteLength
|
|
45
|
+
let pad = (4 - (offset % 4)) % 4
|
|
46
|
+
if (pad) {
|
|
47
|
+
blocks.push(new Uint8Array(pad))
|
|
48
|
+
offset += pad
|
|
49
|
+
}
|
|
50
|
+
return block
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let parts: PartHeader[] = data.parts.map((part) => {
|
|
54
|
+
let g = part.geometry
|
|
55
|
+
if (layoutStride(g.layout) !== STANDARD_FLOATS) {
|
|
56
|
+
throw new Error("encodeModel: part '" + part.name + "' has a " + layoutKey(g.layout) + " layout; only standard is written")
|
|
57
|
+
}
|
|
58
|
+
let vertices = push(new Uint8Array(g.vertices.buffer, g.vertices.byteOffset, g.vertices.byteLength))
|
|
59
|
+
let index = push(new Uint8Array(g.indices.buffer, g.indices.byteOffset, g.indices.byteLength))
|
|
60
|
+
return {
|
|
61
|
+
...vertices,
|
|
62
|
+
name: part.name,
|
|
63
|
+
material: part.material,
|
|
64
|
+
layout: "standard",
|
|
65
|
+
vertexCount: g.vertices.length / STANDARD_FLOATS,
|
|
66
|
+
indexBits: g.indices instanceof Uint32Array ? 32 : 16,
|
|
67
|
+
index,
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
let images = data.images.map((image) => push(image))
|
|
71
|
+
let header: Header = { materials: data.materials, images, parts, bounds: Array.from(data.bounds) }
|
|
72
|
+
|
|
73
|
+
let json = new TextEncoder().encode(JSON.stringify(header))
|
|
74
|
+
let jsonPadded = json.byteLength + ((4 - (json.byteLength % 4)) % 4)
|
|
75
|
+
let out = new Uint8Array(12 + jsonPadded + offset)
|
|
76
|
+
let dv = new DataView(out.buffer)
|
|
77
|
+
dv.setUint32(0, MAGIC, true)
|
|
78
|
+
dv.setUint32(4, VERSION, true)
|
|
79
|
+
dv.setUint32(8, json.byteLength, true)
|
|
80
|
+
out.set(json, 12)
|
|
81
|
+
let at = 12 + jsonPadded
|
|
82
|
+
for (let block of blocks) {
|
|
83
|
+
out.set(block, at)
|
|
84
|
+
at += block.byteLength
|
|
85
|
+
}
|
|
86
|
+
return out
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Read a .srtm container back into ModelData. The geometry arrays are
|
|
91
|
+
* VIEWS onto `bytes` (copied once only when the input is not 4-aligned),
|
|
92
|
+
* so the bytes must outlive the model.
|
|
93
|
+
*/
|
|
94
|
+
export function decodeModel(bytes: Uint8Array): ModelData {
|
|
95
|
+
if (bytes.byteOffset % 4 !== 0) bytes = new Uint8Array(bytes)
|
|
96
|
+
let buffer = bytes.buffer as ArrayBuffer
|
|
97
|
+
let base = bytes.byteOffset
|
|
98
|
+
if (bytes.byteLength < 12) throw new Error("decodeModel: not a model file (too short)")
|
|
99
|
+
let head = new DataView(buffer, base, 12)
|
|
100
|
+
if (head.getUint32(0, true) !== MAGIC) throw new Error("decodeModel: not a model file (bad magic)")
|
|
101
|
+
let version = head.getUint32(4, true)
|
|
102
|
+
if (version !== VERSION) throw new Error("decodeModel: version " + version + ", expected " + VERSION)
|
|
103
|
+
let jsonLength = head.getUint32(8, true)
|
|
104
|
+
let header: Header = JSON.parse(new TextDecoder().decode(new Uint8Array(buffer, base + 12, jsonLength)))
|
|
105
|
+
let payload = base + 12 + jsonLength + ((4 - (jsonLength % 4)) % 4)
|
|
106
|
+
|
|
107
|
+
let parts = header.parts.map((part) => {
|
|
108
|
+
if (part.layout !== "standard") throw new Error("decodeModel: part '" + part.name + "' has an unsupported layout " + part.layout)
|
|
109
|
+
let indexCount = part.index.bytes / (part.indexBits / 8)
|
|
110
|
+
return {
|
|
111
|
+
name: part.name,
|
|
112
|
+
material: part.material,
|
|
113
|
+
geometry: {
|
|
114
|
+
vertices: new Float32Array(buffer, payload + part.offset, part.vertexCount * STANDARD_FLOATS),
|
|
115
|
+
indices: part.indexBits === 32 ? new Uint32Array(buffer, payload + part.index.offset, indexCount) : new Uint16Array(buffer, payload + part.index.offset, indexCount),
|
|
116
|
+
label: part.name,
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
let images = header.images.map((block) => new Uint8Array(buffer, payload + block.offset, block.bytes))
|
|
121
|
+
return { parts, materials: header.materials, images, bounds: Float32Array.from(header.bounds) }
|
|
122
|
+
}
|
package/src/model.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Models in a scene: ModelData (parsed glTF or a decoded .srtm) becomes a
|
|
2
|
+
// Group of meshes, one per part, with the images uploaded as textures and
|
|
3
|
+
// a material per glTF material - Three's `gltf.scene`, an object you add
|
|
4
|
+
// to the scene and place with setTransform. The model owns what it
|
|
5
|
+
// created (geometry buffers, textures): dispose() frees them and detaches
|
|
6
|
+
// the group. loadGltf / loadModel are the read-then-create conveniences
|
|
7
|
+
// over flux:fs; parseGltf / decodeModel + createModel are the primitives
|
|
8
|
+
// under them, for bytes obtained any other way (a binary import, a fetch).
|
|
9
|
+
|
|
10
|
+
import { file } from "flux:fs"
|
|
11
|
+
import { decodeImage } from "@solidrt/core"
|
|
12
|
+
import { createTexture, destroyTexture } from "@solidrt/core/gpu"
|
|
13
|
+
import type { TextureId } from "@solidrt/core/gpu"
|
|
14
|
+
import { gltfExternalUris, isGlb, parseGltf } from "./gltf.ts"
|
|
15
|
+
import type { ModelData, ModelMaterial } from "./gltf.ts"
|
|
16
|
+
import { decodeModel } from "./model-file.ts"
|
|
17
|
+
import { disposeGeometry } from "./geometry-gpu.ts"
|
|
18
|
+
import { lit } from "./material.ts"
|
|
19
|
+
import type { Material } from "./material.ts"
|
|
20
|
+
import { add, createGroup, createMesh, remove } from "./scene.ts"
|
|
21
|
+
import type { Mesh, SceneNode } from "./scene.ts"
|
|
22
|
+
|
|
23
|
+
export type ModelOptions = {
|
|
24
|
+
/** The material for each glTF material (default: `lit` with its color,
|
|
25
|
+
* map and transparency). `map` is the uploaded base color texture, or
|
|
26
|
+
* null. Called once per material, shared by every part using it. */
|
|
27
|
+
material?: (material: ModelMaterial, map: TextureId | null) => Material
|
|
28
|
+
/** Debug name for the textures. */
|
|
29
|
+
label?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A model in the scene: a Group whose children are the parts' meshes. */
|
|
33
|
+
export type Model = SceneNode & {
|
|
34
|
+
kind: "group"
|
|
35
|
+
/** The parts by name, in file order; each `mesh` is a child of the model. */
|
|
36
|
+
parts: { name: string; mesh: Mesh }[]
|
|
37
|
+
/** One per glTF material, in file order. */
|
|
38
|
+
materials: Material[]
|
|
39
|
+
/** Local [minX, minY, minZ, maxX, maxY, maxZ] over every part. */
|
|
40
|
+
bounds: Float32Array
|
|
41
|
+
/** Detach the model and free its geometry buffers and textures. */
|
|
42
|
+
dispose(): void
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build the scene object for parsed model data: upload its images (repeat
|
|
47
|
+
* wrap, mipmapped), make a material per glTF material, a mesh per part,
|
|
48
|
+
* all under one Group. Synchronous - the data is already in memory.
|
|
49
|
+
*/
|
|
50
|
+
export function createModel(data: ModelData, opts: ModelOptions = {}): Model {
|
|
51
|
+
let label = opts.label
|
|
52
|
+
let textures: TextureId[] = data.images.map((bytes, i) => {
|
|
53
|
+
let image = decodeImage(bytes)
|
|
54
|
+
return createTexture(image.data, image.width, image.height, {
|
|
55
|
+
wrap: "repeat",
|
|
56
|
+
mipmap: true,
|
|
57
|
+
autoFree: false,
|
|
58
|
+
label: label ? label + "-image" + i : undefined,
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
let make = opts.material ?? ((m: ModelMaterial, map: TextureId | null): Material => lit({ color: m.color, map: map ?? undefined, transparent: m.transparent }))
|
|
62
|
+
let materials = data.materials.map((m) => make(m, m.map === null ? null : textures[m.map]!))
|
|
63
|
+
|
|
64
|
+
let model = createGroup() as Model
|
|
65
|
+
model.parts = data.parts.map((part) => {
|
|
66
|
+
let material = materials[part.material]
|
|
67
|
+
if (material === undefined) throw new Error("createModel: part '" + part.name + "' names a missing material " + part.material)
|
|
68
|
+
let mesh = createMesh(part.geometry, material)
|
|
69
|
+
add(model, mesh)
|
|
70
|
+
return { name: part.name, mesh }
|
|
71
|
+
})
|
|
72
|
+
model.materials = materials
|
|
73
|
+
model.bounds = data.bounds
|
|
74
|
+
model.dispose = () => {
|
|
75
|
+
if (model.parent !== null) remove(model)
|
|
76
|
+
for (let part of model.parts) disposeGeometry(part.mesh.geometry)
|
|
77
|
+
for (let id of textures) destroyTexture(id)
|
|
78
|
+
textures.length = 0
|
|
79
|
+
}
|
|
80
|
+
return model
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Read a .glb or .gltf (with its external .bin and image files, resolved
|
|
85
|
+
* next to it) and build the model. The parse runs on the runtime - fine
|
|
86
|
+
* for models of tens of thousands of vertices; bake bigger ones with
|
|
87
|
+
* `srt tool 3d/model` and use loadModel.
|
|
88
|
+
*/
|
|
89
|
+
export async function loadGltf(path: string, opts?: ModelOptions): Promise<Model> {
|
|
90
|
+
let bytes = await file(path).bytes()
|
|
91
|
+
let files = new Map<string, Uint8Array>()
|
|
92
|
+
if (!isGlb(bytes)) {
|
|
93
|
+
let dir = path.slice(0, path.lastIndexOf("/") + 1)
|
|
94
|
+
for (let uri of gltfExternalUris(bytes)) {
|
|
95
|
+
if (!files.has(uri)) files.set(uri, await file(dir + decodeURIComponent(uri)).bytes())
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return createModel(parseGltf(bytes, (uri) => files.get(uri)!), opts)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Read a baked .srtm model (`srt tool 3d/model`) and build it: no parsing,
|
|
102
|
+
* the geometry views the file's bytes directly. */
|
|
103
|
+
export async function loadModel(path: string, opts?: ModelOptions): Promise<Model> {
|
|
104
|
+
return createModel(decodeModel(await file(path).bytes()), opts)
|
|
105
|
+
}
|
package/src/orbit.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// about that point instead - the spot under the fingers stays under the
|
|
23
23
|
// fingers, with the target sliding toward it. Only the app can own that
|
|
24
24
|
// mapping: screen-to-ray needs the projection and the element's placement
|
|
25
|
-
// (fov, aspect,
|
|
25
|
+
// (fov, aspect, designSize scaling), which live in the app's camera and layout,
|
|
26
26
|
// not here.
|
|
27
27
|
//
|
|
28
28
|
// Anchored zoom leaves the target wherever the zoom carried it - possibly a
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
import { createSignal } from "@solidjs/signals"
|
|
49
49
|
import { createTransform } from "@solidrt/core"
|
|
50
50
|
import type { PointerEvent } from "@solidrt/core"
|
|
51
|
-
import type {
|
|
51
|
+
import type { CameraUpdate } from "./scene.ts"
|
|
52
52
|
import type { Vec3 } from "./math.ts"
|
|
53
53
|
|
|
54
54
|
// Baseline sensitivities at rotateSpeed/zoomSpeed 1, in radians per dragged
|
|
@@ -57,6 +57,9 @@ const DRAG_AZIMUTH = 0.008
|
|
|
57
57
|
const DRAG_ELEVATION = 0.006
|
|
58
58
|
const WHEEL_ZOOM = 0.0015
|
|
59
59
|
|
|
60
|
+
/** What an orbit camera drives: a Scene, or one of its Views. */
|
|
61
|
+
export type OrbitTarget = { setCamera(update: CameraUpdate): void }
|
|
62
|
+
|
|
60
63
|
export type OrbitCameraOptions = {
|
|
61
64
|
/** The point the camera orbits and looks at (default origin). */
|
|
62
65
|
target?: Vec3
|
|
@@ -123,7 +126,7 @@ export type OrbitCamera = {
|
|
|
123
126
|
/** Whether the auto-orbit is running. Reactive (signal-backed), so HUD
|
|
124
127
|
* text can read it. */
|
|
125
128
|
orbiting(): boolean
|
|
126
|
-
/** Advance the auto-orbit and push any pose change to the
|
|
129
|
+
/** Advance the auto-orbit and push any pose change to the driven camera.
|
|
127
130
|
* Call from onFrame with the frame's dt in seconds; returns whether the
|
|
128
131
|
* pose changed. */
|
|
129
132
|
update(dt: number): boolean
|
|
@@ -142,12 +145,13 @@ export type OrbitCamera = {
|
|
|
142
145
|
let clampNum = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v))
|
|
143
146
|
|
|
144
147
|
/**
|
|
145
|
-
* Create an orbit camera driving `
|
|
146
|
-
* near, and far stay yours via
|
|
147
|
-
* immediately. In a component tree,
|
|
148
|
-
*
|
|
148
|
+
* Create an orbit camera driving `camera`'s position and target, where
|
|
149
|
+
* `camera` is a Scene or one of its Views (fov, near, and far stay yours via
|
|
150
|
+
* its setCamera). The initial pose applies immediately. In a component tree,
|
|
151
|
+
* reach the scene via `<Scene ref>` or useScene() (a view via `<View ref>`)
|
|
152
|
+
* and hand the handlers to whichever element owns input.
|
|
149
153
|
*/
|
|
150
|
-
export function createOrbitCamera(
|
|
154
|
+
export function createOrbitCamera(camera: OrbitTarget, options: OrbitCameraOptions = {}): OrbitCamera {
|
|
151
155
|
let target: Vec3 = options.target ? [options.target[0], options.target[1], options.target[2]] : [0, 0, 0]
|
|
152
156
|
let azimuth = options.azimuth ?? 0
|
|
153
157
|
let elevation = options.elevation ?? 0
|
|
@@ -197,7 +201,7 @@ export function createOrbitCamera(scene: Scene, options: OrbitCameraOptions = {}
|
|
|
197
201
|
target[2] + distance * ce * Math.cos(azimuth),
|
|
198
202
|
]
|
|
199
203
|
}
|
|
200
|
-
let apply = () =>
|
|
204
|
+
let apply = () => camera.setCamera({ position: eye(), target })
|
|
201
205
|
|
|
202
206
|
// Zoom by `ratio` (new distance over old, before clamping) about a world
|
|
203
207
|
// anchor (null zooms toward the target). Scaling eye and target about the
|
package/src/order.ts
CHANGED
|
@@ -22,13 +22,20 @@ export type Orderable<T> = {
|
|
|
22
22
|
* center. The center, not the origin (Three's key), so geometry built
|
|
23
23
|
* off-origin sorts by where it is; and not the nearest bounds point, which
|
|
24
24
|
* would draw a large translucent ground plane over the small translucents
|
|
25
|
-
* resting on it. Per-mesh only: no per-triangle sort, no OIT.
|
|
25
|
+
* resting on it. Per-mesh only: no per-triangle sort, no OIT. `entry`
|
|
26
|
+
* picks the id ordered for each mesh (default its own `_entry`; a view
|
|
27
|
+
* passes its per-mesh entries in its target).
|
|
26
28
|
*/
|
|
27
|
-
export function orderEntries<T>(
|
|
29
|
+
export function orderEntries<T>(
|
|
30
|
+
meshes: readonly Orderable<T>[],
|
|
31
|
+
view: Mat4,
|
|
32
|
+
first?: T,
|
|
33
|
+
entry: (m: Orderable<T>) => T | null = m => m._entry,
|
|
34
|
+
): T[] {
|
|
28
35
|
let opaque: Orderable<T>[] = []
|
|
29
36
|
let transparent: Orderable<T>[] = []
|
|
30
37
|
for (let m of meshes) {
|
|
31
|
-
if (m
|
|
38
|
+
if (entry(m) === null) continue
|
|
32
39
|
;(m._transparent ? transparent : opaque).push(m)
|
|
33
40
|
}
|
|
34
41
|
// Array sort is stable, so equal keys keep add order.
|
|
@@ -45,7 +52,7 @@ export function orderEntries<T>(meshes: readonly Orderable<T>[], view: Mat4, fir
|
|
|
45
52
|
}
|
|
46
53
|
let order: T[] = []
|
|
47
54
|
if (first !== undefined) order.push(first)
|
|
48
|
-
for (let m of opaque) order.push(m
|
|
49
|
-
for (let m of transparent) order.push(m
|
|
55
|
+
for (let m of opaque) order.push(entry(m)!)
|
|
56
|
+
for (let m of transparent) order.push(entry(m)!)
|
|
50
57
|
return order
|
|
51
58
|
}
|
package/src/profile.ts
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
// for custom flat work. The swept-solid generators consuming this
|
|
9
9
|
// vocabulary - extrude, lathe, sweep, tube - live in sweep.ts.
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
import type { Geometry } from "./geometry.ts"
|
|
11
|
+
import { packGeometry } from "./geometry.ts"
|
|
12
|
+
import type { Geometry, GeometryOptions } from "./geometry.ts"
|
|
13
13
|
import type { Vec2 } from "./math.ts"
|
|
14
14
|
|
|
15
15
|
/** A profile point: `p` in profile space, `smooth` to share an averaged
|
|
@@ -298,7 +298,7 @@ export function roundRect(
|
|
|
298
298
|
* profile's bounding box to the unit square like plane(); rotate flat the
|
|
299
299
|
* same way: `rotation={[-Math.PI / 2, 0, 0]}`.
|
|
300
300
|
*/
|
|
301
|
-
export function shape(profile: Profile,
|
|
301
|
+
export function shape(profile: Profile, options: GeometryOptions = {}): Geometry {
|
|
302
302
|
let pts = normalizeProfile(profile)
|
|
303
303
|
let { minX, maxY, w, h } = profileBounds(pts)
|
|
304
304
|
let px = pts.map((p) => p.x)
|
|
@@ -307,9 +307,5 @@ export function shape(profile: Profile, label?: string): Geometry {
|
|
|
307
307
|
for (let p of pts) {
|
|
308
308
|
verts.push(p.x, p.y, 0, 0, 0, 1, (p.x - minX) / w, (maxY - p.y) / h)
|
|
309
309
|
}
|
|
310
|
-
return
|
|
311
|
-
vertices: new Float32Array(verts),
|
|
312
|
-
indices: packIndices(earClip(px, py), pts.length),
|
|
313
|
-
label,
|
|
314
|
-
}
|
|
310
|
+
return packGeometry(verts, earClip(px, py), options)
|
|
315
311
|
}
|