dsh-3d-viewer 0.1.0 → 0.2.0

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.
Files changed (47) hide show
  1. package/README.md +180 -107
  2. package/README.zh.md +180 -107
  3. package/THIRD_PARTY_NOTICES.md +54 -52
  4. package/assets/hdri/LICENSE.md +17 -16
  5. package/dist/client.fbx.js +14 -0
  6. package/dist/client.gltf.js +14 -0
  7. package/dist/client.js +4472 -0
  8. package/dist/client.obj.js +19 -0
  9. package/dist/client.stl.js +14 -0
  10. package/dist/client.tex-exr.js +14 -0
  11. package/dist/client.tex-hdr.js +18 -0
  12. package/dist/client.tex-tga.js +14 -0
  13. package/index.js +167 -7
  14. package/package.json +72 -48
  15. package/assets/preview.png +0 -0
  16. package/build.mjs +0 -49
  17. package/client.js +0 -4462
  18. package/scripts/embed-hdri.mjs +0 -70
  19. package/src/bytes.js +0 -158
  20. package/src/client.js +0 -57
  21. package/src/formats.js +0 -100
  22. package/src/generated/hdri-data.js +0 -8
  23. package/src/loaders/gltf.js +0 -251
  24. package/src/loaders/model-loader.js +0 -232
  25. package/src/loaders/workspace.js +0 -295
  26. package/src/model.js +0 -417
  27. package/src/scene/animation.js +0 -149
  28. package/src/scene/blue-noise.js +0 -48
  29. package/src/scene/camera.js +0 -83
  30. package/src/scene/createSceneView.js +0 -375
  31. package/src/scene/environment.js +0 -187
  32. package/src/scene/ground.js +0 -286
  33. package/src/scene/lighting.js +0 -41
  34. package/src/scene/materials.js +0 -263
  35. package/src/scene/postprocessing.js +0 -155
  36. package/src/scene/render-loop.js +0 -98
  37. package/src/scene/viewer-error.js +0 -16
  38. package/src/ui/AnimationBar.js +0 -95
  39. package/src/ui/ModelViewer.js +0 -278
  40. package/src/ui/ViewerSurface.js +0 -87
  41. package/src/ui/chrome.js +0 -220
  42. package/src/ui/dictionaries.js +0 -99
  43. package/src/ui/icons.js +0 -88
  44. package/src/ui/options.js +0 -23
  45. package/src/ui/primitives.js +0 -88
  46. package/src/ui/styles.js +0 -89
  47. package/src/vendor/FBXLoaderPbr.js +0 -4772
@@ -1,278 +0,0 @@
1
- import * as React from 'react';
2
- import { isGlbAddress, normalizeFormat } from '../formats.js';
3
- import { loadModelBytes } from '../loaders/model-loader.js';
4
- import { collectModelStats, disposeObject } from '../model.js';
5
- import { createSceneView } from '../scene/createSceneView.js';
6
- import { ViewerError } from '../scene/viewer-error.js';
7
- import { en } from './dictionaries.js';
8
- import { DEFAULT_SETTINGS } from './options.js';
9
- import { ViewerSurface } from './ViewerSurface.js';
10
-
11
- const h = React.createElement;
12
-
13
- function fallbackTranslate(key, params) {
14
- const template = en[key] ?? key;
15
- if (params === undefined) return template;
16
- return template.replace(/\{(\w+)\}/g, (_match, name) => String(params[name] ?? ''));
17
- }
18
-
19
- function loadingState() {
20
- return { status: 'loading', failure: null, stats: null, clips: [] };
21
- }
22
-
23
- function errorState(error) {
24
- return {
25
- status: 'error',
26
- failure: {
27
- code: error instanceof ViewerError ? error.code : 'scene',
28
- message: error instanceof Error ? error.message : String(error),
29
- detail: error instanceof Error ? (error.stack ?? error.message) : String(error),
30
- },
31
- stats: null,
32
- clips: [],
33
- };
34
- }
35
-
36
- function byteLengthOf(data) {
37
- if (typeof data === 'string') {
38
- try {
39
- return new TextEncoder().encode(data).byteLength;
40
- } catch {
41
- return data.length;
42
- }
43
- }
44
- if (data instanceof ArrayBuffer) return data.byteLength;
45
- if (ArrayBuffer.isView(data)) return data.byteLength;
46
- return null;
47
- }
48
-
49
- /**
50
- * Push one settings snapshot into a live view.
51
- *
52
- * @param {ReturnType<typeof createSceneView>} view Live scene view.
53
- * @param {{ grid: boolean, autoRotate: boolean, wireframe: boolean, material: string, environment: string }} settings Desired settings.
54
- */
55
- function applyViewSettings(view, settings) {
56
- view.setGrid(settings.grid);
57
- view.setMaterialPreset(settings.material);
58
- view.setEnvironment(settings.environment);
59
- view.setWireframe(settings.wireframe);
60
- view.setAutoRotate(settings.autoRotate);
61
- }
62
-
63
- /**
64
- * Right-Sidebar document body for one 3D model.
65
- *
66
- * Owns loading, lifecycle, and settings; rendering is delegated to the pure
67
- * `ViewerSurface`, which can be server-rendered without a WebGL context.
68
- *
69
- * @param {{
70
- * format: 'fbx' | 'gltf' | 'obj' | 'stl',
71
- * content?: { kind?: string, data?: Uint8Array | ArrayBuffer | string },
72
- * resourceAddress?: string,
73
- * readWorkspaceFile?: Function,
74
- * t?: (key: string, params?: Record<string, unknown>) => string,
75
- * fallbackT?: (key: string, params?: Record<string, unknown>) => string,
76
- * }} props Model format, document content, workspace reader, and translator.
77
- */
78
- function ModelViewer(props) {
79
- const format = normalizeFormat(props.format);
80
- const glb = format === 'gltf' && isGlbAddress(props.resourceAddress);
81
- const content = props.content;
82
- const t = props.t ?? props.fallbackT ?? fallbackTranslate;
83
- const data = content !== undefined && content !== null
84
- && (content.kind === 'bytes' || content.kind === 'text')
85
- ? content.data
86
- : null;
87
-
88
- const [runtime, setRuntime] = React.useState(loadingState);
89
- const [settings, setSettings] = React.useState(DEFAULT_SETTINGS);
90
- const [attempt, setAttempt] = React.useState(0);
91
- const hostRef = React.useRef(null);
92
- const viewRef = React.useRef(null);
93
- const tRef = React.useRef(t);
94
- const settingsRef = React.useRef(settings);
95
- const readerRef = React.useRef(typeof props.readWorkspaceFile === 'function' ? props.readWorkspaceFile : null);
96
- const addressRef = React.useRef(typeof props.resourceAddress === 'string' ? props.resourceAddress : null);
97
-
98
- tRef.current = t;
99
- settingsRef.current = settings;
100
- readerRef.current = typeof props.readWorkspaceFile === 'function' ? props.readWorkspaceFile : null;
101
- addressRef.current = typeof props.resourceAddress === 'string' ? props.resourceAddress : null;
102
-
103
- React.useEffect(() => {
104
- const host = hostRef.current;
105
- if (host === null) return undefined;
106
-
107
- let disposed = false;
108
- let model = null;
109
- let view = null;
110
- let assets = null;
111
- const controller = new AbortController();
112
-
113
- // Release whatever the load produced, whichever stage it reached. A view
114
- // owns the model once it adopts the scene, so only one of them is disposed.
115
- const release = () => {
116
- if (view !== null) {
117
- view.dispose();
118
- view = null;
119
- viewRef.current = null;
120
- } else if (model !== null) {
121
- disposeObject(model);
122
- model = null;
123
- }
124
- assets?.dispose();
125
- assets = null;
126
- };
127
-
128
- setRuntime((previous) => previous.status === 'loading' ? previous : loadingState());
129
-
130
- const run = async () => {
131
- if (disposed) return;
132
-
133
- try {
134
- if (data === null) {
135
- throw new ViewerError('parse', 'The preview supplied no binary model content.');
136
- }
137
-
138
- let loaded;
139
- try {
140
- loaded = await loadModelBytes(data, format, {
141
- resourceAddress: addressRef.current ?? undefined,
142
- readWorkspaceFile: readerRef.current ?? undefined,
143
- signal: controller.signal,
144
- });
145
- } catch (error) {
146
- if (disposed || controller.signal.aborted) return;
147
- throw new ViewerError(
148
- 'parse',
149
- error instanceof Error ? error.message : String(error),
150
- error,
151
- );
152
- }
153
-
154
- if (disposed || controller.signal.aborted) {
155
- loaded.assets?.dispose();
156
- disposeObject(loaded.object);
157
- return;
158
- }
159
-
160
- model = loaded.object;
161
- assets = loaded.assets ?? null;
162
-
163
- const nextStats = collectModelStats(model);
164
- if (nextStats.renderables === 0) {
165
- release();
166
- if (!disposed) setRuntime({ status: 'empty', failure: null, stats: null, clips: [] });
167
- return;
168
- }
169
-
170
- const current = settingsRef.current;
171
- assets?.onTextureLoad(() => {
172
- if (!disposed) view?.invalidate();
173
- });
174
- view = createSceneView(host, model, nextStats, {
175
- ariaLabel: tRef.current('canvasLabel'),
176
- environment: current.environment,
177
- });
178
- // The view owns the model from here on; disposing the view disposes it.
179
- model = null;
180
- applyViewSettings(view, current);
181
-
182
- if (disposed) {
183
- release();
184
- return;
185
- }
186
-
187
- viewRef.current = view;
188
- setRuntime({
189
- status: 'ready',
190
- failure: null,
191
- stats: nextStats,
192
- clips: view.animations,
193
- fileSize: byteLengthOf(data),
194
- });
195
- } catch (error) {
196
- if (disposed || controller.signal.aborted) return;
197
-
198
- release();
199
- // createSceneView appends its canvas before it can fail; never leave a
200
- // half-initialized stage behind. A superseded effect must not clear the
201
- // replacement viewer's canvas (its cleanup already ran).
202
- host.replaceChildren();
203
- viewRef.current = null;
204
- setRuntime(errorState(error));
205
- }
206
- };
207
-
208
- const timer = setTimeout(() => {
209
- void run();
210
- }, 16);
211
-
212
- return () => {
213
- disposed = true;
214
- controller.abort();
215
- clearTimeout(timer);
216
- release();
217
- };
218
- }, [data, attempt, format]);
219
-
220
- React.useEffect(() => {
221
- const view = viewRef.current;
222
- if (view !== null) applyViewSettings(view, settings);
223
- }, [settings]);
224
-
225
- const updateSettings = (patch) => {
226
- setSettings((previous) => ({ ...previous, ...patch }));
227
- };
228
-
229
- const retry = () => {
230
- setRuntime(loadingState());
231
- setAttempt((value) => value + 1);
232
- };
233
-
234
- return h(ViewerSurface, {
235
- format,
236
- glb,
237
- t,
238
- view: viewRef.current,
239
- settings,
240
- runtime,
241
- hostRef,
242
- onSettingsChange: updateSettings,
243
- onRetry: retry,
244
- });
245
- }
246
-
247
- /**
248
- * Build a viewer component bound to one document format.
249
- *
250
- * @param {'fbx' | 'gltf' | 'obj' | 'stl'} format Registered format name.
251
- * @returns {(props: object) => unknown} Slot body component.
252
- */
253
- function createViewer(format) {
254
- function Viewer(props) {
255
- return h(ModelViewer, { ...props, format });
256
- }
257
- return Viewer;
258
- }
259
-
260
- /** FBX document body. */
261
- export const FbxViewer = createViewer('fbx');
262
-
263
- /** OBJ document body. */
264
- export const ObjViewer = createViewer('obj');
265
-
266
- /** glTF / GLB document body. */
267
- export const GltfViewer = createViewer('gltf');
268
-
269
- /** STL document body. */
270
- export const StlViewer = createViewer('stl');
271
-
272
- /** Slot bodies keyed by the format registry id. */
273
- export const VIEWER_COMPONENTS = Object.freeze({
274
- fbx: FbxViewer,
275
- obj: ObjViewer,
276
- gltf: GltfViewer,
277
- stl: StlViewer,
278
- });
@@ -1,87 +0,0 @@
1
- import * as React from 'react';
2
- import { AnimationBar } from './AnimationBar.js';
3
- import {
4
- Badge,
5
- Hint,
6
- StatsPanel,
7
- StatusOverlay,
8
- Toolbar,
9
- ViewControls,
10
- } from './chrome.js';
11
- import { CSS } from './styles.js';
12
-
13
- const h = React.createElement;
14
-
15
- /**
16
- * Pure presentation of one 3D document body.
17
- *
18
- * All load, WebGL, and animation lifecycle lives in `ModelViewer`. Keeping the
19
- * markup free of hooks makes the ready/loading/empty/error surfaces renderable
20
- * without a canvas or a WebGL context.
21
- *
22
- * @param {{
23
- * format: 'fbx' | 'gltf' | 'obj' | 'stl',
24
- * glb: boolean,
25
- * t: (key: string, params?: Record<string, unknown>) => string,
26
- * view: object | null,
27
- * settings: { grid: boolean, autoRotate: boolean, wireframe: boolean, material: string, environment: string },
28
- * runtime: { status: string, failure: object | null, stats: object | null, clips: Array<{ name: string, duration: number }>, fileSize?: number | null },
29
- * hostRef: { current: HTMLElement | null },
30
- * onSettingsChange: (patch: Record<string, unknown>) => void,
31
- * onRetry: () => void,
32
- * }} props Surface state and callbacks.
33
- */
34
- export function ViewerSurface({
35
- format,
36
- glb,
37
- t,
38
- view,
39
- settings,
40
- runtime,
41
- hostRef,
42
- onSettingsChange,
43
- onRetry,
44
- }) {
45
- const ready = runtime.status === 'ready' && view !== null;
46
- const stats = runtime.stats;
47
-
48
- return h('div', { className: 'dsh3dv-root', 'data-dsh3d-viewer': '', 'data-dsh3d-format': format },
49
- h('style', null, CSS),
50
- h('div', { className: 'dsh3dv-stage', ref: hostRef, 'data-dsh3d-stage': '' }),
51
- h('div', { className: 'dsh3dv-top' },
52
- h(Badge, { t, format, glb }),
53
- h('div', { className: 'dsh3dv-top-actions' },
54
- ready
55
- ? h(ViewControls, {
56
- material: settings.material,
57
- environment: settings.environment,
58
- onMaterialChange: (id) => onSettingsChange({ material: id, wireframe: false }),
59
- onEnvironmentChange: (id) => onSettingsChange({ environment: id }),
60
- t,
61
- })
62
- : null,
63
- ready
64
- ? h(Toolbar, {
65
- view,
66
- settings,
67
- onChange: onSettingsChange,
68
- t,
69
- })
70
- : null)),
71
- ready && stats !== null
72
- ? h('div', { className: 'dsh3dv-hud' },
73
- h(StatsPanel, { stats, fileSize: runtime.fileSize, t }))
74
- : null,
75
- ready && runtime.clips.length > 0
76
- ? h(AnimationBar, { view, clips: runtime.clips, t })
77
- : null,
78
- ready
79
- ? h(Hint, { t })
80
- : null,
81
- h(StatusOverlay, {
82
- status: runtime.status,
83
- failure: runtime.failure,
84
- onRetry,
85
- t,
86
- }));
87
- }
package/src/ui/chrome.js DELETED
@@ -1,220 +0,0 @@
1
- import * as React from 'react';
2
- import { badgeKeyFor } from '../formats.js';
3
- import { CubeIcon, FrameIcon, GridIcon, OrbitIcon, WireframeIcon } from './icons.js';
4
- import { ENVIRONMENT_OPTIONS, MATERIAL_OPTIONS } from './options.js';
5
- import { StatRow, ToolButton, errorText, formatBytes, formatCount } from './primitives.js';
6
-
7
- const h = React.createElement;
8
-
9
- /** Swatch shape used by the material and environment rails. */
10
- const PICKER_SWATCH = Object.freeze({
11
- material: 'dsh3dv-ball',
12
- environment: 'dsh3dv-env',
13
- });
14
-
15
- export function Badge({ t, format, glb }) {
16
- return h('span', { className: 'dsh3dv-badge' },
17
- h(CubeIcon),
18
- h('span', null, t(badgeKeyFor(format, glb))));
19
- }
20
-
21
- export function Toolbar({ view, settings, onChange, t }) {
22
- return h('div', { className: 'dsh3dv-toolbar', role: 'toolbar', 'aria-label': t('controls') },
23
- h(ToolButton, {
24
- label: t('reset'),
25
- onClick: () => view?.reset(),
26
- }, h(FrameIcon)),
27
- h(ToolButton, {
28
- label: t('autoRotate'),
29
- active: settings.autoRotate,
30
- onClick: () => onChange({ autoRotate: !settings.autoRotate }),
31
- }, h(OrbitIcon)),
32
- h(ToolButton, {
33
- label: t('grid'),
34
- active: settings.grid,
35
- onClick: () => onChange({ grid: !settings.grid }),
36
- }, h(GridIcon)),
37
- h(ToolButton, {
38
- label: t('wireframe'),
39
- active: settings.wireframe,
40
- onClick: () => onChange({ wireframe: !settings.wireframe }),
41
- }, h(WireframeIcon)));
42
- }
43
-
44
- export function StatsPanel({ stats, fileSize, t }) {
45
- return h('div', {
46
- className: 'dsh3dv-stats',
47
- 'data-dsh3d-stats': '',
48
- role: 'group',
49
- 'aria-label': t('statsTitle'),
50
- },
51
- Number.isFinite(fileSize) ? h(StatRow, { label: t('statsFileSize'), value: formatBytes(fileSize) }) : null,
52
- h(StatRow, { label: t('statsMeshes'), value: formatCount(stats.meshes) }),
53
- h(StatRow, { label: t('statsVertices'), value: formatCount(stats.vertices) }),
54
- h(StatRow, { label: t('statsTriangles'), value: formatCount(stats.triangles) }));
55
- }
56
-
57
- /**
58
- * Find the option matching a settings id, falling back to the first one.
59
- *
60
- * @param {ReadonlyArray<{ id: string }>} options Rail options.
61
- * @param {string} value Current settings id.
62
- * @returns {{ id: string, labelKey: string, className: string }} Selected option.
63
- */
64
- function findOption(options, value) {
65
- return options.find((option) => option.id === value) ?? options[0];
66
- }
67
-
68
- /**
69
- * One swatch trigger and its popup menu.
70
- *
71
- * The open state is owned by `ViewControls` so opening one rail closes the
72
- * other; the picker owns only focus management.
73
- */
74
- function Picker({ kind, groupLabel, options, value, open, onOpenChange, onSelect, t }) {
75
- const triggerRef = React.useRef(null);
76
- const menuRef = React.useRef(null);
77
- const selected = findOption(options, value);
78
- const swatch = PICKER_SWATCH[kind];
79
- const label = `${groupLabel}: ${t(selected.labelKey)}`;
80
-
81
- React.useEffect(() => {
82
- if (!open) return;
83
- menuRef.current?.querySelector('[aria-checked="true"]')?.focus?.();
84
- }, [open]);
85
-
86
- return h('div', { className: 'dsh3dv-picker' },
87
- h('button', {
88
- type: 'button',
89
- className: 'dsh3dv-picker-trigger',
90
- 'data-dsh3d-picker': kind,
91
- 'aria-haspopup': 'menu',
92
- 'aria-expanded': open,
93
- 'aria-label': label,
94
- title: label,
95
- ref: triggerRef,
96
- onClick: () => onOpenChange(open ? null : kind),
97
- },
98
- h('span', {
99
- className: `dsh3dv-picker-swatch ${swatch} ${selected.className}`,
100
- 'aria-hidden': true,
101
- })),
102
- open
103
- ? h('div', {
104
- className: 'dsh3dv-picker-menu',
105
- role: 'menu',
106
- 'data-dsh3d-picker-menu': kind,
107
- 'aria-label': groupLabel,
108
- ref: menuRef,
109
- }, options.map((option) => h('button', {
110
- key: option.id,
111
- type: 'button',
112
- className: 'dsh3dv-picker-option',
113
- role: 'menuitemradio',
114
- 'aria-checked': value === option.id,
115
- title: t(option.labelKey),
116
- onClick: () => {
117
- onSelect(option.id);
118
- onOpenChange(null);
119
- triggerRef.current?.focus?.();
120
- },
121
- },
122
- h('span', {
123
- className: `dsh3dv-picker-option-swatch ${swatch} ${option.className}`,
124
- 'aria-hidden': true,
125
- }),
126
- h('span', { className: 'dsh3dv-picker-option-label' }, t(option.labelKey)))))
127
- : null);
128
- }
129
-
130
- export function ViewControls({ material, environment, onMaterialChange, onEnvironmentChange, t }) {
131
- const [open, setOpen] = React.useState(null);
132
- const rootRef = React.useRef(null);
133
-
134
- React.useEffect(() => {
135
- if (open === null) return undefined;
136
-
137
- const closeOnOutside = (event) => {
138
- if (event.type === 'keydown' && event.key !== 'Escape') return;
139
- if (event.type === 'pointerdown' && rootRef.current?.contains(event.target)) return;
140
- setOpen(null);
141
- };
142
-
143
- document.addEventListener('pointerdown', closeOnOutside);
144
- document.addEventListener('keydown', closeOnOutside);
145
- return () => {
146
- document.removeEventListener('pointerdown', closeOnOutside);
147
- document.removeEventListener('keydown', closeOnOutside);
148
- };
149
- }, [open]);
150
-
151
- return h('div', {
152
- className: 'dsh3dv-view-controls',
153
- 'data-dsh3d-controls': '',
154
- ref: rootRef,
155
- },
156
- h(Picker, {
157
- kind: 'material',
158
- groupLabel: t('materials'),
159
- options: MATERIAL_OPTIONS,
160
- value: material,
161
- open: open === 'material',
162
- onOpenChange: setOpen,
163
- onSelect: onMaterialChange,
164
- t,
165
- }),
166
- h(Picker, {
167
- kind: 'environment',
168
- groupLabel: t('environment'),
169
- options: ENVIRONMENT_OPTIONS,
170
- value: environment,
171
- open: open === 'environment',
172
- onOpenChange: setOpen,
173
- onSelect: onEnvironmentChange,
174
- t,
175
- }));
176
- }
177
-
178
- export function Hint({ t }) {
179
- return h('div', { className: 'dsh3dv-hint' }, t('hint'));
180
- }
181
-
182
- export function StatusOverlay({ status, failure, onRetry, t }) {
183
- if (status === 'loading') {
184
- return h('div', { className: 'dsh3dv-overlay', 'data-dsh3d-status': 'loading' },
185
- h('div', {
186
- className: 'dsh3dv-panel dsh3dv-panel--loading',
187
- role: 'status',
188
- 'aria-live': 'polite',
189
- },
190
- h('span', { className: 'dsh3dv-spinner', 'aria-hidden': true }),
191
- h('span', null, t('loading'))));
192
- }
193
-
194
- if (status === 'empty') {
195
- return h('div', { className: 'dsh3dv-overlay', 'data-dsh3d-status': 'empty' },
196
- h('div', { className: 'dsh3dv-panel', role: 'status' },
197
- h('div', { className: 'dsh3dv-panel-icon' }, h(CubeIcon)),
198
- h('p', { className: 'dsh3dv-panel-title' }, t('emptyTitle')),
199
- h('p', { className: 'dsh3dv-panel-text' }, t('empty'))));
200
- }
201
-
202
- if (status === 'error') {
203
- return h('div', { className: 'dsh3dv-overlay', 'data-dsh3d-status': 'error' },
204
- h('div', { className: 'dsh3dv-panel dsh3dv-panel--error', role: 'alert' },
205
- h('div', { className: 'dsh3dv-panel-icon' }, h(CubeIcon)),
206
- h('p', { className: 'dsh3dv-panel-title' }, t('errorTitle')),
207
- h('p', { className: 'dsh3dv-panel-text' }, errorText(failure, t)),
208
- h('div', { className: 'dsh3dv-actions' },
209
- h('button', {
210
- type: 'button',
211
- className: 'dsh3dv-action',
212
- onClick: onRetry,
213
- }, t('retry'))),
214
- h('details', { className: 'dsh3dv-details' },
215
- h('summary', null, t('details')),
216
- h('pre', null, failure?.detail ?? ''))));
217
- }
218
-
219
- return null;
220
- }
@@ -1,99 +0,0 @@
1
- export const zh = {
2
- rendererTitle: 'FBX 3D',
3
- objRendererTitle: 'OBJ 3D',
4
- gltfRendererTitle: 'glTF / GLB 3D',
5
- stlRendererTitle: 'STL 3D',
6
- badge: 'FBX',
7
- objBadge: 'OBJ',
8
- gltfBadge: 'glTF',
9
- glbBadge: 'GLB',
10
- stlBadge: 'STL',
11
- loading: '正在构建三维场景…',
12
- emptyTitle: '没有可显示的网格',
13
- empty: '这个文件不包含可见的网格模型。',
14
- errorTitle: '无法显示这个三维模型',
15
- parseFailed: '模型文件解析失败,请确认文件完整且格式受支持。',
16
- webglUnavailable: '当前浏览器或设备无法创建 WebGL 2 上下文,暂时不能显示三维模型。',
17
- renderFailed: '三维场景初始化失败。',
18
- retry: '重试',
19
- controls: '三维视图控制',
20
- reset: '复位视角',
21
- grid: '显示网格',
22
- wireframe: '线框模式',
23
- materials: '材质球',
24
- materialOriginal: '原始材质',
25
- materialDiffuse: '纯漫反射',
26
- materialPlastic: '亮面塑料',
27
- materialMetal: '镜面金属',
28
- materialMetalPolished: '抛光金属',
29
- materialClay: '雕塑陶土',
30
- environment: '环境',
31
- envNature: '自然',
32
- envCity: '城市',
33
- envIndoor: '室内',
34
- envStudio: '影棚',
35
- autoRotate: '自动旋转',
36
- play: '播放',
37
- pause: '暂停',
38
- animation: '动画',
39
- animationProgress: '动画进度',
40
- animationFallback: '动画 {n}',
41
- statsTitle: '模型统计',
42
- statsFileSize: '文件大小',
43
- statsMeshes: '网格数量',
44
- statsVertices: '顶点数量',
45
- statsTriangles: '三角面数量',
46
- hint: '拖拽旋转 · 滚轮缩放 · 右键平移',
47
- canvasLabel: '三维模型。拖拽旋转,滚轮缩放,右键平移,方向键旋转。',
48
- details: '技术详情',
49
- };
50
-
51
- export const en = {
52
- rendererTitle: 'FBX 3D',
53
- objRendererTitle: 'OBJ 3D',
54
- gltfRendererTitle: 'glTF / GLB 3D',
55
- stlRendererTitle: 'STL 3D',
56
- badge: 'FBX',
57
- objBadge: 'OBJ',
58
- gltfBadge: 'glTF',
59
- glbBadge: 'GLB',
60
- stlBadge: 'STL',
61
- loading: 'Building 3D scene…',
62
- emptyTitle: 'No visible meshes',
63
- empty: 'This file contains no visible mesh geometry.',
64
- errorTitle: 'Could not display this 3D model',
65
- parseFailed: 'The model file could not be parsed. Check that it is complete and uses a supported format.',
66
- webglUnavailable: 'This browser or device cannot create a WebGL 2 context, so the 3D model cannot be shown.',
67
- renderFailed: 'The 3D scene could not be initialized.',
68
- retry: 'Try again',
69
- controls: '3D view controls',
70
- reset: 'Reset view',
71
- grid: 'Show grid',
72
- wireframe: 'Wireframe',
73
- materials: 'Materials',
74
- materialOriginal: 'Original',
75
- materialDiffuse: 'Pure Diffuse',
76
- materialPlastic: 'Glossy Plastic',
77
- materialMetal: 'Mirror Metal',
78
- materialMetalPolished: 'Polished Metal',
79
- materialClay: 'Sculpt Clay',
80
- environment: 'Env',
81
- envNature: 'Nature',
82
- envCity: 'City',
83
- envIndoor: 'Indoor',
84
- envStudio: 'Studio',
85
- autoRotate: 'Auto-rotate',
86
- play: 'Play',
87
- pause: 'Pause',
88
- animation: 'Animation',
89
- animationProgress: 'Animation progress',
90
- animationFallback: 'Animation {n}',
91
- statsTitle: 'Model statistics',
92
- statsFileSize: 'File size',
93
- statsMeshes: 'Meshes',
94
- statsVertices: 'Vertices',
95
- statsTriangles: 'Triangles',
96
- hint: 'Drag to rotate · Scroll to zoom · Right-drag to pan',
97
- canvasLabel: '3D model. Drag to rotate, scroll to zoom, right-drag to pan, or use the arrow keys.',
98
- details: 'Technical details',
99
- };