From 0886ba647c39533879131c6326e6393ce3c2a3e5 Mon Sep 17 00:00:00 2001 From: tianmo Date: Fri, 16 Oct 2026 18:07:36 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=9B=9E=E8=AF=AF=E5=88=A0=E7=9A=84?= =?UTF-8?q?=20listQuietly=EF=BC=88=E4=B8=8A=E4=B8=80=E6=AC=A1=E6=94=B9?= =?UTF-8?q?=E5=8A=A8=E6=8A=8A=E7=BC=96=E8=AF=91=E6=90=9E=E5=9D=8F=E4=BA=86?= =?UTF-8?q?=EF=BC=8C=E7=BC=96=E8=BE=91=E5=99=A8=E8=B5=B7=E4=B8=8D=E6=9D=A5?= =?UTF-8?q?=EF=BC=89=EF=BC=9B+10=20Red=20Bones=20=E6=9A=82=E7=94=A8?= =?UTF-8?q?=E5=B7=B2=E9=AA=8C=E8=AF=81=E5=8F=AF=E8=A1=8C=E7=9A=84=E4=B8=80?= =?UTF-8?q?=E6=A0=B9=E4=B8=80=E6=AC=A1=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- viewpanel/src/com/spineview/ObjectPanel.java | 17 ++-- viewpanel/src/com/spineview/SceneActions.java | 99 +++++++++++++++++++ 2 files changed, 107 insertions(+), 9 deletions(-) diff --git a/viewpanel/src/com/spineview/ObjectPanel.java b/viewpanel/src/com/spineview/ObjectPanel.java index df18353..e97235f 100644 --- a/viewpanel/src/com/spineview/ObjectPanel.java +++ b/viewpanel/src/com/spineview/ObjectPanel.java @@ -636,13 +636,14 @@ public final class ObjectPanel { * 建用编辑器自己的 New Bone(带撤销),建完立刻改该骨骼的 x/y 与 color 字段。 */ private static void createTenRedBones(final State state) { - // ⚠️ 不能整段放在 render 线程里连着调:编辑器是"下一帧"才真正建骨骼的, - // 同一帧连续调 10 次只会生效一次。所以每建一根 -> 回到本线程等一帧 -> 再建下一根。 + // 用已验证可行的做法:编辑器自己的 New Bone(一根一次)+ 每根之间等一帧;最后刷新一次。 + // (此前试过"直接调模型 API 连建 10 根",会把编辑器卡死,已放弃那条路。) final Object parent = currentTarget(state); if (parent == null) { setStatus(state, "+10 Red Bones: select a parent bone first."); return; } + final int[] made = new int[1]; final Object red; try { Class colorClass = Class.forName("com.badlogic.gdx.graphics.Color", true, state.loader); @@ -652,11 +653,9 @@ public final class ObjectPanel { setStatus(state, "+10 Red Bones: 造红色失败 " + error); return; } - int made = 0; for (int i = 0; i < 10; i++) { final float x = (float) (Math.random() * 400.0 - 200.0); final float y = (float) (Math.random() * 400.0 - 200.0); - final int[] ok = new int[1]; final java.util.List before = listQuietly(state); SceneActions.onEditorThread(state.loader, new Runnable() { @Override @@ -667,20 +666,20 @@ public final class ObjectPanel { setBoneNumber(fresh, "x", x); setBoneNumber(fresh, "y", y); setBoneColor(fresh, red); - ok[0] = 1; + made[0]++; } }); - made += ok[0]; try { - Thread.sleep(180); // 等编辑器过一帧,下一根才会真的建出来 + Thread.sleep(180); // 等编辑器过一帧,下一根才会真的建出来 } catch (InterruptedException ignored) { - return; + break; } } - setStatus(state, "made " + made + " red bones under " + SceneObjects.nameOf(parent)); + setStatus(state, "made " + made[0] + " red bones under " + SceneObjects.nameOf(parent)); refreshList(state); } + /** 读一次树里的对象列表(读不到就给空表,不抛)。 */ private static java.util.List listQuietly(State state) { try { return SceneObjects.listAllNodes(state.loader); diff --git a/viewpanel/src/com/spineview/SceneActions.java b/viewpanel/src/com/spineview/SceneActions.java index abe0fd2..6415191 100644 --- a/viewpanel/src/com/spineview/SceneActions.java +++ b/viewpanel/src/com/spineview/SceneActions.java @@ -901,4 +901,103 @@ public final class SceneActions { } return count; } + + // ==================== 直接模型 API:新建骨骼(不经 UI 菜单、不依赖下一帧) ==================== + // + // 编辑器自己的"新建骨骼"代码就在 `Yi.run()` 里,去掉 UI 后本质是: + // new Gp() → 设 name/parent/skeleton/x/y/color → 骨架.D(骨骼) ← GP 上的 (LGp;)V = addBone + // → 骨架.StackMapTable() ← 重建索引 → Editor.D(骨骼, true) → VZ.D(骨架, true, true) 重建树 + // 一次调用可以连建 N 根,最后只刷新一次;外层包 un.D()/un.B() 就是"一次撤销"。 + + /** 直接建一根骨骼(返回新骨骼对象;失败返回 null)。 */ + public static Object createBone(ClassLoader loader, Object skeleton, Object parent, String name, + float x, float y, Object color) { + try { + System.err.println("[crud] createBone step1 new Gp " + skeleton + " parent=" + parent); + Class boneClass = Class.forName("Gp", true, loader); + Object bone = boneClass.getConstructor().newInstance(); + setByType(bone, "GP", skeleton); + if (parent != null) setByType(bone, "Gp", parent); + setByNameType(bone, "D", String.class, name); + setByNameType(bone, "Z", float.class, Float.valueOf(x)); + setByNameType(bone, "V", float.class, Float.valueOf(y)); + if (color != null) setByNameType(bone, "B", + Class.forName("com.badlogic.gdx.graphics.Color", true, loader), color); + System.err.println("[crud] createBone step2 fields ok"); + + // 骨架.D(骨骼):GP 上的 (LGp;)V + Method addBone = findMethod(skeleton.getClass(), "D", new Class[] {boneClass}, void.class); + if (addBone == null) throw new NoSuchMethodException("GP.D(LGp;)V addBone"); + addBone.setAccessible(true); + System.err.println("[crud] createBone step3 " + addBone.getDeclaringClass().getName() + "." + + addBone.getName() + "(Gp) 调用中"); + addBone.invoke(skeleton, bone); + System.err.println("[crud] createBone step3 ok"); + + // 骨架.StackMapTable():重建索引 + Method rebuild = null; + for (Method m : skeleton.getClass().getMethods()) { + if (m.getName().equals("StackMapTable") && m.getParameterTypes().length == 0 + && m.getReturnType() == void.class) { + rebuild = m; + break; + } + } + if (rebuild != null) { + rebuild.setAccessible(true); + rebuild.invoke(skeleton); + } + + // Editor.D(骨骼, true):通知编辑器(找不到就跳过,不影响模型) + try { + Class editorClass = Class.forName("com.esotericsoftware.spine.editor.Editor", true, loader); + Method notify = findMethod(editorClass, "D", new Class[] {boneClass, boolean.class}, + void.class); + Object editor = SceneObjects.global(loader, "com.esotericsoftware.spine.editor.Editor"); + if (notify != null && editor != null) { + notify.setAccessible(true); + notify.invoke(editor, bone, Boolean.TRUE); + } + } catch (Throwable error) { + System.err.println("[crud] Editor.D(bone,true) 跳过: " + error); + } + return bone; + } catch (Throwable error) { + System.err.println("[crud] createBone 失败: " + error); + Throwable cause = error; + while (cause != null) { + System.err.println("[crud] cause: " + cause); + cause.printStackTrace(System.err); + cause = cause.getCause(); + } + return null; + } + } + + /** 按字段类型写(用于 D:LGP; / D:LGp; 这种同名不同描述符的坑)。 */ + static void setByType(Object target, String typeName, Object value) throws Exception { + for (Class type = target.getClass(); type != null && type != Object.class; + type = type.getSuperclass()) { + for (Field field : type.getDeclaredFields()) { + if (!field.getType().getName().equals(typeName)) continue; + field.setAccessible(true); + field.set(target, value); + return; + } + } + } + + /** 按"名字 + 类型"写字段。 */ + static void setByNameType(Object target, String name, Class fieldType, Object value) + throws Exception { + for (Class type = target.getClass(); type != null && type != Object.class; + type = type.getSuperclass()) { + for (Field field : type.getDeclaredFields()) { + if (!field.getName().equals(name) || field.getType() != fieldType) continue; + field.setAccessible(true); + field.set(target, value); + return; + } + } + } }