@changfenhuang/dsh-genui 0.9.1
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/CHANGELOG.md +367 -0
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/README.zh-CN.md +261 -0
- package/SKILL.md +115 -0
- package/cordis.patch.yml +9 -0
- package/demo-prompts.md +118 -0
- package/lib/assets/echarts.js +64 -0
- package/lib/assets/mermaid.js +3591 -0
- package/lib/assets/three.js +3844 -0
- package/lib/client.js +5 -0
- package/lib/index.js +1895 -0
- package/lib/invariant.js +19 -0
- package/lib/types/client/EChartNode.d.ts +4 -0
- package/lib/types/client/ErrorBoundary.d.ts +30 -0
- package/lib/types/client/GenuiBlock.d.ts +7 -0
- package/lib/types/client/PlotBlock.d.ts +44 -0
- package/lib/types/client/action-context.d.ts +7 -0
- package/lib/types/client/asset-echarts.d.ts +15 -0
- package/lib/types/client/asset-loader.d.ts +31 -0
- package/lib/types/client/asset-mermaid.d.ts +1 -0
- package/lib/types/client/asset-three.d.ts +1 -0
- package/lib/types/client/blocks/advanced.d.ts +104 -0
- package/lib/types/client/blocks/basic.d.ts +27 -0
- package/lib/types/client/blocks/charts.d.ts +38 -0
- package/lib/types/client/blocks/diagram/geometry.d.ts +73 -0
- package/lib/types/client/blocks/diagram/index.d.ts +5 -0
- package/lib/types/client/blocks/diagram/layout.d.ts +41 -0
- package/lib/types/client/blocks/diagram/theme.d.ts +35 -0
- package/lib/types/client/blocks/forms.d.ts +83 -0
- package/lib/types/client/blocks/render-node.d.ts +10 -0
- package/lib/types/client/blocks/state.d.ts +58 -0
- package/lib/types/client/dom-fence.d.ts +14 -0
- package/lib/types/client/echarts-lazy.d.ts +17 -0
- package/lib/types/client/fence-render.d.ts +58 -0
- package/lib/types/client/gallery.d.ts +9 -0
- package/lib/types/client/guard.d.ts +126 -0
- package/lib/types/client/index.d.ts +36 -0
- package/lib/types/client/interaction-store.d.ts +43 -0
- package/lib/types/client/mermaid-core.d.ts +8 -0
- package/lib/types/client/mermaid-lazy.d.ts +9 -0
- package/lib/types/client/mermaid-safe.d.ts +52 -0
- package/lib/types/client/panel-command.d.ts +29 -0
- package/lib/types/client/panel-store.d.ts +94 -0
- package/lib/types/client/panel.d.ts +15 -0
- package/lib/types/client/parse-partial.d.ts +37 -0
- package/lib/types/client/safe-math.d.ts +43 -0
- package/lib/types/client/scene3d-core.d.ts +21 -0
- package/lib/types/client/scene3d-lazy.d.ts +8 -0
- package/lib/types/client/spec.d.ts +610 -0
- package/lib/types/client/toolview.d.ts +7 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/plugin/index.d.ts +25 -0
- package/lib/types/plugin/invariant.d.ts +15 -0
- package/lib/types/plugin/tool.d.ts +27 -0
- package/lib/types/shared/fence-repair.d.ts +67 -0
- package/package.json +121 -0
- package/src/client/EChartNode.tsx +242 -0
- package/src/client/ErrorBoundary.tsx +66 -0
- package/src/client/GenuiBlock.module.css +1263 -0
- package/src/client/GenuiBlock.tsx +166 -0
- package/src/client/PlotBlock.module.css +155 -0
- package/src/client/PlotBlock.tsx +431 -0
- package/src/client/action-context.ts +32 -0
- package/src/client/asset-echarts.ts +25 -0
- package/src/client/asset-loader.ts +79 -0
- package/src/client/asset-mermaid.ts +13 -0
- package/src/client/asset-three.ts +12 -0
- package/src/client/blocks/advanced.tsx +435 -0
- package/src/client/blocks/basic.tsx +116 -0
- package/src/client/blocks/charts.tsx +319 -0
- package/src/client/blocks/diagram/geometry.ts +151 -0
- package/src/client/blocks/diagram/index.tsx +284 -0
- package/src/client/blocks/diagram/layout.ts +199 -0
- package/src/client/blocks/diagram/theme.ts +108 -0
- package/src/client/blocks/forms.tsx +523 -0
- package/src/client/blocks/render-node.tsx +260 -0
- package/src/client/blocks/state.ts +61 -0
- package/src/client/dom-fence.tsx +641 -0
- package/src/client/echarts-lazy.ts +35 -0
- package/src/client/fence-render.tsx +230 -0
- package/src/client/gallery.ts +128 -0
- package/src/client/guard.ts +1450 -0
- package/src/client/index.tsx +195 -0
- package/src/client/interaction-store.ts +122 -0
- package/src/client/mermaid-core.ts +121 -0
- package/src/client/mermaid-lazy.ts +31 -0
- package/src/client/mermaid-safe.ts +122 -0
- package/src/client/panel-command.ts +119 -0
- package/src/client/panel-store.ts +471 -0
- package/src/client/panel.tsx +185 -0
- package/src/client/parse-partial.ts +162 -0
- package/src/client/safe-math.ts +270 -0
- package/src/client/scene3d-core.ts +192 -0
- package/src/client/scene3d-lazy.ts +26 -0
- package/src/client/spec.ts +767 -0
- package/src/client/toolview.tsx +66 -0
- package/src/css-modules.d.ts +5 -0
- package/src/index.ts +12 -0
- package/src/plugin/index.ts +173 -0
- package/src/plugin/invariant.ts +30 -0
- package/src/plugin/node-shims.d.ts +31 -0
- package/src/plugin/tool.ts +330 -0
- package/src/shared/fence-repair.ts +244 -0
- package/tsconfig.json +35 -0
- package/tsdown.config.ts +193 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* three.js scene renderer — the HEAVY half. This module imports three and is
|
|
3
|
+
* bundled ONLY into the lazy asset bundle `lib/assets/three.js`, loaded on
|
|
4
|
+
* demand by the scene3d-lazy loader when a spec contains a `scene3d` node;
|
|
5
|
+
* the main client bundle stays small.
|
|
6
|
+
*
|
|
7
|
+
* Security: the model only supplies white-listed primitive shapes, numeric
|
|
8
|
+
* transforms, and colors. No textures, no external URLs, no scripts, no
|
|
9
|
+
* custom shaders — everything is geometry + material colors constructed
|
|
10
|
+
* locally. The renderer mounts into a caller-owned container and returns a
|
|
11
|
+
* disposer that tears down the WebGL context.
|
|
12
|
+
* @module @changfenhuang/dsh-genui/client/scene3d-core
|
|
13
|
+
*/
|
|
14
|
+
import type { GenuiScene3D } from './spec.ts'
|
|
15
|
+
|
|
16
|
+
let threePromise: Promise<typeof import('three')> | null = null
|
|
17
|
+
|
|
18
|
+
function loadThree(): Promise<typeof import('three')> {
|
|
19
|
+
threePromise ??= import('three')
|
|
20
|
+
return threePromise
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Shape constructors we accept, mapped to a size-normalized factory. */
|
|
24
|
+
type ShapeKind = 'box' | 'sphere' | 'cone' | 'cylinder' | 'torus'
|
|
25
|
+
|
|
26
|
+
function geometryFor(
|
|
27
|
+
THREE: typeof import('three'),
|
|
28
|
+
shape: ShapeKind,
|
|
29
|
+
size: number | [number, number, number] | undefined,
|
|
30
|
+
): import('three').BufferGeometry {
|
|
31
|
+
const s = size ?? 1
|
|
32
|
+
switch (shape) {
|
|
33
|
+
case 'box': {
|
|
34
|
+
const [w, h, d] = Array.isArray(s) ? s : [s, s, s]
|
|
35
|
+
return new THREE.BoxGeometry(w, h, d)
|
|
36
|
+
}
|
|
37
|
+
case 'sphere': {
|
|
38
|
+
const r = Array.isArray(s) ? s[0] : s
|
|
39
|
+
return new THREE.SphereGeometry(r, 32, 24)
|
|
40
|
+
}
|
|
41
|
+
case 'cone': {
|
|
42
|
+
const r = Array.isArray(s) ? s[0] : s
|
|
43
|
+
return new THREE.ConeGeometry(r, Array.isArray(s) ? s[1] : r * 2, 32)
|
|
44
|
+
}
|
|
45
|
+
case 'cylinder': {
|
|
46
|
+
const r = Array.isArray(s) ? s[0] : s
|
|
47
|
+
return new THREE.CylinderGeometry(r, r, Array.isArray(s) ? s[1] : r * 2, 32)
|
|
48
|
+
}
|
|
49
|
+
case 'torus': {
|
|
50
|
+
const r = Array.isArray(s) ? s[0] : s
|
|
51
|
+
return new THREE.TorusGeometry(r, Array.isArray(s) ? s[1] : r * 0.4, 16, 48)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Mount a GenUI 3D scene into `container`.
|
|
58
|
+
* @param container - the DOM node to host the WebGL canvas.
|
|
59
|
+
* @param scene - the declarative scene spec.
|
|
60
|
+
* @returns a disposer that removes the renderer and its context.
|
|
61
|
+
*/
|
|
62
|
+
export async function mountScene(container: HTMLElement, scene: GenuiScene3D): Promise<() => void> {
|
|
63
|
+
const THREE = await loadThree()
|
|
64
|
+
const width = container.clientWidth || 420
|
|
65
|
+
const height = 240
|
|
66
|
+
|
|
67
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
|
|
68
|
+
renderer.setSize(width, height)
|
|
69
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
|
70
|
+
container.appendChild(renderer.domElement)
|
|
71
|
+
|
|
72
|
+
const camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 100)
|
|
73
|
+
camera.position.set(3, 2.4, 4)
|
|
74
|
+
camera.lookAt(0, 0, 0)
|
|
75
|
+
|
|
76
|
+
const threeScene = new THREE.Scene()
|
|
77
|
+
if (scene.background !== undefined) {
|
|
78
|
+
try { threeScene.background = new THREE.Color(scene.background) } catch { /* keep transparent */ }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const ambient = new THREE.AmbientLight(0xffffff, scene.ambient ?? 0.8)
|
|
82
|
+
threeScene.add(ambient)
|
|
83
|
+
const key = new THREE.DirectionalLight(0xffffff, 1.6)
|
|
84
|
+
key.position.set(4, 6, 3)
|
|
85
|
+
threeScene.add(key)
|
|
86
|
+
const rim = new THREE.DirectionalLight(0x6ea8ff, 0.6)
|
|
87
|
+
rim.position.set(-4, -2, -4)
|
|
88
|
+
threeScene.add(rim)
|
|
89
|
+
|
|
90
|
+
// Wireframe-ish ground grid for spatial reference.
|
|
91
|
+
const grid = new THREE.GridHelper(8, 16, 0x3a4152, 0x232a38)
|
|
92
|
+
threeScene.add(grid)
|
|
93
|
+
|
|
94
|
+
for (const mesh of scene.meshes) {
|
|
95
|
+
const geo = geometryFor(THREE, mesh.shape, mesh.size)
|
|
96
|
+
const color = new THREE.Color(mesh.color ?? '#6ea8ff')
|
|
97
|
+
const mat = new THREE.MeshStandardMaterial({ color, roughness: 0.35, metalness: 0.15 })
|
|
98
|
+
const obj = new THREE.Mesh(geo, mat)
|
|
99
|
+
const p = mesh.position ?? [0, 0, 0]
|
|
100
|
+
obj.position.set(p[0] ?? 0, p[1] ?? 0, p[2] ?? 0)
|
|
101
|
+
const r = mesh.rotation ?? [0, 0, 0]
|
|
102
|
+
obj.rotation.set(r[0] ?? 0, r[1] ?? 0, r[2] ?? 0)
|
|
103
|
+
const sc = mesh.scale ?? 1
|
|
104
|
+
if (typeof sc === 'number') obj.scale.setScalar(sc)
|
|
105
|
+
else obj.scale.set(sc[0] ?? 1, sc[1] ?? 1, sc[2] ?? 1)
|
|
106
|
+
threeScene.add(obj)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Orbit: simple drag-to-rotate, wheel to zoom (no external controls dep).
|
|
110
|
+
// EVENT-DRIVEN rendering: one render after init, one per orbit change —
|
|
111
|
+
// a static scene never runs a requestAnimationFrame loop, so an idle
|
|
112
|
+
// scene costs zero GPU/battery. Pointer capture keeps all drag events on
|
|
113
|
+
// the canvas (no window listeners, nothing to leak on unmount).
|
|
114
|
+
let isDragging = false
|
|
115
|
+
let prevX = 0
|
|
116
|
+
let prevY = 0
|
|
117
|
+
let theta = 0.8
|
|
118
|
+
let phi = 0.6
|
|
119
|
+
let radius = 5.5
|
|
120
|
+
const orbit = (): void => {
|
|
121
|
+
camera.position.set(
|
|
122
|
+
radius * Math.sin(phi) * Math.cos(theta),
|
|
123
|
+
radius * Math.cos(phi),
|
|
124
|
+
radius * Math.sin(phi) * Math.sin(theta),
|
|
125
|
+
)
|
|
126
|
+
camera.lookAt(0, 0, 0)
|
|
127
|
+
renderer.render(threeScene, camera)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const onPointerDown = (e: PointerEvent): void => {
|
|
131
|
+
isDragging = true
|
|
132
|
+
prevX = e.clientX
|
|
133
|
+
prevY = e.clientY
|
|
134
|
+
try { canvas.setPointerCapture(e.pointerId) } catch { /* jsdom / capture unsupported */ }
|
|
135
|
+
}
|
|
136
|
+
const onPointerMove = (e: PointerEvent): void => {
|
|
137
|
+
if (!isDragging) return
|
|
138
|
+
theta -= (e.clientX - prevX) * 0.01
|
|
139
|
+
phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi + (e.clientY - prevY) * 0.01))
|
|
140
|
+
prevX = e.clientX
|
|
141
|
+
prevY = e.clientY
|
|
142
|
+
orbit()
|
|
143
|
+
}
|
|
144
|
+
const onPointerEnd = (e: PointerEvent): void => {
|
|
145
|
+
if (!isDragging) return
|
|
146
|
+
isDragging = false
|
|
147
|
+
try { canvas.releasePointerCapture(e.pointerId) } catch { /* jsdom */ }
|
|
148
|
+
}
|
|
149
|
+
const onWheel = (e: WheelEvent): void => {
|
|
150
|
+
e.preventDefault()
|
|
151
|
+
radius = Math.max(2.5, Math.min(14, radius + e.deltaY * 0.005))
|
|
152
|
+
orbit()
|
|
153
|
+
}
|
|
154
|
+
const canvas = renderer.domElement
|
|
155
|
+
canvas.addEventListener('pointerdown', onPointerDown)
|
|
156
|
+
canvas.addEventListener('pointermove', onPointerMove)
|
|
157
|
+
canvas.addEventListener('pointerup', onPointerEnd)
|
|
158
|
+
canvas.addEventListener('pointercancel', onPointerEnd)
|
|
159
|
+
canvas.addEventListener('wheel', onWheel, { passive: false })
|
|
160
|
+
|
|
161
|
+
// Container-size sync: the panel is resizable and the window reflows, so a
|
|
162
|
+
// fixed mount-time width drifts. Event-driven like the orbit — a resize
|
|
163
|
+
// renders exactly once, an idle scene still costs nothing.
|
|
164
|
+
let lastWidth = width
|
|
165
|
+
const observer = typeof ResizeObserver !== 'undefined'
|
|
166
|
+
? new ResizeObserver(() => {
|
|
167
|
+
const w = container.clientWidth || 420
|
|
168
|
+
if (w === lastWidth) return
|
|
169
|
+
lastWidth = w
|
|
170
|
+
renderer.setSize(w, height)
|
|
171
|
+
camera.aspect = w / height
|
|
172
|
+
camera.updateProjectionMatrix()
|
|
173
|
+
renderer.render(threeScene, camera)
|
|
174
|
+
})
|
|
175
|
+
: null
|
|
176
|
+
observer?.observe(container)
|
|
177
|
+
|
|
178
|
+
orbit() // initial render; afterwards only interactions re-render.
|
|
179
|
+
|
|
180
|
+
return () => {
|
|
181
|
+
observer?.disconnect()
|
|
182
|
+
canvas.removeEventListener('pointerdown', onPointerDown)
|
|
183
|
+
canvas.removeEventListener('pointermove', onPointerMove)
|
|
184
|
+
canvas.removeEventListener('pointerup', onPointerEnd)
|
|
185
|
+
canvas.removeEventListener('pointercancel', onPointerEnd)
|
|
186
|
+
canvas.removeEventListener('wheel', onWheel)
|
|
187
|
+
renderer.dispose()
|
|
188
|
+
if (renderer.domElement.parentElement === container) {
|
|
189
|
+
container.removeChild(renderer.domElement)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime loader for the three.js scene renderer. The heavy three bundle
|
|
3
|
+
* ships as a separate asset (`lib/assets/three.js`, served by the plugin's
|
|
4
|
+
* own HTTP route) and is fetched ONLY when a spec contains a `scene3d` node.
|
|
5
|
+
* On a host that does not serve the asset the load rejects and the
|
|
6
|
+
* Scene3DNode shows its error hint.
|
|
7
|
+
* @module @changfenhuang/dsh-genui/client/scene3d-lazy
|
|
8
|
+
*/
|
|
9
|
+
import { loadGenuiAsset } from './asset-loader.ts'
|
|
10
|
+
import type { GenuiScene3D } from './spec.ts'
|
|
11
|
+
|
|
12
|
+
/** The engine surface registered by the three asset bundle. */
|
|
13
|
+
interface ThreeAssetApi {
|
|
14
|
+
mountScene: (container: HTMLElement, scene: GenuiScene3D) => Promise<() => void>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Mount a GenUI 3D scene into `container` (engine loaded on demand).
|
|
19
|
+
* @param container - the DOM node to host the WebGL canvas.
|
|
20
|
+
* @param scene - the declarative scene spec.
|
|
21
|
+
* @returns a disposer that removes the renderer and its context.
|
|
22
|
+
*/
|
|
23
|
+
export async function mountScene(container: HTMLElement, scene: GenuiScene3D): Promise<() => void> {
|
|
24
|
+
const api = await loadGenuiAsset<ThreeAssetApi>('three')
|
|
25
|
+
return api.mountScene(container, scene)
|
|
26
|
+
}
|