mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-06-27 16:35:57 +00:00
add model viewer for .glb
(GLTF) model in file view (#8111)
## Motivation The GLTF (`.gltf`, `.glb`) 3D model format is very popular for game development and visual productions. For an indie game studio, it would be convenient for a team to view textured 3D models directly from the Forgejo interface (otherwise they need to be downloaded and opened). [Perforce](https://www.perforce.com/products/helix-dam), [Diversion](https://www.diversion.dev/), and GitHub all have this capability to differing extents. Some discussion on 3D file support here: https://codeberg.org/forgejo/forgejo/issues/5188 ## Changes Adds a model viewer similar to [GitHub STL viewer](https://github.com/assimp/assimp/blob/master/test/models/STL/Spider_ascii.stl) for `.glb` model files, and lays some groundwork to support future files. Uses the [model-viewer](https://modelviewer.dev/) library by Google and three.js. The model viewer is interactive and can be rotated and scaled.  ## How to Test 1) Create a new repository or use an existing one. 2) Upload a `.glb` file such as `tests/testdata/data/viewer/Unicode❤♻Test.glb` (CC0 1.0 Universal) 3) View the file in the repository. - Similar to image files, the 3D model should be rendered in a viewer. - Use mouse clicks to turn and zoom. ## Licenses Libraries used for this change include three.js and @google/model-viewer, which are MIT and Apache-2.0 licenses respectively. Both of these are compatible with Forgejo's GPL3.0 license. ## Future Plans 1) `.gltf` was not attempted because it is a multiple file format, referencing other files in the same directory. Still need to experiment with this to see if it can work. `.glb` is a single file containing a `.gltf` and all of its other file/texture dependencies so was easier to implement. 2) The PR diff still shows the model as an unviewable bin file, but clicking the "View File" button takes you to a view screen where this model viewer is used. It would be nice to view the before and after of the model in two side-by-side model viewers, akin to reviewing a change in an image. 3) Also inserted stubs for adding contexts for GLTF, STL, OBJ, and 3MF. These ultimately don't do anything yet as only `.glb` files can be detected by the type sniffer of all of these. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests - I added test coverage for checking GLB file content using the first few bytes. - [x] in their respective `typesniffer_test.go` for unit tests. ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] I do not want this change to show in the release notes. - [ ] I want the title to show in the release notes with a link to this pull request. - [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title. <!--start release-notes-assistant--> ## Release notes <!--URL:https://codeberg.org/forgejo/forgejo--> - User Interface features - [PR](https://codeberg.org/forgejo/forgejo/pulls/8111): <!--number 8111 --><!--line 0 --><!--description YWRkIG1vZGVsIHZpZXdlciBmb3IgYC5nbGJgIChHTFRGKSBtb2RlbCBpbiBmaWxlIHZpZXc=-->add model viewer for `.glb` (GLTF) model in file view<!--description--> <!--end release-notes-assistant--> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8111 Reviewed-by: oliverpool <oliverpool@noreply.codeberg.org> Co-authored-by: Alex Smith <amsmith.pro@pm.me> Co-committed-by: Alex Smith <amsmith.pro@pm.me>
This commit is contained in:
parent
b2c4fc9f94
commit
690532efb8
14 changed files with 256 additions and 4 deletions
|
@ -23,6 +23,11 @@ var wellKnownMimeTypesLower = map[string]string{
|
|||
".wasm": "application/wasm",
|
||||
".webp": "image/webp",
|
||||
".xml": "text/xml; charset=utf-8",
|
||||
".glb": "model/gltf-binary",
|
||||
".gltf": "model/gltf+json",
|
||||
".obj": "model/obj",
|
||||
".stl": "model/stl",
|
||||
".3mf": "model/3mf",
|
||||
|
||||
// well, there are some types missing from the builtin list
|
||||
".txt": "text/plain; charset=utf-8",
|
||||
|
|
|
@ -24,6 +24,16 @@ const (
|
|||
AvifMimeType = "image/avif"
|
||||
// ApplicationOctetStream MIME type of binary files.
|
||||
ApplicationOctetStream = "application/octet-stream"
|
||||
// GLTFMimeType MIME type of GLTF files.
|
||||
GLTFMimeType = "model/gltf+json"
|
||||
// GLBMimeType MIME type of GLB files.
|
||||
GLBMimeType = "model/gltf-binary"
|
||||
// OBJMimeType MIME type of OBJ files.
|
||||
OBJMimeType = "model/obj"
|
||||
// STLMimeType MIME type of STL files.
|
||||
STLMimeType = "model/stl"
|
||||
// 3MFMimeType MIME type of 3MF files.
|
||||
ThreeMFMimeType = "model/3mf"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -67,6 +77,36 @@ func (ct SniffedType) IsAudio() bool {
|
|||
return strings.Contains(ct.contentType, "audio/")
|
||||
}
|
||||
|
||||
// Is3DModel detects if data is a 3D format
|
||||
func (ct SniffedType) Is3DModel() bool {
|
||||
return strings.Contains(ct.contentType, "model/")
|
||||
}
|
||||
|
||||
// IsGLTFFile detects if data is an SVG image format
|
||||
func (ct SniffedType) IsGLTF() bool {
|
||||
return strings.Contains(ct.contentType, GLTFMimeType)
|
||||
}
|
||||
|
||||
// IsGLBFile detects if data is an GLB image format
|
||||
func (ct SniffedType) IsGLB() bool {
|
||||
return strings.Contains(ct.contentType, GLBMimeType)
|
||||
}
|
||||
|
||||
// IsOBJFile detects if data is an OBJ image format
|
||||
func (ct SniffedType) IsOBJ() bool {
|
||||
return strings.Contains(ct.contentType, OBJMimeType)
|
||||
}
|
||||
|
||||
// IsSTLTextFile detects if data is an STL text format
|
||||
func (ct SniffedType) IsSTL() bool {
|
||||
return strings.Contains(ct.contentType, STLMimeType)
|
||||
}
|
||||
|
||||
// Is3MFFile detects if data is an 3MF image format
|
||||
func (ct SniffedType) Is3MF() bool {
|
||||
return strings.Contains(ct.contentType, ThreeMFMimeType)
|
||||
}
|
||||
|
||||
// IsRepresentableAsText returns true if file content can be represented as
|
||||
// plain text or is empty.
|
||||
func (ct SniffedType) IsRepresentableAsText() bool {
|
||||
|
@ -75,7 +115,7 @@ func (ct SniffedType) IsRepresentableAsText() bool {
|
|||
|
||||
// IsBrowsableBinaryType returns whether a non-text type can be displayed in a browser
|
||||
func (ct SniffedType) IsBrowsableBinaryType() bool {
|
||||
return ct.IsImage() || ct.IsSvgImage() || ct.IsPDF() || ct.IsVideo() || ct.IsAudio()
|
||||
return ct.IsImage() || ct.IsSvgImage() || ct.IsPDF() || ct.IsVideo() || ct.IsAudio() || ct.Is3DModel()
|
||||
}
|
||||
|
||||
// GetMimeType returns the mime type
|
||||
|
@ -135,6 +175,13 @@ func DetectContentType(data []byte) SniffedType {
|
|||
ct = "audio/ogg" // for most cases, it is used as an audio container
|
||||
}
|
||||
}
|
||||
|
||||
// GLTF is unsupported by http.DetectContentType
|
||||
// hexdump -n 4 -C glTF.glb
|
||||
if bytes.HasPrefix(data, []byte("glTF")) {
|
||||
ct = GLBMimeType
|
||||
}
|
||||
|
||||
return SniffedType{ct}
|
||||
}
|
||||
|
||||
|
|
|
@ -117,6 +117,14 @@ func TestIsAudio(t *testing.T) {
|
|||
assert.True(t, DetectContentType([]byte("ID3Toy\n====\t* hi 🌞, ..."+"🌛"[0:2])).IsText()) // test ID3 tag with incomplete UTF8 char
|
||||
}
|
||||
|
||||
func TestIsGLB(t *testing.T) {
|
||||
glb, _ := hex.DecodeString("676c5446")
|
||||
assert.True(t, DetectContentType(glb).IsGLB())
|
||||
assert.True(t, DetectContentType(glb).Is3DModel())
|
||||
assert.False(t, DetectContentType([]byte("plain text")).IsGLB())
|
||||
assert.False(t, DetectContentType([]byte("plain text")).Is3DModel())
|
||||
}
|
||||
|
||||
func TestDetectContentTypeFromReader(t *testing.T) {
|
||||
mp3, _ := base64.StdEncoding.DecodeString("SUQzBAAAAAABAFRYWFgAAAASAAADbWFqb3JfYnJhbmQAbXA0MgBUWFhYAAAAEQAAA21pbm9yX3Zl")
|
||||
st, err := DetectContentTypeFromReader(bytes.NewReader(mp3))
|
||||
|
@ -145,3 +153,15 @@ func TestDetectContentTypeAvif(t *testing.T) {
|
|||
|
||||
assert.True(t, st.IsImage())
|
||||
}
|
||||
|
||||
func TestDetectContentTypeModelGLB(t *testing.T) {
|
||||
glb, err := hex.DecodeString("676c5446")
|
||||
require.NoError(t, err)
|
||||
|
||||
st, err := DetectContentTypeFromReader(bytes.NewReader(glb))
|
||||
require.NoError(t, err)
|
||||
|
||||
// print st for debugging
|
||||
assert.Equal(t, "model/gltf-binary", st.GetMimeType())
|
||||
assert.True(t, st.IsGLB())
|
||||
}
|
||||
|
|
116
package-lock.json
generated
116
package-lock.json
generated
|
@ -12,6 +12,7 @@
|
|||
"@github/markdown-toolbar-element": "2.2.3",
|
||||
"@github/quote-selection": "2.1.0",
|
||||
"@github/text-expander-element": "2.8.0",
|
||||
"@google/model-viewer": "4.1.0",
|
||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||
"@primer/octicons": "19.14.0",
|
||||
"ansi_up": "6.0.5",
|
||||
|
@ -1222,6 +1223,22 @@
|
|||
"dom-input-range": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google/model-viewer": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@google/model-viewer/-/model-viewer-4.1.0.tgz",
|
||||
"integrity": "sha512-7WB/jS6wfBfRl/tWhsUUvDMKFE1KlKME97coDLlZQfvJD0nCwjhES1lJ+k7wnmf7T3zMvCfn9mIjM/mgZapuig==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@monogrid/gainmap-js": "^3.1.0",
|
||||
"lit": "^3.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": "^0.172.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
|
@ -2004,6 +2021,21 @@
|
|||
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lit-labs/ssr-dom-shim": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz",
|
||||
"integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@lit/reactive-element": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.0.tgz",
|
||||
"integrity": "sha512-L2qyoZSQClcBmq0qajBVbhYEcG6iK0XfLn66ifLe/RfC0/ihpc+pl0Wdn8bJ8o+hj38cG0fGXRgSS20MuXn7qA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mcaptcha/core-glue": {
|
||||
"version": "0.1.0-alpha-5",
|
||||
"resolved": "https://registry.npmjs.org/@mcaptcha/core-glue/-/core-glue-0.1.0-alpha-5.tgz",
|
||||
|
@ -2064,6 +2096,18 @@
|
|||
"langium": "3.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@monogrid/gainmap-js": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.1.0.tgz",
|
||||
"integrity": "sha512-Obb0/gEd/HReTlg8ttaYk+0m62gQJmCblMOjHSMHRrBP2zdfKMHLCRbh/6ex9fSUJMKdjjIEiohwkbGD3wj2Nw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"promise-worker-transferable": "^1.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">= 0.159.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz",
|
||||
|
@ -3493,8 +3537,7 @@
|
|||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "2.0.11",
|
||||
|
@ -8768,6 +8811,12 @@
|
|||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "9.0.21",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
|
||||
|
@ -9307,6 +9356,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
|
||||
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-proto-prop": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-proto-prop/-/is-proto-prop-3.0.1.tgz",
|
||||
|
@ -10023,6 +10078,15 @@
|
|||
"npm": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lilconfig": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||
|
@ -10051,6 +10115,37 @@
|
|||
"uc.micro": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lit": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz",
|
||||
"integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^2.1.0",
|
||||
"lit-element": "^4.2.0",
|
||||
"lit-html": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lit-element": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.0.tgz",
|
||||
"integrity": "sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.2.0",
|
||||
"@lit/reactive-element": "^2.1.0",
|
||||
"lit-html": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lit-html": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.0.tgz",
|
||||
"integrity": "sha512-RHoswrFAxY2d8Cf2mm4OZ1DgzCoBKUKSPvA1fhtSELxUERq2aQQ2h05pO9j81gS1o7RIRJ+CePLogfyahwmynw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/loader-runner": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
|
||||
|
@ -12363,6 +12458,16 @@
|
|||
"dev": true,
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/promise-worker-transferable": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz",
|
||||
"integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"is-promise": "^2.1.0",
|
||||
"lie": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/proto-list": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
||||
|
@ -14425,6 +14530,13 @@
|
|||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.172.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.172.0.tgz",
|
||||
"integrity": "sha512-6HMgMlzU97MsV7D/tY8Va38b83kz8YJX+BefKjspMNAv0Vx6dxMogHOrnRl/sbMIs3BPUKijPqDqJ/+UwJbIow==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/throttle-debounce": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz",
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"@github/markdown-toolbar-element": "2.2.3",
|
||||
"@github/quote-selection": "2.1.0",
|
||||
"@github/text-expander-element": "2.8.0",
|
||||
"@google/model-viewer": "4.1.0",
|
||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||
"@primer/octicons": "19.14.0",
|
||||
"ansi_up": "6.0.5",
|
||||
|
@ -78,8 +79,8 @@
|
|||
"eslint-plugin-playwright": "2.2.0",
|
||||
"eslint-plugin-regexp": "2.9.0",
|
||||
"eslint-plugin-sonarjs": "3.0.2",
|
||||
"eslint-plugin-unicorn": "59.0.1",
|
||||
"eslint-plugin-toml": "0.12.0",
|
||||
"eslint-plugin-unicorn": "59.0.1",
|
||||
"eslint-plugin-vitest-globals": "1.5.0",
|
||||
"eslint-plugin-vue": "10.2.0",
|
||||
"eslint-plugin-vue-scoped-css": "2.10.0",
|
||||
|
|
|
@ -342,6 +342,20 @@ func LFSFileGet(ctx *context.Context) {
|
|||
ctx.Data["IsVideoFile"] = true
|
||||
case st.IsAudio():
|
||||
ctx.Data["IsAudioFile"] = true
|
||||
case st.Is3DModel():
|
||||
ctx.Data["Is3DModelFile"] = true
|
||||
switch {
|
||||
case st.IsGLB():
|
||||
ctx.Data["IsGLBFile"] = true
|
||||
case st.IsSTL():
|
||||
ctx.Data["IsSTLFile"] = true
|
||||
case st.IsGLTF():
|
||||
ctx.Data["IsGLTFFile"] = true
|
||||
case st.IsOBJ():
|
||||
ctx.Data["IsOBJFile"] = true
|
||||
case st.Is3MF():
|
||||
ctx.Data["Is3MFFile"] = true
|
||||
}
|
||||
case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()):
|
||||
ctx.Data["IsImageFile"] = true
|
||||
}
|
||||
|
|
|
@ -624,6 +624,20 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
|
|||
ctx.Data["IsVideoFile"] = true
|
||||
case fInfo.st.IsAudio():
|
||||
ctx.Data["IsAudioFile"] = true
|
||||
case fInfo.st.Is3DModel():
|
||||
ctx.Data["Is3DModelFile"] = true
|
||||
switch {
|
||||
case fInfo.st.IsGLB():
|
||||
ctx.Data["IsGLBFile"] = true
|
||||
case fInfo.st.IsSTL():
|
||||
ctx.Data["IsSTLFile"] = true
|
||||
case fInfo.st.IsGLTF():
|
||||
ctx.Data["IsGLTFFile"] = true
|
||||
case fInfo.st.IsOBJ():
|
||||
ctx.Data["IsOBJFile"] = true
|
||||
case fInfo.st.Is3MF():
|
||||
ctx.Data["Is3MFFile"] = true
|
||||
}
|
||||
case fInfo.st.IsImage() && (setting.UI.SVG.Enabled || !fInfo.st.IsSvgImage()):
|
||||
ctx.Data["IsImageFile"] = true
|
||||
ctx.Data["CanCopyContent"] = true
|
||||
|
|
|
@ -32,6 +32,12 @@
|
|||
</audio>
|
||||
{{else if .IsPDFFile}}
|
||||
<div class="pdf-content is-loading" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "repo.diff.view_file"}}"></div>
|
||||
{{else if .Is3DModelFile}}
|
||||
{{if .IsGLBFile}}
|
||||
<model-viewer src="{{$.RawFileLink}}" ar shadow-intensity="2" camera-controls touch-action="pan-y"></model-viewer>
|
||||
{{else}}
|
||||
<a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}!</a>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
||||
{{end}}
|
||||
|
|
|
@ -116,6 +116,12 @@
|
|||
</audio>
|
||||
{{else if .IsPDFFile}}
|
||||
<div class="pdf-content is-loading" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "repo.diff.view_file"}}"></div>
|
||||
{{else if .Is3DModelFile}}
|
||||
{{if .IsGLBFile}}
|
||||
<model-viewer src="{{$.RawFileLink}}" ar shadow-intensity="2" camera-controls touch-action="pan-y"></model-viewer>
|
||||
{{else}}
|
||||
<a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
||||
{{end}}
|
||||
|
|
14
tests/testdata/data/viewer/README.md
vendored
Normal file
14
tests/testdata/data/viewer/README.md
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
# GLTF 3D Model Viewer
|
||||
|
||||
⚠️ Currently supports `.glb` files only.
|
||||
|
||||
3D models with the `.glb` format are rendered in repository file view.
|
||||
|
||||
## 🔨 How to Test
|
||||
|
||||
1) Create a new repository or use an existing one.
|
||||
2) Upload a `.glb` file such as [Unicode❤♻Test.glb](./Unicode❤♻Test.glb) (CC0 1.0 Universal).
|
||||
3) View the file in the repository.
|
||||
- Similar to image files, the 3D model should be rendered in a viewer.
|
||||
- Use mouse clicks to turn and zoom.
|
||||
|
BIN
tests/testdata/data/viewer/Unicode❤♻Test.glb
vendored
Normal file
BIN
tests/testdata/data/viewer/Unicode❤♻Test.glb
vendored
Normal file
Binary file not shown.
|
@ -415,6 +415,11 @@ td .commit-summary {
|
|||
max-width: 600px !important;
|
||||
}
|
||||
|
||||
model-viewer {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.pdf-content {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
|
|
|
@ -23,6 +23,7 @@ import {initStopwatch} from './features/stopwatch.js';
|
|||
import {initFindFileInRepo} from './features/repo-findfile.js';
|
||||
import {initCommentContent, initMarkupContent} from './markup/content.js';
|
||||
import {initPdfViewer} from './render/pdf.js';
|
||||
import {initGltfViewer} from './render/gltf.js';
|
||||
|
||||
import {initUserAuthOauth2, initUserAuth} from './features/user-auth.js';
|
||||
import {
|
||||
|
@ -187,6 +188,7 @@ onDomReady(() => {
|
|||
initUserAuth();
|
||||
initRepoDiffView();
|
||||
initPdfViewer();
|
||||
initGltfViewer();
|
||||
initScopedAccessTokenCategories();
|
||||
initColorPickers();
|
||||
});
|
||||
|
|
6
web_src/js/render/gltf.js
Normal file
6
web_src/js/render/gltf.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export async function initGltfViewer() {
|
||||
const els = document.querySelectorAll('model-viewer');
|
||||
if (!els.length) return;
|
||||
|
||||
await import(/* webpackChunkName: "@google/model-viewer" */'@google/model-viewer');
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue