镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Commit 6c86fe9

Browse files
committed
feat(browser): support indexed inline model geometry
1 parent 5145690 commit 6c86fe9

3 files changed

Lines changed: 82 additions & 1 deletion

File tree

‎storage/framework/core/browser/README.md‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,37 @@
11
# Stacks Frontend
22

3-
This core package contains the core frontend/browser functionalities of Stacks, all of which are auto-imported into your projects.
3+
This package contains Stacks browser APIs. Import module APIs explicitly; the STX runtime and compiler determine which composables are available without imports.
4+
5+
## Native model geometry
6+
7+
`@stacksjs/browser/model-viewer` supports native box, tube and cylinder shapes,
8+
same-origin binary STL files, and indexed inline meshes for procedural models.
9+
Pass the scene to the STX `ModelViewer` component.
10+
11+
```ts
12+
import type { ModelScene } from '@stacksjs/browser/model-viewer'
13+
14+
const scene: ModelScene = {
15+
nodes: [{
16+
id: 'panel',
17+
geometry: {
18+
kind: 'mesh',
19+
vertices: [[0, 0, 0], [100, 0, 0], [0, 50, 0]],
20+
triangles: [[0, 1, 2]],
21+
},
22+
color: '#254b38',
23+
}],
24+
}
25+
```
26+
27+
Coordinates follow the same authored units as the rest of the scene. Vertices
28+
and triangle indices are JSON-compatible triples. The viewer validates array
29+
sizes, finite Float32 coordinates, integer in-range indices and repeated
30+
indices. Each mesh permits up to 500,000 vertices and 500,000 triangles, subject
31+
to the existing whole-scene detail limit. Input arrays are copied, not mutated.
32+
Inline meshes require no file request or persistent STL cache entry. The STL
33+
loader retains its same-origin requirement; inline meshes do not enable remote
34+
URLs. Rendering geometry does not establish its physical safety or suitability.
435

536
## ☘️ Features
637

‎storage/framework/core/browser/src/model-viewer.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export type ModelVector = [number, number, number]
33
export type ModelMatrix = number[]
44
export type ModelGeometry =
55
| { kind: 'stl', src: string }
6+
| { kind: 'mesh', vertices: ModelVector[], triangles: [number, number, number][] }
67
| { kind: 'box', size: ModelVector }
78
| { kind: 'tube', outer: number, inner: number, length: number }
89
| { kind: 'cylinder', radius: number, length: number, innerRadius?: number, segments?: number }
@@ -79,6 +80,27 @@ export function parseBinaryModelStl(buffer: ArrayBuffer): Float32Array {
7980
}
8081

8182
export function createModelGeometry(geometry: Exclude<ModelGeometry, { kind: 'stl' }>): Float32Array {
83+
// Indexed geometry is authored in the scene, so it needs no URL, fetch or
84+
// retained STL resource. This keeps interactive procedural models native.
85+
if (geometry.kind === 'mesh') {
86+
if (!Array.isArray(geometry.vertices) || geometry.vertices.length < 3 || geometry.vertices.length > 500000
87+
|| !Array.isArray(geometry.triangles) || !geometry.triangles.length || geometry.triangles.length > 500000)
88+
throw new Error('An inline mesh requires 3-500000 vertices and 1-500000 triangles.')
89+
for (const vertex of geometry.vertices) {
90+
if (!Array.isArray(vertex) || vertex.length !== 3 || ![0, 1, 2].every(axis => Number.isFinite(vertex[axis]) && Number.isFinite(Math.fround(vertex[axis]!))))
91+
throw new Error('An inline mesh contains an invalid coordinate.')
92+
}
93+
const output = new Float32Array(geometry.triangles.length * 9)
94+
for (let face = 0; face < geometry.triangles.length; face++) {
95+
const indices = geometry.triangles[face]!
96+
if (!Array.isArray(indices) || indices.length !== 3 || ![0, 1, 2].every(corner => Number.isInteger(indices[corner]) && indices[corner]! >= 0 && indices[corner]! < geometry.vertices.length)
97+
|| new Set(indices).size !== 3)
98+
throw new Error('An inline mesh contains an invalid triangle index.')
99+
for (let corner = 0; corner < 3; corner++)
100+
output.set(geometry.vertices[indices[corner]!]!, face * 9 + corner * 3)
101+
}
102+
return output
103+
}
82104
const positions: number[] = []
83105
const triangle = (a: ModelVector, b: ModelVector, c: ModelVector) => positions.push(...a, ...b, ...c)
84106
const quad = (a: ModelVector, b: ModelVector, c: ModelVector, d: ModelVector) => { triangle(a, b, c); triangle(a, c, d) }

‎storage/framework/core/browser/tests/model-viewer.test.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,33 @@
11
import { expect, test } from 'bun:test'
22
import { createModelGeometry, modelTransform, multiplyModelMatrices, parseBinaryModelStl, transformModelPoint } from '../src/model-viewer'
3+
import type { ModelGeometry } from '../src/model-viewer'
4+
5+
test('indexed inline meshes preserve winding and repeated shared vertices without a fetch', () => {
6+
const geometry: ModelGeometry = { kind: 'mesh', vertices: [[0, 0, 0], [10, 0, 0], [10, 20, 0], [0, 20, 0]], triangles: [[0, 1, 2], [0, 2, 3]] }
7+
const output = createModelGeometry(geometry)
8+
expect([...output]).toEqual([0, 0, 0, 10, 0, 0, 10, 20, 0, 0, 0, 0, 10, 20, 0, 0, 20, 0])
9+
output[0] = 99
10+
expect(geometry.vertices[0]).toEqual([0, 0, 0])
11+
})
12+
13+
test('inline meshes reject malformed, nonfinite, Float32-overflowing and oversized input before delivery', () => {
14+
const valid: Extract<ModelGeometry, { kind: 'mesh' }> = { kind: 'mesh', vertices: [[0, 0, 0], [1, 0, 0], [0, 1, 0]], triangles: [[0, 1, 2]] }
15+
const invalid = [
16+
{ ...valid, vertices: [] }, { ...valid, triangles: [] },
17+
{ ...valid, vertices: [[Number.NaN, 0, 0], [1, 0, 0], [0, 1, 0]] },
18+
{ ...valid, vertices: [[1e100, 0, 0], [1, 0, 0], [0, 1, 0]] },
19+
{ ...valid, vertices: [[0, 0], [1, 0, 0], [0, 1, 0]] },
20+
{ ...valid, vertices: [[0, , 0], [1, 0, 0], [0, 1, 0]] },
21+
{ ...valid, triangles: [[0, 1, 3]] }, { ...valid, triangles: [[0, -1, 2]] },
22+
{ ...valid, triangles: [[0, 1.5, 2]] }, { ...valid, triangles: [[0, 0, 2]] },
23+
{ ...valid, triangles: [[0, 1]] },
24+
{ ...valid, triangles: [[0, , 2]] },
25+
{ ...valid, vertices: Array.from({ length: 500001 }, () => [0, 0, 0]) },
26+
{ ...valid, triangles: Array.from({ length: 500001 }, () => [0, 1, 2]) },
27+
]
28+
for (const input of invalid)
29+
expect(() => createModelGeometry(input as typeof valid)).toThrow()
30+
})
331

432
test('model transforms preserve CAD millimeters and compose local rotations before world placement', () => {
533
const point = transformModelPoint([70, 155, 6], modelTransform([0, 27, 200], [90, 0, 0]))

0 commit comments

Comments
 (0)