Skip to content

Commit a0012ee

Browse files
Thomas Gorisseclaude
andcommitted
fix: migrate Scene to UiHelper, fix build regression, update MCP
- Scene.kt: replace manual SurfaceHolder.Callback/SurfaceTextureListener with Filament UiHelper for both SurfaceView and TextureView branches. UiHelper handles surface lifecycle more robustly, fixing the black-screen issue on Feature Level 1 / OpenGL ES emulators (Apple M3 translator). Swap chain flags now sourced from uiHelper.swapChainFlags. - build.gradle: restore AGP to 8.13.2 (was accidentally downgraded to 8.11.2) - Remove .github/workflows/publish-release.yml: closeAndReleaseRepository task no longer exists with SonatypeHost.CENTRAL_PORTAL — upload-release.yml handles the full publish cycle in one step. - mcp: add geometry-scene sample (cube/sphere/plane, no GLB required), bump to 3.0.1, publish to npm. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8f4683f commit a0012ee

7 files changed

Lines changed: 163 additions & 103 deletions

File tree

.github/workflows/publish-release.yml

Lines changed: 0 additions & 30 deletions
This file was deleted.

mcp/dist/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ try {
1414
catch {
1515
API_DOCS = "SceneView API docs not found. Run `npm run prepare` to bundle llms.txt.";
1616
}
17-
const server = new Server({ name: "@sceneview/mcp", version: "3.0.0" }, { capabilities: { resources: {}, tools: {} } });
17+
const server = new Server({ name: "@sceneview/mcp", version: "3.0.1" }, { capabilities: { resources: {}, tools: {} } });
1818
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
1919
resources: [
2020
{

mcp/dist/samples.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,61 @@ fun ModelViewerScreen() {
3030
)
3131
}
3232
}
33+
}`,
34+
},
35+
"geometry-scene": {
36+
id: "geometry-scene",
37+
title: "3D Geometry Scene",
38+
description: "Procedural 3D scene using primitive geometry nodes (cube, sphere, plane) — no GLB required",
39+
dependency: "io.github.sceneview:sceneview:3.0.0",
40+
prompt: "Create an Android Compose screen called `GeometrySceneScreen` that renders a full-screen 3D scene with a red rotating cube, a metallic blue sphere, and a green floor plane. No model files — use SceneView built-in geometry nodes. Orbit camera. Use SceneView `io.github.sceneview:sceneview:3.0.0`.",
41+
code: `@Composable
42+
fun GeometrySceneScreen() {
43+
val engine = rememberEngine()
44+
val materialLoader = rememberMaterialLoader(engine)
45+
val t = rememberInfiniteTransition(label = "spin")
46+
val angle by t.animateFloat(
47+
initialValue = 0f, targetValue = 360f,
48+
animationSpec = infiniteRepeatable(tween(4_000, easing = LinearEasing)),
49+
label = "angle"
50+
)
51+
52+
Scene(
53+
modifier = Modifier.fillMaxSize(),
54+
engine = engine,
55+
materialLoader = materialLoader,
56+
mainLightNode = rememberMainLightNode(engine) { intensity(80_000f) },
57+
cameraManipulator = rememberCameraManipulator()
58+
) {
59+
// Rotating red cube
60+
CubeNode(
61+
engine,
62+
size = Size(0.5f, 0.5f, 0.5f),
63+
materialInstance = materialLoader.createColorInstance(
64+
Color.Red, metallic = 0f, roughness = 0.5f
65+
),
66+
position = Position(x = -0.6f),
67+
rotation = Rotation(y = angle)
68+
)
69+
// Metallic blue sphere
70+
SphereNode(
71+
engine,
72+
radius = 0.3f,
73+
materialInstance = materialLoader.createColorInstance(
74+
Color.Blue, metallic = 0.8f, roughness = 0.2f
75+
),
76+
position = Position(x = 0.6f)
77+
)
78+
// Floor plane
79+
PlaneNode(
80+
engine,
81+
size = Size(2f, 0f, 2f),
82+
materialInstance = materialLoader.createColorInstance(
83+
Color(0xFF4CAF50), metallic = 0f, roughness = 0.9f
84+
),
85+
position = Position(y = -0.35f)
86+
)
87+
}
3388
}`,
3489
},
3590
"ar-tap-to-place": {

mcp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sceneview-mcp",
3-
"version": "3.0.0",
3+
"version": "3.0.1",
44
"description": "MCP server for SceneView — 3D and AR with Jetpack Compose for Android. Give Claude the full SceneView SDK so it writes correct, compilable Kotlin.",
55
"keywords": [
66
"mcp",

mcp/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ try {
2323
}
2424

2525
const server = new Server(
26-
{ name: "@sceneview/mcp", version: "3.0.0" },
26+
{ name: "@sceneview/mcp", version: "3.0.1" },
2727
{ capabilities: { resources: {}, tools: {} } }
2828
);
2929

mcp/src/samples.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export type SampleId =
22
| "model-viewer"
3+
| "geometry-scene"
34
| "ar-tap-to-place"
45
| "ar-placement-cursor"
56
| "ar-augmented-image"
@@ -50,6 +51,63 @@ fun ModelViewerScreen() {
5051
}`,
5152
},
5253

54+
"geometry-scene": {
55+
id: "geometry-scene",
56+
title: "3D Geometry Scene",
57+
description: "Procedural 3D scene using primitive geometry nodes (cube, sphere, plane) — no GLB required",
58+
dependency: "io.github.sceneview:sceneview:3.0.0",
59+
prompt:
60+
"Create an Android Compose screen called `GeometrySceneScreen` that renders a full-screen 3D scene with a red rotating cube, a metallic blue sphere, and a green floor plane. No model files — use SceneView built-in geometry nodes. Orbit camera. Use SceneView `io.github.sceneview:sceneview:3.0.0`.",
61+
code: `@Composable
62+
fun GeometrySceneScreen() {
63+
val engine = rememberEngine()
64+
val materialLoader = rememberMaterialLoader(engine)
65+
val t = rememberInfiniteTransition(label = "spin")
66+
val angle by t.animateFloat(
67+
initialValue = 0f, targetValue = 360f,
68+
animationSpec = infiniteRepeatable(tween(4_000, easing = LinearEasing)),
69+
label = "angle"
70+
)
71+
72+
Scene(
73+
modifier = Modifier.fillMaxSize(),
74+
engine = engine,
75+
materialLoader = materialLoader,
76+
mainLightNode = rememberMainLightNode(engine) { intensity(80_000f) },
77+
cameraManipulator = rememberCameraManipulator()
78+
) {
79+
// Rotating red cube
80+
CubeNode(
81+
engine,
82+
size = Size(0.5f, 0.5f, 0.5f),
83+
materialInstance = materialLoader.createColorInstance(
84+
Color.Red, metallic = 0f, roughness = 0.5f
85+
),
86+
position = Position(x = -0.6f),
87+
rotation = Rotation(y = angle)
88+
)
89+
// Metallic blue sphere
90+
SphereNode(
91+
engine,
92+
radius = 0.3f,
93+
materialInstance = materialLoader.createColorInstance(
94+
Color.Blue, metallic = 0.8f, roughness = 0.2f
95+
),
96+
position = Position(x = 0.6f)
97+
)
98+
// Floor plane
99+
PlaneNode(
100+
engine,
101+
size = Size(2f, 0f, 2f),
102+
materialInstance = materialLoader.createColorInstance(
103+
Color(0xFF4CAF50), metallic = 0f, roughness = 0.9f
104+
),
105+
position = Position(y = -0.35f)
106+
)
107+
}
108+
}`,
109+
},
110+
53111
"ar-tap-to-place": {
54112
id: "ar-tap-to-place",
55113
title: "AR Tap-to-Place",

sceneview/src/main/java/io/github/sceneview/Scene.kt

Lines changed: 47 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ package io.github.sceneview
33
import android.content.Context
44
import android.content.Context.WINDOW_SERVICE
55
import android.graphics.PixelFormat
6-
import android.graphics.SurfaceTexture
76
import android.opengl.EGLContext
87
import android.view.MotionEvent
98
import android.view.Surface
10-
import android.view.SurfaceHolder
119
import android.view.SurfaceView
1210
import android.view.TextureView
1311
import android.view.WindowManager
@@ -43,6 +41,7 @@ import com.google.android.filament.SwapChain
4341
import com.google.android.filament.View
4442
import com.google.android.filament.Viewport
4543
import com.google.android.filament.android.DisplayHelper
44+
import com.google.android.filament.android.UiHelper
4645
import io.github.sceneview.collision.CollisionSystem
4746
import io.github.sceneview.collision.HitResult
4847
import io.github.sceneview.environment.Environment
@@ -360,41 +359,54 @@ fun Scene(
360359
(context.getSystemService(WINDOW_SERVICE) as WindowManager).defaultDisplay
361360
}
362361

362+
// UiHelper manages swap chain creation/destruction and handles surface lifecycle more robustly
363+
// than a bare SurfaceHolder.Callback (fixes rendering on Feature Level 1 / OpenGL ES emulators).
364+
val uiHelperRef = remember { AtomicReference<UiHelper?>(null) }
365+
DisposableEffect(engine) {
366+
onDispose { uiHelperRef.getAndSet(null)?.detach() }
367+
}
368+
369+
// Shared RendererCallback — wired to whichever surface type is active.
370+
fun makeRendererCallback(viewHeight: () -> Int) = object : UiHelper.RendererCallback {
371+
override fun onNativeWindowChanged(surface: Surface) {
372+
val uiHelper = uiHelperRef.get() ?: return
373+
swapChainRef.getAndSet(
374+
engine.createSwapChain(surface, uiHelper.swapChainFlags)
375+
)?.let { engine.destroySwapChain(it) }
376+
displayHelper.attach(renderer, display)
377+
if (cameraGestureDetectorRef.get() == null) {
378+
cameraGestureDetectorRef.set(
379+
CameraGestureDetector(
380+
viewHeight = viewHeight,
381+
cameraManipulator = cameraManipulator
382+
)
383+
)
384+
}
385+
}
386+
387+
override fun onDetachedFromSurface() {
388+
cameraGestureDetectorRef.set(null)
389+
swapChainRef.getAndSet(null)?.let { engine.destroySwapChain(it) }
390+
engine.flushAndWait()
391+
displayHelper.detach()
392+
}
393+
394+
override fun onResizeView(width: Int, height: Int) {
395+
applyResize(width, height)
396+
engine.drainFramePipeline()
397+
}
398+
}
399+
363400
when (surfaceType) {
364401
SurfaceType.Surface -> AndroidView(
365402
modifier = modifier,
366403
factory = { ctx ->
367404
SurfaceView(ctx).also { sv ->
368405
if (!isOpaque) sv.holder.setFormat(PixelFormat.TRANSLUCENT)
369-
sv.holder.addCallback(object : SurfaceHolder.Callback {
370-
override fun surfaceCreated(holder: SurfaceHolder) {}
371-
372-
override fun surfaceChanged(
373-
holder: SurfaceHolder, format: Int, width: Int, height: Int
374-
) {
375-
if (swapChainRef.get() == null) {
376-
swapChainRef.set(engine.createSwapChain(holder.surface))
377-
displayHelper.attach(renderer, display)
378-
cameraGestureDetectorRef.set(
379-
CameraGestureDetector(
380-
viewHeight = { sv.height },
381-
cameraManipulator = cameraManipulator
382-
)
383-
)
384-
}
385-
applyResize(width, height)
386-
engine.drainFramePipeline()
387-
}
388-
389-
override fun surfaceDestroyed(holder: SurfaceHolder) {
390-
cameraGestureDetectorRef.set(null)
391-
swapChainRef.getAndSet(null)?.let {
392-
runCatching { engine.destroySwapChain(it) }
393-
}
394-
engine.flushAndWait()
395-
displayHelper.detach()
396-
}
397-
})
406+
val uiHelper = UiHelper(UiHelper.ContextErrorHandler { })
407+
uiHelper.renderCallback = makeRendererCallback { sv.height }
408+
uiHelper.attachTo(sv)
409+
uiHelperRef.set(uiHelper)
398410
sv.setOnTouchListener { _, event -> touchDispatcher(event); true }
399411
}
400412
},
@@ -406,45 +418,10 @@ fun Scene(
406418
factory = { ctx ->
407419
TextureView(ctx).also { tv ->
408420
tv.isOpaque = isOpaque
409-
var textureSurface: Surface? = null
410-
tv.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
411-
override fun onSurfaceTextureAvailable(
412-
st: SurfaceTexture, width: Int, height: Int
413-
) {
414-
textureSurface = Surface(st)
415-
swapChainRef.set(engine.createSwapChain(textureSurface!!))
416-
displayHelper.attach(renderer, display)
417-
cameraGestureDetectorRef.set(
418-
CameraGestureDetector(
419-
viewHeight = { tv.height },
420-
cameraManipulator = cameraManipulator
421-
)
422-
)
423-
applyResize(width, height)
424-
engine.drainFramePipeline()
425-
}
426-
427-
override fun onSurfaceTextureSizeChanged(
428-
st: SurfaceTexture, width: Int, height: Int
429-
) {
430-
applyResize(width, height)
431-
engine.drainFramePipeline()
432-
}
433-
434-
override fun onSurfaceTextureDestroyed(st: SurfaceTexture): Boolean {
435-
cameraGestureDetectorRef.set(null)
436-
swapChainRef.getAndSet(null)?.let {
437-
runCatching { engine.destroySwapChain(it) }
438-
}
439-
engine.flushAndWait()
440-
displayHelper.detach()
441-
textureSurface?.release()
442-
textureSurface = null
443-
return true
444-
}
445-
446-
override fun onSurfaceTextureUpdated(st: SurfaceTexture) {}
447-
}
421+
val uiHelper = UiHelper(UiHelper.ContextErrorHandler { })
422+
uiHelper.renderCallback = makeRendererCallback { tv.height }
423+
uiHelper.attachTo(tv)
424+
uiHelperRef.set(uiHelper)
448425
tv.setOnTouchListener { _, event -> touchDispatcher(event); true }
449426
}
450427
},

0 commit comments

Comments
 (0)