drawdown-arcore-depth 0.1.4
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/LICENSE +21 -0
- package/NOTICE.md +7 -0
- package/README.md +190 -0
- package/android/build.gradle +42 -0
- package/android/consumer-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +11 -0
- package/android/src/main/java/org/drawdown/arcoredepth/ArCameraRenderer.java +202 -0
- package/android/src/main/java/org/drawdown/arcoredepth/DepthObservation.java +46 -0
- package/android/src/main/java/org/drawdown/arcoredepth/DepthObservationSampler.java +509 -0
- package/android/src/main/java/org/drawdown/arcoredepth/DrawDownArCoreDepthPlugin.java +567 -0
- package/android/src/main/res/values/strings.xml +3 -0
- package/dist/esm/definitions.d.ts +54 -0
- package/dist/esm/definitions.d.ts.map +1 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +9 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +21 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +6 -0
- package/dist/plugin.js +7 -0
- package/dist/web.cjs.js +11 -0
- package/package.json +50 -0
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
package org.drawdown.arcoredepth;
|
|
2
|
+
|
|
3
|
+
import android.Manifest;
|
|
4
|
+
import android.content.pm.PackageManager;
|
|
5
|
+
import android.graphics.Color;
|
|
6
|
+
import android.os.Build;
|
|
7
|
+
import android.os.Handler;
|
|
8
|
+
import android.os.Looper;
|
|
9
|
+
import android.view.Gravity;
|
|
10
|
+
import android.view.View;
|
|
11
|
+
import android.view.ViewGroup;
|
|
12
|
+
import android.widget.FrameLayout;
|
|
13
|
+
import android.widget.TextView;
|
|
14
|
+
|
|
15
|
+
import com.getcapacitor.JSArray;
|
|
16
|
+
import com.getcapacitor.JSObject;
|
|
17
|
+
import com.getcapacitor.Plugin;
|
|
18
|
+
import com.getcapacitor.PermissionState;
|
|
19
|
+
import com.getcapacitor.PluginCall;
|
|
20
|
+
import com.getcapacitor.PluginMethod;
|
|
21
|
+
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
22
|
+
import com.getcapacitor.annotation.Permission;
|
|
23
|
+
import com.getcapacitor.annotation.PermissionCallback;
|
|
24
|
+
import com.google.ar.core.ArCoreApk;
|
|
25
|
+
import com.google.ar.core.Camera;
|
|
26
|
+
import com.google.ar.core.Config;
|
|
27
|
+
import com.google.ar.core.Frame;
|
|
28
|
+
import com.google.ar.core.Session;
|
|
29
|
+
import com.google.ar.core.exceptions.CameraNotAvailableException;
|
|
30
|
+
import com.google.ar.core.exceptions.UnavailableApkTooOldException;
|
|
31
|
+
import com.google.ar.core.exceptions.UnavailableArcoreNotInstalledException;
|
|
32
|
+
import com.google.ar.core.exceptions.UnavailableDeviceNotCompatibleException;
|
|
33
|
+
import com.google.ar.core.exceptions.UnavailableSdkTooOldException;
|
|
34
|
+
import com.google.ar.core.exceptions.UnavailableUserDeclinedInstallationException;
|
|
35
|
+
|
|
36
|
+
import android.opengl.GLSurfaceView;
|
|
37
|
+
|
|
38
|
+
import java.util.ArrayList;
|
|
39
|
+
import java.util.List;
|
|
40
|
+
|
|
41
|
+
@CapacitorPlugin(
|
|
42
|
+
name = "DrawDownArCoreDepth",
|
|
43
|
+
permissions = {
|
|
44
|
+
@Permission(alias = "camera", strings = { Manifest.permission.CAMERA })
|
|
45
|
+
})
|
|
46
|
+
public class DrawDownArCoreDepthPlugin extends Plugin {
|
|
47
|
+
private static final String PLUGIN_VERSION = "0.1.4";
|
|
48
|
+
private static final long WARMUP_MS = 1200L;
|
|
49
|
+
|
|
50
|
+
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
|
51
|
+
private final DepthObservationSampler sampler = new DepthObservationSampler();
|
|
52
|
+
private final Object sweepLock = new Object();
|
|
53
|
+
|
|
54
|
+
private Session session;
|
|
55
|
+
private GLSurfaceView surfaceView;
|
|
56
|
+
private FrameLayout nativeOverlay;
|
|
57
|
+
private TextView instructionText;
|
|
58
|
+
private ArCameraRenderer renderer;
|
|
59
|
+
private PluginCall pendingStartCall;
|
|
60
|
+
private PluginCall pendingSweepCall;
|
|
61
|
+
|
|
62
|
+
private boolean sessionResumed = false;
|
|
63
|
+
private volatile boolean sweepActive = false;
|
|
64
|
+
private long sweepCollectionStartsAtMs = 0L;
|
|
65
|
+
private long sweepEndsAtMs = 0L;
|
|
66
|
+
private long lastSampleAtMs = 0L;
|
|
67
|
+
private long sampleIntervalMs = 250L;
|
|
68
|
+
private int maxObservations = 10;
|
|
69
|
+
private int minObservations = 5;
|
|
70
|
+
private final List<DepthObservation> sweepObservations = new ArrayList<>();
|
|
71
|
+
|
|
72
|
+
@PluginMethod
|
|
73
|
+
public void checkSupport(PluginCall call) {
|
|
74
|
+
getActivity().runOnUiThread(() -> {
|
|
75
|
+
JSObject result = new JSObject();
|
|
76
|
+
result.put("pluginVersion", PLUGIN_VERSION);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
ArCoreApk.Availability availability = ArCoreApk.getInstance().checkAvailability(getActivity());
|
|
80
|
+
if (availability.isTransient()) {
|
|
81
|
+
result.put("isSupported", false);
|
|
82
|
+
result.put("hasDepthApi", false);
|
|
83
|
+
result.put("message", "ARCore support is still being checked. Retry in a moment.");
|
|
84
|
+
call.resolve(result);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!availability.isSupported()) {
|
|
89
|
+
result.put("isSupported", false);
|
|
90
|
+
result.put("hasDepthApi", false);
|
|
91
|
+
result.put("message", "This Android device is not supported by ARCore.");
|
|
92
|
+
call.resolve(result);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
boolean hasDepth = false;
|
|
97
|
+
Session probe = null;
|
|
98
|
+
try {
|
|
99
|
+
probe = new Session(getActivity());
|
|
100
|
+
hasDepth = probe.isDepthModeSupported(Config.DepthMode.AUTOMATIC);
|
|
101
|
+
} finally {
|
|
102
|
+
if (probe != null) {
|
|
103
|
+
probe.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
result.put("isSupported", true);
|
|
108
|
+
result.put("hasDepthApi", hasDepth);
|
|
109
|
+
result.put("message", hasDepth
|
|
110
|
+
? "ARCore Depth is available on this device."
|
|
111
|
+
: "ARCore is available, but the Depth API is not supported on this device.");
|
|
112
|
+
call.resolve(result);
|
|
113
|
+
} catch (UnavailableArcoreNotInstalledException e) {
|
|
114
|
+
result.put("isSupported", true);
|
|
115
|
+
result.put("hasDepthApi", false);
|
|
116
|
+
result.put("message", "Google Play Services for AR is not installed.");
|
|
117
|
+
call.resolve(result);
|
|
118
|
+
} catch (UnavailableApkTooOldException e) {
|
|
119
|
+
result.put("isSupported", true);
|
|
120
|
+
result.put("hasDepthApi", false);
|
|
121
|
+
result.put("message", "Google Play Services for AR needs to be updated.");
|
|
122
|
+
call.resolve(result);
|
|
123
|
+
} catch (UnavailableSdkTooOldException e) {
|
|
124
|
+
result.put("isSupported", false);
|
|
125
|
+
result.put("hasDepthApi", false);
|
|
126
|
+
result.put("message", "This app's ARCore SDK is too old for the installed AR service.");
|
|
127
|
+
call.resolve(result);
|
|
128
|
+
} catch (UnavailableDeviceNotCompatibleException e) {
|
|
129
|
+
result.put("isSupported", false);
|
|
130
|
+
result.put("hasDepthApi", false);
|
|
131
|
+
result.put("message", "This device is not compatible with ARCore.");
|
|
132
|
+
call.resolve(result);
|
|
133
|
+
} catch (Exception e) {
|
|
134
|
+
result.put("isSupported", false);
|
|
135
|
+
result.put("hasDepthApi", false);
|
|
136
|
+
result.put("message", "Unable to check ARCore Depth support: " + safeMessage(e));
|
|
137
|
+
call.resolve(result);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@PluginMethod
|
|
143
|
+
public void startDbhSession(PluginCall call) {
|
|
144
|
+
// From v0.1.2 the RFID/NFC tree tag is the field anchor for the 1.3 m DBH plane.
|
|
145
|
+
// Legacy ArUco arguments are deliberately ignored for backward compatibility.
|
|
146
|
+
Double targetHeight = call.getDouble("targetHeightM", 1.3);
|
|
147
|
+
|
|
148
|
+
if (targetHeight == null || Math.abs(targetHeight - 1.3) > 0.01) {
|
|
149
|
+
call.reject("DrawDown DBH requires the RFID/NFC tag to identify the 1.3 m measurement plane.");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!hasCameraPermission()) {
|
|
154
|
+
requestAllPermissions(call, "cameraPermissionResult");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
startSessionInternal(call);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@PermissionCallback
|
|
162
|
+
private void cameraPermissionResult(PluginCall call) {
|
|
163
|
+
if (!hasCameraPermission()) {
|
|
164
|
+
call.reject("Camera permission is required for ARCore DBH.");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
startSessionInternal(call);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private void startSessionInternal(PluginCall call) {
|
|
171
|
+
getActivity().runOnUiThread(() -> {
|
|
172
|
+
if (session != null) {
|
|
173
|
+
JSObject already = new JSObject();
|
|
174
|
+
already.put("started", true);
|
|
175
|
+
already.put("message", "ARCore DBH session is already running.");
|
|
176
|
+
call.resolve(already);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
ArCoreApk.InstallStatus installStatus = ArCoreApk.getInstance().requestInstall(getActivity(), true);
|
|
182
|
+
if (installStatus == ArCoreApk.InstallStatus.INSTALL_REQUESTED) {
|
|
183
|
+
JSObject install = new JSObject();
|
|
184
|
+
install.put("started", false);
|
|
185
|
+
install.put("installationRequested", true);
|
|
186
|
+
install.put("message", "Google Play Services for AR installation was requested. Retry after installation completes.");
|
|
187
|
+
call.resolve(install);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
session = new Session(getActivity());
|
|
192
|
+
if (!session.isDepthModeSupported(Config.DepthMode.AUTOMATIC)) {
|
|
193
|
+
session.close();
|
|
194
|
+
session = null;
|
|
195
|
+
call.reject("This ARCore device does not support the Depth API.");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
Config config = session.getConfig();
|
|
200
|
+
config.setDepthMode(Config.DepthMode.AUTOMATIC);
|
|
201
|
+
config.setFocusMode(Config.FocusMode.AUTO);
|
|
202
|
+
config.setUpdateMode(Config.UpdateMode.LATEST_CAMERA_IMAGE);
|
|
203
|
+
config.setPlaneFindingMode(Config.PlaneFindingMode.DISABLED);
|
|
204
|
+
config.setLightEstimationMode(Config.LightEstimationMode.DISABLED);
|
|
205
|
+
session.configure(config);
|
|
206
|
+
|
|
207
|
+
pendingStartCall = call;
|
|
208
|
+
createNativeCameraOverlay();
|
|
209
|
+
} catch (UnavailableArcoreNotInstalledException
|
|
210
|
+
| UnavailableUserDeclinedInstallationException e) {
|
|
211
|
+
cleanupSession();
|
|
212
|
+
call.reject("Google Play Services for AR is not installed or installation was declined.");
|
|
213
|
+
} catch (UnavailableApkTooOldException e) {
|
|
214
|
+
cleanupSession();
|
|
215
|
+
call.reject("Google Play Services for AR needs to be updated.");
|
|
216
|
+
} catch (UnavailableSdkTooOldException e) {
|
|
217
|
+
cleanupSession();
|
|
218
|
+
call.reject("The DrawDown ARCore SDK needs to be updated.");
|
|
219
|
+
} catch (UnavailableDeviceNotCompatibleException e) {
|
|
220
|
+
cleanupSession();
|
|
221
|
+
call.reject("This device is not compatible with ARCore.");
|
|
222
|
+
} catch (Exception e) {
|
|
223
|
+
cleanupSession();
|
|
224
|
+
call.reject("Unable to start ARCore DBH: " + safeMessage(e));
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
@PluginMethod
|
|
230
|
+
public void captureDbhSweep(PluginCall call) {
|
|
231
|
+
if (session == null || !sessionResumed || surfaceView == null) {
|
|
232
|
+
call.reject("ARCore DBH session is not running.");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
Boolean retainFrames = call.getBoolean("retainFrames", false);
|
|
237
|
+
if (Boolean.TRUE.equals(retainFrames)) {
|
|
238
|
+
call.reject("This plugin deliberately does not retain sweep frames.");
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
int durationMs = intOption(call, "durationMs", 3000, 1000, 8000);
|
|
243
|
+
int requestedMin = intOption(call, "minObservations", 5, 3, 20);
|
|
244
|
+
int requestedMax = intOption(call, "maxObservations", 10, requestedMin, 30);
|
|
245
|
+
|
|
246
|
+
synchronized (sweepLock) {
|
|
247
|
+
if (pendingSweepCall != null) {
|
|
248
|
+
call.reject("A DBH sweep is already in progress.");
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
pendingSweepCall = call;
|
|
253
|
+
sweepObservations.clear();
|
|
254
|
+
minObservations = requestedMin;
|
|
255
|
+
maxObservations = requestedMax;
|
|
256
|
+
sampleIntervalMs = Math.max(100L, durationMs / Math.max(1, requestedMax * 2));
|
|
257
|
+
long now = System.currentTimeMillis();
|
|
258
|
+
sweepCollectionStartsAtMs = now + WARMUP_MS;
|
|
259
|
+
sweepEndsAtMs = sweepCollectionStartsAtMs + durationMs;
|
|
260
|
+
lastSampleAtMs = 0L;
|
|
261
|
+
sweepActive = true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
updateInstruction("Use the RFID/NFC tag to locate 1.3 m • aim at bark beside the tag");
|
|
265
|
+
mainHandler.postDelayed(
|
|
266
|
+
() -> updateInstruction("Stand 1–1.5 m away • move slowly sideways • keep crosshair on bark"),
|
|
267
|
+
WARMUP_MS);
|
|
268
|
+
mainHandler.postDelayed(
|
|
269
|
+
() -> finishSweep(false),
|
|
270
|
+
WARMUP_MS + durationMs + 150L);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
@PluginMethod
|
|
274
|
+
public void stopDbhSession(PluginCall call) {
|
|
275
|
+
getActivity().runOnUiThread(() -> {
|
|
276
|
+
finishSweep(true);
|
|
277
|
+
cleanupSession();
|
|
278
|
+
call.resolve();
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private void createNativeCameraOverlay() {
|
|
283
|
+
if (session == null) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
FrameLayout root = getActivity().findViewById(android.R.id.content);
|
|
288
|
+
nativeOverlay = new FrameLayout(getActivity());
|
|
289
|
+
nativeOverlay.setBackgroundColor(Color.BLACK);
|
|
290
|
+
|
|
291
|
+
surfaceView = new GLSurfaceView(getActivity());
|
|
292
|
+
surfaceView.setEGLContextClientVersion(2);
|
|
293
|
+
surfaceView.setPreserveEGLContextOnPause(true);
|
|
294
|
+
renderer = new ArCameraRenderer(session, new ArCameraRenderer.Callback() {
|
|
295
|
+
@Override
|
|
296
|
+
public void onRendererReady(int textureId) {
|
|
297
|
+
mainHandler.post(() -> {
|
|
298
|
+
if (session == null) {
|
|
299
|
+
rejectPendingStart("ARCore session was closed before the camera became ready.");
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
session.setCameraTextureName(textureId);
|
|
304
|
+
updateDisplayGeometry();
|
|
305
|
+
session.resume();
|
|
306
|
+
sessionResumed = true;
|
|
307
|
+
surfaceView.onResume();
|
|
308
|
+
updateInstruction("Use the RFID/NFC tag to locate 1.3 m • aim at bark beside the tag");
|
|
309
|
+
|
|
310
|
+
PluginCall start = pendingStartCall;
|
|
311
|
+
pendingStartCall = null;
|
|
312
|
+
if (start != null) {
|
|
313
|
+
JSObject result = new JSObject();
|
|
314
|
+
result.put("started", true);
|
|
315
|
+
result.put("message", "ARCore Depth session started at the RFID/NFC tag height.");
|
|
316
|
+
start.resolve(result);
|
|
317
|
+
}
|
|
318
|
+
} catch (CameraNotAvailableException e) {
|
|
319
|
+
rejectPendingStart("Camera is not available for ARCore DBH.");
|
|
320
|
+
cleanupSession();
|
|
321
|
+
} catch (Exception e) {
|
|
322
|
+
rejectPendingStart("Unable to resume ARCore DBH: " + safeMessage(e));
|
|
323
|
+
cleanupSession();
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
@Override
|
|
329
|
+
public void onFrame(Frame frame, int viewWidth, int viewHeight) {
|
|
330
|
+
handleArFrame(frame, viewWidth, viewHeight);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
@Override
|
|
334
|
+
public int getDisplayRotation() {
|
|
335
|
+
return currentDisplayRotation();
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
surfaceView.setRenderer(renderer);
|
|
339
|
+
surfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
|
|
340
|
+
|
|
341
|
+
nativeOverlay.addView(surfaceView, new FrameLayout.LayoutParams(
|
|
342
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
343
|
+
ViewGroup.LayoutParams.MATCH_PARENT));
|
|
344
|
+
|
|
345
|
+
addCrosshair(nativeOverlay);
|
|
346
|
+
instructionText = createInstructionText();
|
|
347
|
+
FrameLayout.LayoutParams instructions = new FrameLayout.LayoutParams(
|
|
348
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
349
|
+
ViewGroup.LayoutParams.WRAP_CONTENT);
|
|
350
|
+
instructions.gravity = Gravity.TOP;
|
|
351
|
+
instructions.setMargins(dp(20), dp(34), dp(20), 0);
|
|
352
|
+
nativeOverlay.addView(instructionText, instructions);
|
|
353
|
+
|
|
354
|
+
root.addView(nativeOverlay, new FrameLayout.LayoutParams(
|
|
355
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
356
|
+
ViewGroup.LayoutParams.MATCH_PARENT));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private void handleArFrame(Frame frame, int viewWidth, int viewHeight) {
|
|
360
|
+
if (!sweepActive) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
long now = System.currentTimeMillis();
|
|
365
|
+
if (now < sweepCollectionStartsAtMs || now > sweepEndsAtMs) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (lastSampleAtMs > 0 && now - lastSampleAtMs < sampleIntervalMs) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
lastSampleAtMs = now;
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
Camera camera = frame.getCamera();
|
|
375
|
+
DepthObservation observation = sampler.sample(frame, camera, viewWidth, viewHeight);
|
|
376
|
+
if (observation == null) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
boolean reachedMax = false;
|
|
381
|
+
synchronized (sweepLock) {
|
|
382
|
+
if (!sweepActive || pendingSweepCall == null) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
sweepObservations.add(observation);
|
|
386
|
+
reachedMax = sweepObservations.size() >= maxObservations;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (reachedMax) {
|
|
390
|
+
mainHandler.post(() -> finishSweep(false));
|
|
391
|
+
}
|
|
392
|
+
} catch (Exception ignored) {
|
|
393
|
+
// Invalid frames are simply skipped; the app's aggregate QA rejects
|
|
394
|
+
// a sweep with too few valid observations.
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private void finishSweep(boolean stoppedByCaller) {
|
|
399
|
+
PluginCall sweepCall;
|
|
400
|
+
List<DepthObservation> observations;
|
|
401
|
+
synchronized (sweepLock) {
|
|
402
|
+
sweepCall = pendingSweepCall;
|
|
403
|
+
if (sweepCall == null) {
|
|
404
|
+
sweepActive = false;
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
pendingSweepCall = null;
|
|
408
|
+
sweepActive = false;
|
|
409
|
+
observations = new ArrayList<>(sweepObservations);
|
|
410
|
+
sweepObservations.clear();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
JSArray observationArray = new JSArray();
|
|
414
|
+
for (DepthObservation observation : observations) {
|
|
415
|
+
observationArray.put(observation.toJsObject());
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
JSObject result = new JSObject();
|
|
419
|
+
result.put("observations", observationArray);
|
|
420
|
+
result.put("deviceModel", Build.MANUFACTURER + " " + Build.MODEL);
|
|
421
|
+
result.put("arcoreVersion", installedArcoreVersion());
|
|
422
|
+
result.put("minimumRequested", minObservations);
|
|
423
|
+
result.put("stoppedByCaller", stoppedByCaller);
|
|
424
|
+
sweepCall.resolve(result);
|
|
425
|
+
|
|
426
|
+
if (!stoppedByCaller) {
|
|
427
|
+
updateInstruction(observations.size() >= minObservations
|
|
428
|
+
? "Depth sweep complete"
|
|
429
|
+
: "Depth sweep complete • too few valid observations");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private void cleanupSession() {
|
|
434
|
+
sweepActive = false;
|
|
435
|
+
|
|
436
|
+
if (surfaceView != null) {
|
|
437
|
+
try {
|
|
438
|
+
surfaceView.onPause();
|
|
439
|
+
} catch (Exception ignored) {
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (session != null) {
|
|
444
|
+
try {
|
|
445
|
+
if (sessionResumed) {
|
|
446
|
+
session.pause();
|
|
447
|
+
}
|
|
448
|
+
} catch (Exception ignored) {
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
session.close();
|
|
452
|
+
} catch (Exception ignored) {
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
sessionResumed = false;
|
|
456
|
+
session = null;
|
|
457
|
+
renderer = null;
|
|
458
|
+
|
|
459
|
+
if (nativeOverlay != null) {
|
|
460
|
+
ViewGroup parent = (ViewGroup) nativeOverlay.getParent();
|
|
461
|
+
if (parent != null) {
|
|
462
|
+
parent.removeView(nativeOverlay);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
nativeOverlay = null;
|
|
466
|
+
surfaceView = null;
|
|
467
|
+
instructionText = null;
|
|
468
|
+
|
|
469
|
+
if (pendingStartCall != null) {
|
|
470
|
+
rejectPendingStart("ARCore DBH session was stopped.");
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private void rejectPendingStart(String message) {
|
|
475
|
+
PluginCall start = pendingStartCall;
|
|
476
|
+
pendingStartCall = null;
|
|
477
|
+
if (start != null) {
|
|
478
|
+
start.reject(message);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private boolean hasCameraPermission() {
|
|
483
|
+
return getPermissionState("camera") == PermissionState.GRANTED;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private int currentDisplayRotation() {
|
|
487
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && getActivity().getDisplay() != null) {
|
|
488
|
+
return getActivity().getDisplay().getRotation();
|
|
489
|
+
}
|
|
490
|
+
return getActivity().getWindowManager().getDefaultDisplay().getRotation();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private void updateDisplayGeometry() {
|
|
494
|
+
if (session == null || surfaceView == null) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
int width = Math.max(1, surfaceView.getWidth());
|
|
498
|
+
int height = Math.max(1, surfaceView.getHeight());
|
|
499
|
+
session.setDisplayGeometry(currentDisplayRotation(), width, height);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private TextView createInstructionText() {
|
|
503
|
+
TextView text = new TextView(getActivity());
|
|
504
|
+
text.setTextColor(Color.WHITE);
|
|
505
|
+
text.setTextSize(16f);
|
|
506
|
+
text.setGravity(Gravity.CENTER);
|
|
507
|
+
text.setPadding(dp(12), dp(10), dp(12), dp(10));
|
|
508
|
+
text.setBackgroundColor(0x99000000);
|
|
509
|
+
return text;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private void updateInstruction(String message) {
|
|
513
|
+
mainHandler.post(() -> {
|
|
514
|
+
if (instructionText != null) {
|
|
515
|
+
instructionText.setText(message);
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private void addCrosshair(FrameLayout parent) {
|
|
521
|
+
View horizontal = new View(getActivity());
|
|
522
|
+
horizontal.setBackgroundColor(Color.WHITE);
|
|
523
|
+
FrameLayout.LayoutParams h = new FrameLayout.LayoutParams(dp(64), dp(2));
|
|
524
|
+
h.gravity = Gravity.CENTER;
|
|
525
|
+
parent.addView(horizontal, h);
|
|
526
|
+
|
|
527
|
+
View vertical = new View(getActivity());
|
|
528
|
+
vertical.setBackgroundColor(Color.WHITE);
|
|
529
|
+
FrameLayout.LayoutParams v = new FrameLayout.LayoutParams(dp(2), dp(64));
|
|
530
|
+
v.gravity = Gravity.CENTER;
|
|
531
|
+
parent.addView(vertical, v);
|
|
532
|
+
|
|
533
|
+
View center = new View(getActivity());
|
|
534
|
+
center.setBackgroundColor(0x00000000);
|
|
535
|
+
FrameLayout.LayoutParams c = new FrameLayout.LayoutParams(dp(18), dp(18));
|
|
536
|
+
c.gravity = Gravity.CENTER;
|
|
537
|
+
center.setBackgroundResource(android.R.drawable.presence_online);
|
|
538
|
+
parent.addView(center, c);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
private int dp(int value) {
|
|
542
|
+
float density = getActivity().getResources().getDisplayMetrics().density;
|
|
543
|
+
return Math.round(value * density);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
private String installedArcoreVersion() {
|
|
547
|
+
try {
|
|
548
|
+
return getActivity()
|
|
549
|
+
.getPackageManager()
|
|
550
|
+
.getPackageInfo("com.google.ar.core", 0)
|
|
551
|
+
.versionName;
|
|
552
|
+
} catch (Exception e) {
|
|
553
|
+
return "unknown";
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private static int intOption(PluginCall call, String key, int defaultValue, int min, int max) {
|
|
558
|
+
Integer value = call.getInt(key, defaultValue);
|
|
559
|
+
int resolved = value == null ? defaultValue : value;
|
|
560
|
+
return Math.max(min, Math.min(max, resolved));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private static String safeMessage(Exception e) {
|
|
564
|
+
String message = e.getMessage();
|
|
565
|
+
return message == null || message.trim().isEmpty() ? e.getClass().getSimpleName() : message;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type ArcoreRangeSource = 'arcore_depth16' | 'arcore_raw_depth' | 'arcore_surface_hit';
|
|
2
|
+
export type BoundaryConfidence = 'high' | 'medium' | 'low';
|
|
3
|
+
export interface SupportResult {
|
|
4
|
+
isSupported: boolean;
|
|
5
|
+
hasDepthApi: boolean;
|
|
6
|
+
pluginVersion?: string;
|
|
7
|
+
message?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface StartDbhSessionOptions {
|
|
10
|
+
/** RFID/NFC tree tag is the field anchor that identifies the 1.3 m DBH plane. */
|
|
11
|
+
anchorType?: 'rfid_tag';
|
|
12
|
+
targetHeightM: 1.3;
|
|
13
|
+
/** @deprecated Ignored from v0.1.2; retained only for backward compatibility. */
|
|
14
|
+
markerDictionary?: 'DICT_6X6_250';
|
|
15
|
+
/** @deprecated Ignored from v0.1.2; retained only for backward compatibility. */
|
|
16
|
+
markerId?: 0;
|
|
17
|
+
/** @deprecated Ignored from v0.1.2; retained only for backward compatibility. */
|
|
18
|
+
markerSizeCm?: 10;
|
|
19
|
+
}
|
|
20
|
+
export interface StartDbhSessionResult {
|
|
21
|
+
started: boolean;
|
|
22
|
+
installationRequested?: boolean;
|
|
23
|
+
message?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface CaptureDbhSweepOptions {
|
|
26
|
+
durationMs: number;
|
|
27
|
+
minObservations: number;
|
|
28
|
+
maxObservations: number;
|
|
29
|
+
retainFrames: false;
|
|
30
|
+
}
|
|
31
|
+
export interface DbhObservation {
|
|
32
|
+
frontSurfaceDepthM: number;
|
|
33
|
+
/** Effective focal length in pixels along the measured trunk-width axis. */
|
|
34
|
+
focalLengthXPx: number;
|
|
35
|
+
/** Trunk tangent-to-tangent apparent width in corresponding image pixels. */
|
|
36
|
+
trunkWidthPx: number;
|
|
37
|
+
rangeSource: ArcoreRangeSource;
|
|
38
|
+
trackingConfidence?: number;
|
|
39
|
+
boundaryConfidence?: BoundaryConfidence;
|
|
40
|
+
depthMadM?: number;
|
|
41
|
+
timestampMs?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface CaptureDbhSweepResult {
|
|
44
|
+
observations: DbhObservation[];
|
|
45
|
+
deviceModel?: string;
|
|
46
|
+
arcoreVersion?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface DrawDownArCoreDepthPlugin {
|
|
49
|
+
checkSupport(): Promise<SupportResult>;
|
|
50
|
+
startDbhSession(options: StartDbhSessionOptions): Promise<StartDbhSessionResult>;
|
|
51
|
+
captureDbhSweep(options: CaptureDbhSweepOptions): Promise<CaptureDbhSweepResult>;
|
|
52
|
+
stopDbhSession(): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=definitions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG,kBAAkB,GAAG,oBAAoB,CAAC;AAC7F,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;AAE3D,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,gBAAgB,EAAE,cAAc,CAAC;IACjC,QAAQ,EAAE,CAAC,CAAC;IACZ,YAAY,EAAE,EAAE,CAAC;IACjB,aAAa,EAAE,GAAG,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,KAAK,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,4EAA4E;IAC5E,cAAc,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,iBAAiB,CAAC;IAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,cAAc,EAAE,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,yBAAyB;IACxC,YAAY,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;IACvC,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACjF,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACjF,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAE/D,QAAA,MAAM,mBAAmB,2BAEvB,CAAC;AAEH,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,mBAAmB,EAAE,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { registerPlugin } from '@capacitor/core';
|
|
2
|
+
const DrawDownArCoreDepth = registerPlugin('DrawDownArCoreDepth', {
|
|
3
|
+
web: () => import('./web').then((m) => new m.DrawDownArCoreDepthWeb()),
|
|
4
|
+
});
|
|
5
|
+
export * from './definitions';
|
|
6
|
+
export { DrawDownArCoreDepth };
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGjD,MAAM,mBAAmB,GAAG,cAAc,CAA4B,qBAAqB,EAAE;IAC3F,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,sBAAsB,EAAE,CAAC;CACvE,CAAC,CAAC;AAEH,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,mBAAmB,EAAE,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { WebPlugin } from '@capacitor/core';
|
|
2
|
+
import type { CaptureDbhSweepOptions, CaptureDbhSweepResult, DrawDownArCoreDepthPlugin, StartDbhSessionOptions, StartDbhSessionResult, SupportResult } from './definitions';
|
|
3
|
+
export declare class DrawDownArCoreDepthWeb extends WebPlugin implements DrawDownArCoreDepthPlugin {
|
|
4
|
+
checkSupport(): Promise<SupportResult>;
|
|
5
|
+
startDbhSession(_options: StartDbhSessionOptions): Promise<StartDbhSessionResult>;
|
|
6
|
+
captureDbhSweep(_options: CaptureDbhSweepOptions): Promise<CaptureDbhSweepResult>;
|
|
7
|
+
stopDbhSession(): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=web.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,EACV,sBAAsB,EACtB,qBAAqB,EACrB,yBAAyB,EACzB,sBAAsB,EACtB,qBAAqB,EACrB,aAAa,EACd,MAAM,eAAe,CAAC;AAEvB,qBAAa,sBAAuB,SAAQ,SAAU,YAAW,yBAAyB;IAClF,YAAY,IAAI,OAAO,CAAC,aAAa,CAAC;IAStC,eAAe,CAAC,QAAQ,EAAE,sBAAsB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAIjF,eAAe,CAAC,QAAQ,EAAE,sBAAsB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAIjF,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;CAGtC"}
|