@reearth/core 0.0.7-alpha.13 → 0.0.7-alpha.15

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 (39) hide show
  1. package/dist/core.js +6864 -5606
  2. package/dist/core.umd.cjs +71 -71
  3. package/dist/index.d.ts +56 -13
  4. package/package.json +8 -5
  5. package/src/Map/Layer/hooks.ts +6 -3
  6. package/src/Map/Layer/index.tsx +2 -0
  7. package/src/Map/Layers/hooks.ts +17 -0
  8. package/src/Map/Layers/index.tsx +12 -1
  9. package/src/Map/Layers/keys.ts +1 -0
  10. package/src/Map/Sketch/hooks.ts +405 -399
  11. package/src/Map/Sketch/index.tsx +65 -18
  12. package/src/Map/Sketch/sketchMachine.ts +359 -4
  13. package/src/Map/Sketch/sketchMachine.typegen.ts +58 -1
  14. package/src/Map/Sketch/types.ts +10 -20
  15. package/src/Map/Sketch/usePluginSketchLayer.ts +105 -0
  16. package/src/Map/Sketch/useSketch.ts +559 -0
  17. package/src/Map/Sketch/useSketchFeature.ts +198 -0
  18. package/src/Map/hooks.ts +32 -1
  19. package/src/Map/index.tsx +24 -0
  20. package/src/Map/ref.ts +8 -0
  21. package/src/Map/types/index.ts +21 -1
  22. package/src/Map/types/viewerProperty.ts +2 -0
  23. package/src/Visualizer/coreContext.tsx +2 -0
  24. package/src/Visualizer/hooks.ts +25 -0
  25. package/src/Visualizer/index.tsx +13 -0
  26. package/src/engines/Cesium/Feature/index.tsx +6 -2
  27. package/src/engines/Cesium/Sketch/ControlPoint.tsx +128 -24
  28. package/src/engines/Cesium/Sketch/ExtrudedControlPoints.tsx +70 -25
  29. package/src/engines/Cesium/Sketch/ExtrudedMeasurement.tsx +3 -1
  30. package/src/engines/Cesium/Sketch/ExtrudedPolygonEntity.tsx +14 -14
  31. package/src/engines/Cesium/Sketch/PolylineEntity.tsx +7 -4
  32. package/src/engines/Cesium/Sketch/SurfaceAddingPoints.tsx +60 -0
  33. package/src/engines/Cesium/Sketch/SurfaceControlPoints.tsx +125 -35
  34. package/src/engines/Cesium/Sketch/constants.ts +5 -0
  35. package/src/engines/Cesium/Sketch/index.tsx +68 -29
  36. package/src/engines/Cesium/core/labels/JapanGSIOptimalBVmapVectorMapLabel/JapanGSIOptimalBVmapLabelImagery.tsx +8 -1
  37. package/src/engines/Cesium/core/labels/JapanGSIOptimalBVmapVectorMapLabel/JapanGSIOptimalBVmapVectorMapLabel.tsx +14 -2
  38. package/src/engines/Cesium/core/labels/LabelImageryLayers.tsx +10 -1
  39. package/src/engines/Cesium/hooks/useEngineRef.ts +36 -0
@@ -8,22 +8,16 @@ import {
8
8
  useCallback,
9
9
  useEffect,
10
10
  useImperativeHandle,
11
+ useMemo,
11
12
  useRef,
12
13
  useState,
13
14
  } from "react";
14
- import invariant from "tiny-invariant";
15
15
  import { v4 as uuidv4 } from "uuid";
16
+ import { InterpreterFrom, StateFrom } from "xstate";
16
17
 
18
+ import { ControlPointMouseEventHandler } from "../../engines/Cesium/Sketch";
17
19
  import { InteractionModeType } from "../../Visualizer/interactionMode";
18
- import { LayerSimple, LazyLayer } from "../Layers";
19
- import {
20
- Feature,
21
- EngineRef,
22
- LayersRef,
23
- MouseEventCallback,
24
- MouseEventProps,
25
- SketchRef,
26
- } from "../types";
20
+ import { Feature, EngineRef, LayersRef, SketchRef } from "../types";
27
21
  import { useGet } from "../utils";
28
22
 
29
23
  import { PRESET_APPEARANCE, PRESET_COLOR } from "./preset";
@@ -34,7 +28,12 @@ import {
34
28
  SketchFeature,
35
29
  SketchEventProps,
36
30
  SketchOptions,
31
+ SketchEditingFeature,
32
+ SketchEditFeatureChangeCb,
37
33
  } from "./types";
34
+ import usePluginSketchLayer from "./usePluginSketchLayer";
35
+ import useSketch from "./useSketch";
36
+ import useSketchFeature from "./useSketchFeature";
38
37
  import { useWindowEvent } from "./utils";
39
38
 
40
39
  import { OnLayerSelectType } from ".";
@@ -53,14 +52,22 @@ type Props = {
53
52
  onSketchTypeChange?: (type: SketchType | undefined, from?: "editor" | "plugin") => void;
54
53
  onSketchFeatureCreate?: (feature: SketchFeature | null) => void;
55
54
  onSketchPluginFeatureCreate?: (props: SketchEventProps) => void;
55
+ onSketchFeatureUpdate?: (feature: SketchFeature) => void;
56
+ onSketchPluginFeatureUpdate?: (props: SketchEventProps) => void;
57
+ onSketchFeatureDelete?: (layerId: string, featureId: string) => void;
58
+ onSketchPluginFeatureDelete?: (props: { layerId: string; featureId: string }) => void;
56
59
  onLayerSelect?: OnLayerSelectType;
60
+ sketchEditingFeature?: SketchEditingFeature;
61
+ onSketchEditFeature?: (feature: SketchEditingFeature | undefined) => void;
62
+ onMount?: () => void;
57
63
  };
58
64
 
59
- const PLUGIN_LAYER_ID_LENGTH = 36;
60
-
61
65
  const sketchMachine = createSketchMachine();
62
66
 
63
- export default function useHooks({
67
+ export type sketchState = StateFrom<typeof sketchMachine>;
68
+ export type SketchInterpreter = InterpreterFrom<typeof sketchMachine>;
69
+
70
+ export default function ({
64
71
  ref,
65
72
  engineRef,
66
73
  layersRef,
@@ -69,11 +76,24 @@ export default function useHooks({
69
76
  onSketchTypeChange,
70
77
  onSketchFeatureCreate,
71
78
  onSketchPluginFeatureCreate,
79
+ onSketchFeatureUpdate,
80
+ onSketchPluginFeatureUpdate,
81
+ onSketchFeatureDelete,
82
+ onSketchPluginFeatureDelete,
72
83
  onLayerSelect,
84
+ sketchEditingFeature,
85
+ onSketchEditFeature,
86
+ onMount,
73
87
  }: Props) {
74
88
  const [state, send] = useMachine(sketchMachine);
75
89
  const [type, updateType] = useState<SketchType | undefined>();
76
90
  const [from, updateFrom] = useState<"editor" | "plugin">("editor");
91
+
92
+ const setType = useCallback((type: SketchType | undefined, from?: "editor" | "plugin") => {
93
+ updateType(type);
94
+ updateFrom(from ?? "editor");
95
+ }, []);
96
+
77
97
  const [disableInteraction, setDisableInteraction] = useState(false);
78
98
 
79
99
  const [sketchOptions, setSketchOptions] = useState<SketchOptions>({
@@ -81,9 +101,10 @@ export default function useHooks({
81
101
  appearance: PRESET_APPEARANCE,
82
102
  dataOnly: false,
83
103
  disableShadow: false,
84
- enableRelativeHeight: false,
85
104
  rightClickToAbort: true,
86
105
  autoResetInteractionMode: true,
106
+ // NOTE: Centroid extrude is not finalized yet
107
+ useCentroidExtrudedHeight: false,
87
108
  });
88
109
 
89
110
  const overrideOptions = useCallback((options: SketchOptions) => {
@@ -96,13 +117,16 @@ export default function useHooks({
96
117
 
97
118
  const [geometryOptions, setGeometryOptions] = useState<GeometryOptionsXYZ | null>(null);
98
119
  const [extrudedHeight, setExtrudedHeight] = useState(0);
120
+ const [extrudedPoint, setExtrudedPoint] = useState<Position3d | undefined>();
121
+
122
+ const [centroidBasePoint, setCentroidBasePoint] = useState<Position3d | undefined>();
123
+ const [centroidExtrudedPoint, setCentroidExtrudedPoint] = useState<Position3d | undefined>();
124
+
125
+ const [selectedControlPointIndex, setSelectedControlPointIndex] = useState<number | undefined>();
99
126
  const markerGeometryRef = useRef<GeometryOptionsXYZ | null>(null);
100
127
  const pointerLocationRef = useRef<[lng: number, lat: number, height: number]>();
101
128
 
102
- const setType = useCallback((type: SketchType | undefined, from?: "editor" | "plugin") => {
103
- updateType(type);
104
- updateFrom(from ?? "editor");
105
- }, []);
129
+ const isEditing = useMemo(() => state.matches("editing"), [state]);
106
130
 
107
131
  const createFeature = useCallback(() => {
108
132
  const geoOptions = type === "marker" ? markerGeometryRef.current : geometryOptions;
@@ -121,9 +145,26 @@ export default function useHooks({
121
145
  });
122
146
  }, [extrudedHeight, geometryOptions, markerGeometryRef, type, engineRef]);
123
147
 
148
+ const updateFeature = useCallback(() => {
149
+ if (geometryOptions == null || !selectedFeature?.id) {
150
+ return null;
151
+ }
152
+ const geometry = engineRef.current?.createGeometry(geometryOptions);
153
+ if (geometry == null) {
154
+ return null;
155
+ }
156
+ return feature(geometry, {
157
+ id: selectedFeature?.id,
158
+ type: geometryOptions?.type,
159
+ positions: geometryOptions?.controlPoints,
160
+ extrudedHeight,
161
+ });
162
+ }, [extrudedHeight, geometryOptions, selectedFeature, engineRef]);
163
+
124
164
  const updateGeometryOptions = useCallback(
125
165
  (controlPoint?: Position3d) => {
126
166
  setExtrudedHeight(0);
167
+ setExtrudedPoint(undefined);
127
168
  if (state.context.type == null || state.context.controlPoints == null) {
128
169
  setGeometryOptions(null);
129
170
  return;
@@ -139,396 +180,155 @@ export default function useHooks({
139
180
  [state, setGeometryOptions, setExtrudedHeight],
140
181
  );
141
182
 
142
- const pluginSketchLayerCreate = useCallback(
143
- (feature: SketchFeature) => {
144
- const newLayer = layersRef.current?.add({
145
- type: "simple",
146
- data: {
147
- type: "geojson",
148
- isSketchLayer: true,
149
- value: {
150
- type: "FeatureCollection",
151
- features: [{ ...feature, id: feature.properties.id }],
152
- },
153
- },
154
- ...sketchOptions.appearance,
155
- });
156
- return { layerId: newLayer?.id, featureId: feature.properties.id };
157
- },
158
- [layersRef, sketchOptions.appearance],
159
- );
183
+ const updateGeometryOptionsRef = useRef(updateGeometryOptions);
184
+ updateGeometryOptionsRef.current = updateGeometryOptions;
160
185
 
161
- const pluginSketchLayerFeatureAdd = useCallback(
162
- (layer: LazyLayer, feature: SketchFeature) => {
163
- if (layer.type !== "simple") return {};
164
- layersRef.current?.override(layer.id, {
165
- data: {
166
- ...layer.data,
167
- type: "geojson",
168
- value: {
169
- type: "FeatureCollection",
170
- features: [
171
- ...((layer.computed?.layer as LayerSimple)?.data?.value?.features ?? []),
172
- { ...feature, id: feature.properties.id },
173
- ],
174
- },
175
- },
176
- });
177
- return { layerId: layer.id, featureId: feature.properties.id };
178
- },
179
- [layersRef],
180
- );
186
+ const updateCentroidPoints = useCallback(
187
+ async (controlPoints: Position3d[]) => {
188
+ const newExtrudeBasePoint = await getCentroid(controlPoints, engineRef);
189
+ setCentroidBasePoint(newExtrudeBasePoint);
181
190
 
182
- const pluginSketchLayerFeatureRemove = useCallback(
183
- (layer: LazyLayer, featureId: string) => {
184
- if (layer.type !== "simple" || layer.computed?.layer.type !== "simple") return;
185
- layersRef.current?.override(layer.id, {
186
- data: {
187
- ...layer.data,
188
- type: "geojson",
189
- value: {
190
- type: "FeatureCollection",
191
- features: [
192
- ...(layer.computed?.layer?.data?.value?.features ?? []).filter(
193
- (feature: GeojsonFeature) => feature.id !== featureId,
194
- ),
195
- ],
196
- },
197
- },
198
- });
191
+ if (!newExtrudeBasePoint) return;
192
+ const centroidExtrudedPoint = engineRef.current?.getExtrudedPoint(
193
+ newExtrudeBasePoint,
194
+ extrudedHeight,
195
+ );
196
+ setCentroidExtrudedPoint(centroidExtrudedPoint);
199
197
  },
200
- [layersRef],
198
+ [engineRef, extrudedHeight],
201
199
  );
202
200
 
203
- const handleFeatureCreate = useCallback(
204
- (feature: SketchFeature) => {
205
- if (sketchOptions.autoResetInteractionMode) {
206
- updateType(undefined);
207
- }
201
+ const {
202
+ pluginSketchLayerCreate,
203
+ pluginSketchLayerFeatureAdd,
204
+ pluginSketchLayerFeatureUpdate,
205
+ pluginSketchLayerFeatureRemove,
206
+ } = usePluginSketchLayer({
207
+ layersRef,
208
+ sketchOptions,
209
+ });
208
210
 
209
- if (from === "editor") {
210
- onSketchFeatureCreate?.(feature);
211
- return;
212
- }
211
+ const { handleFeatureCreate, handleFeatureUpdate, handleFeatureDelete } = useSketchFeature({
212
+ layersRef,
213
+ sketchOptions,
214
+ from,
215
+ updateType,
216
+ onSketchFeatureCreate,
217
+ pluginSketchLayerCreate,
218
+ pluginSketchLayerFeatureAdd,
219
+ pluginSketchLayerFeatureUpdate,
220
+ pluginSketchLayerFeatureRemove,
221
+ onSketchPluginFeatureCreate,
222
+ onSketchPluginFeatureUpdate,
223
+ onSketchPluginFeatureDelete,
224
+ onSketchFeatureUpdate,
225
+ onSketchFeatureDelete,
226
+ onLayerSelect,
227
+ });
213
228
 
214
- if (!sketchOptions.dataOnly) {
215
- const selectedLayer = layersRef.current?.selectedLayer();
216
- const { layerId, featureId } =
217
- selectedLayer?.id?.length !== PLUGIN_LAYER_ID_LENGTH ||
218
- selectedLayer.type !== "simple" ||
219
- selectedLayer.computed?.layer.type !== "simple"
220
- ? pluginSketchLayerCreate(feature)
221
- : pluginSketchLayerFeatureAdd(selectedLayer, feature);
222
-
223
- if (layerId && featureId) {
224
- requestAnimationFrame(() => {
225
- onLayerSelect?.(
226
- layerId,
227
- featureId,
228
- layerId
229
- ? () =>
230
- new Promise(resolve => {
231
- // Wait until computed feature is ready
232
- queueMicrotask(() => {
233
- resolve(layersRef.current?.findById?.(layerId)?.computed);
234
- });
235
- })
236
- : undefined,
237
- undefined,
238
- undefined,
239
- );
240
- });
241
- onSketchPluginFeatureCreate?.({ layerId, featureId, feature });
242
- }
243
- } else {
244
- onSketchPluginFeatureCreate?.({ feature });
245
- }
246
- },
247
- [
248
- layersRef,
249
- from,
250
- sketchOptions.dataOnly,
251
- sketchOptions.autoResetInteractionMode,
252
- pluginSketchLayerCreate,
253
- pluginSketchLayerFeatureAdd,
254
- onSketchFeatureCreate,
255
- onSketchPluginFeatureCreate,
256
- onLayerSelect,
257
- ],
258
- );
229
+ const editFeature = useCallback(
230
+ (feature: SketchEditingFeature | undefined) => {
231
+ onSketchEditFeature?.(feature);
259
232
 
260
- const handleLeftDown = useCallback(
261
- (props: MouseEventProps) => {
262
- if (
263
- disableInteraction ||
264
- !type ||
265
- props.lng === undefined ||
266
- props.lat === undefined ||
267
- props.height === undefined ||
268
- props.x === undefined ||
269
- props.y === undefined
270
- ) {
271
- return;
272
- }
273
- if (!state.matches("idle")) {
274
- return;
275
- }
276
- invariant(state.context.lastControlPoint == null);
277
- const controlPoint = engineRef.current?.toXYZ(props.lng, props.lat, props.height);
278
- if (controlPoint == null) {
279
- return;
280
- }
233
+ if (!state.matches("idle") || !feature) return;
281
234
 
235
+ const type = feature?.feature?.properties?.type as SketchType;
282
236
  send({
283
237
  type: {
284
- marker: "MARKER" as const,
285
- polyline: "POLYLINE" as const,
286
- circle: "CIRCLE" as const,
287
- rectangle: "RECTANGLE" as const,
288
- polygon: "POLYGON" as const,
289
- extrudedCircle: "EXTRUDED_CIRCLE" as const,
290
- extrudedRectangle: "EXTRUDED_RECTANGLE" as const,
291
- extrudedPolygon: "EXTRUDED_POLYGON" as const,
238
+ marker: "EDIT_MARKER" as const,
239
+ polyline: "EDIT_POLYLINE" as const,
240
+ circle: "EDIT_CIRCLE" as const,
241
+ rectangle: "EDIT_RECTANGLE" as const,
242
+ polygon: "EDIT_POLYGON" as const,
243
+ extrudedCircle: "EDIT_EXTRUDED_CIRCLE" as const,
244
+ extrudedRectangle: "EDIT_EXTRUDED_RECTANGLE" as const,
245
+ extrudedPolygon: "EDIT_EXTRUDED_POLYGON" as const,
292
246
  }[type],
293
- pointerPosition: [props.x, props.y],
294
- controlPoint,
247
+ controlPoints: feature?.feature?.properties?.positions,
248
+ extrudedHeight: feature?.feature?.properties?.extrudedHeight,
295
249
  });
296
- setGeometryOptions(null);
297
- markerGeometryRef.current = null;
298
- },
299
- [state, disableInteraction, type, engineRef, send],
300
- );
301
-
302
- const handleMouseMove = useCallback(
303
- (props: MouseEventProps) => {
304
- if (
305
- disableInteraction ||
306
- props.lng === undefined ||
307
- props.lat === undefined ||
308
- props.height === undefined ||
309
- props.x === undefined ||
310
- props.y === undefined ||
311
- !engineRef.current
312
- ) {
313
- return;
314
- }
315
- pointerLocationRef.current = [props.lng, props.lat, props.height];
316
- if (state.matches("drawing")) {
317
- invariant(state.context.type != null);
318
- invariant(state.context.controlPoints != null);
319
- const controlPoint = engineRef.current?.toXYZ(props.lng, props.lat, props.height);
320
- if (
321
- controlPoint == null ||
322
- hasDuplicate(engineRef.current.equalsEpsilon3d, controlPoint, state.context.controlPoints)
323
- ) {
324
- return;
325
- }
326
- updateGeometryOptions(controlPoint);
327
- } else if (state.matches("extruding")) {
328
- invariant(state.context.lastControlPoint != null);
329
- const extrudedHeight = engineRef.current?.getExtrudedHeight(
330
- state.context.lastControlPoint,
331
- [props.x, props.y],
250
+ setGeometryOptions({
251
+ type,
252
+ controlPoints: feature?.feature?.properties?.positions,
253
+ });
254
+ if (feature?.feature?.properties?.extrudedHeight) {
255
+ setExtrudedHeight(feature.feature.properties.extrudedHeight);
256
+ setExtrudedPoint(
257
+ engineRef.current?.getExtrudedPoint(
258
+ feature?.feature?.properties?.positions[
259
+ feature?.feature?.properties?.positions.length - 1
260
+ ],
261
+ feature.feature.properties.extrudedHeight,
262
+ ),
332
263
  );
333
- if (extrudedHeight != null) {
334
- setExtrudedHeight(extrudedHeight);
335
- }
336
264
  }
337
265
  },
338
- [disableInteraction, state, engineRef, updateGeometryOptions, setExtrudedHeight],
266
+ [engineRef, state, onSketchEditFeature, send],
339
267
  );
340
268
 
341
- const handleLeftUp = useCallback(
342
- (props: MouseEventProps) => {
343
- if (
344
- disableInteraction ||
345
- props.lng === undefined ||
346
- props.lat === undefined ||
347
- props.height === undefined ||
348
- props.x === undefined ||
349
- props.y === undefined ||
350
- !engineRef.current
351
- ) {
352
- return;
353
- }
354
- if (
355
- state.context.controlPoints?.length === 1 &&
356
- state.context.lastPointerPosition != null &&
357
- state.context.type !== "marker" &&
358
- engineRef.current?.equalsEpsilon2d(
359
- [props.x, props.y],
360
- state.context.lastPointerPosition,
361
- 0,
362
- 5,
363
- )
364
- ) {
365
- return; // Too close to the first position user clicked.
366
- }
367
-
368
- if (state.matches("drawing")) {
369
- const controlPoint = engineRef.current?.toXYZ(props.lng, props.lat, props.height);
370
- if (controlPoint == null) return;
371
-
372
- if (state.context.type === "marker") {
373
- markerGeometryRef.current = {
374
- type: state.context.type,
375
- controlPoints: [controlPoint],
376
- };
377
- const feature = createFeature();
378
- markerGeometryRef.current = null;
379
- if (feature == null) {
380
- return;
381
- }
382
- handleFeatureCreate(feature);
383
- send({ type: "CREATE" });
384
- setGeometryOptions(null);
385
- return;
386
- }
387
- if (
388
- hasDuplicate(
389
- engineRef.current?.equalsEpsilon3d,
390
- controlPoint,
391
- state.context.controlPoints,
392
- )
393
- ) {
394
- return;
395
- }
396
- if (
397
- state.context.type === "circle" ||
398
- (state.context.type === "rectangle" && state.context.controlPoints?.length === 2)
399
- ) {
400
- const feature = createFeature();
401
- if (feature == null) {
402
- return;
403
- }
404
- handleFeatureCreate(feature);
405
- send({ type: "CREATE" });
406
- setGeometryOptions(null);
407
- return;
408
- } else {
409
- if (props.x === undefined || props.y === undefined) return;
410
- send({
411
- type: "NEXT",
412
- pointerPosition: [props.x, props.y],
413
- controlPoint,
414
- });
415
- }
416
- } else if (state.matches("extruding")) {
417
- const feature = createFeature();
418
- if (feature == null) {
419
- return;
420
- }
421
- handleFeatureCreate(feature);
422
- send({ type: "CREATE" });
423
- setGeometryOptions(null);
269
+ const cancelEdit = useCallback(
270
+ (ignoreAutoReSelect?: boolean) => {
271
+ send({ type: "EXIT_EDIT" });
272
+ updateGeometryOptions(undefined);
273
+ onSketchEditFeature?.(undefined);
274
+ if (ignoreAutoReSelect) {
275
+ ignoreAutoReSelectRef.current = true;
424
276
  }
425
277
  },
426
- [
427
- disableInteraction,
428
- state,
429
- engineRef,
430
- send,
431
- setGeometryOptions,
432
- createFeature,
433
- handleFeatureCreate,
434
- ],
278
+ [onSketchEditFeature, send, updateGeometryOptions],
435
279
  );
436
280
 
437
- const handleDoubleClick = useCallback(
438
- (props: MouseEventProps) => {
439
- if (
440
- disableInteraction ||
441
- props.lng === undefined ||
442
- props.lat === undefined ||
443
- props.height === undefined ||
444
- props.x === undefined ||
445
- props.y === undefined
446
- ) {
447
- return;
448
- }
449
- if (state.matches("drawing.extrudedPolygon")) {
450
- const controlPoint = engineRef.current?.toXYZ(props.lng, props.lat, props.height);
451
- if (controlPoint == null) return;
452
- send({
453
- type: "EXTRUDE",
454
- pointerPosition: [props.x, props.y],
455
- controlPoint,
456
- });
457
- } else if (state.matches("drawing.polyline") || state.matches("drawing.polygon")) {
458
- const feature = createFeature();
459
- if (feature == null) {
460
- return;
461
- }
462
- handleFeatureCreate(feature);
463
- send({ type: "CREATE" });
464
- setGeometryOptions(null);
281
+ const applyEdit = useCallback(() => {
282
+ if (sketchEditingFeature) {
283
+ const feature = updateFeature();
284
+ if (feature) {
285
+ handleFeatureUpdate({ ...feature, id: feature.properties.id });
465
286
  }
466
- },
467
- [disableInteraction, state, engineRef, send, handleFeatureCreate, createFeature],
468
- );
469
-
470
- const handleRightClick = useCallback(() => {
471
- if (!sketchOptions.rightClickToAbort) {
472
- return;
473
- }
474
- if (type !== undefined) {
475
- updateType(undefined);
476
287
  }
477
- if (state.matches("idle")) return;
478
- send({ type: "ABORT" });
288
+ send({ type: "EXIT_EDIT" });
479
289
  updateGeometryOptions(undefined);
480
- }, [type, state, sketchOptions.rightClickToAbort, send, updateGeometryOptions]);
481
-
482
- const mouseDownEventRef = useRef<MouseEventCallback>(handleLeftDown);
483
- mouseDownEventRef.current = handleLeftDown;
484
- const mouseMoveEventRef = useRef<MouseEventCallback>(handleMouseMove);
485
- mouseMoveEventRef.current = handleMouseMove;
486
- const mouseUpEventRef = useRef<MouseEventCallback>(handleLeftUp);
487
- mouseUpEventRef.current = handleLeftUp;
488
- const mouseDoubleClickEventRef = useRef<MouseEventCallback>(handleDoubleClick);
489
- mouseDoubleClickEventRef.current = handleDoubleClick;
490
- const mouseRightClickEventRef = useRef<() => void>(handleRightClick);
491
- mouseRightClickEventRef.current = handleRightClick;
492
-
493
- const onMouseDown = useCallback(
494
- (props: MouseEventProps) => {
495
- mouseDownEventRef.current?.(props);
496
- },
497
- [mouseDownEventRef],
498
- );
499
-
500
- const onMouseMove = useCallback(
501
- (props: MouseEventProps) => {
502
- mouseMoveEventRef.current?.(props);
290
+ onSketchEditFeature?.(undefined);
291
+ }, [
292
+ sketchEditingFeature,
293
+ send,
294
+ updateGeometryOptions,
295
+ handleFeatureUpdate,
296
+ updateFeature,
297
+ onSketchEditFeature,
298
+ ]);
299
+
300
+ const deleteFeature = useCallback(
301
+ (layerId: string, featureId: string) => {
302
+ handleFeatureDelete(layerId, featureId);
503
303
  },
504
- [mouseMoveEventRef],
304
+ [handleFeatureDelete],
505
305
  );
506
306
 
507
- const onMouseUp = useCallback(
508
- (props: MouseEventProps) => {
509
- mouseUpEventRef.current?.(props);
510
- },
511
- [mouseUpEventRef],
512
- );
513
-
514
- const onMouseDoubleClick = useCallback(
515
- (props: MouseEventProps) => {
516
- mouseDoubleClickEventRef.current?.(props);
517
- },
518
- [mouseDoubleClickEventRef],
519
- );
520
-
521
- const onMouseRightClick = useCallback(() => {
522
- mouseRightClickEventRef.current?.();
523
- }, [mouseRightClickEventRef]);
524
-
525
- useEffect(() => {
526
- engineRef.current?.onMouseDown(onMouseDown);
527
- engineRef.current?.onMouseMove(onMouseMove);
528
- engineRef.current?.onMouseUp(onMouseUp);
529
- engineRef.current?.onDoubleClick(onMouseDoubleClick);
530
- engineRef.current?.onRightClick(onMouseRightClick);
531
- }, [engineRef, onMouseDown, onMouseMove, onMouseUp, onMouseDoubleClick, onMouseRightClick]);
307
+ useSketch({
308
+ state,
309
+ engineRef,
310
+ disableInteraction,
311
+ type,
312
+ updateType,
313
+ sketchEditingFeature,
314
+ setSelectedControlPointIndex,
315
+ send,
316
+ setGeometryOptions,
317
+ markerGeometryRef,
318
+ pointerLocationRef,
319
+ geometryOptions,
320
+ updateGeometryOptions,
321
+ extrudedHeight,
322
+ setExtrudedHeight,
323
+ setExtrudedPoint,
324
+ updateCentroidPoints,
325
+ createFeature,
326
+ handleFeatureCreate,
327
+ applyEdit,
328
+ cancelEdit,
329
+ isEditing,
330
+ sketchOptions,
331
+ });
532
332
 
533
333
  useWindowEvent("keydown", event => {
534
334
  if (type === undefined) return;
@@ -548,7 +348,7 @@ export default function useHooks({
548
348
  updateGeometryOptions(controlPoint);
549
349
  } else if (event.key === "Delete" && state.matches("idle") && selectedFeature?.id) {
550
350
  const selectedLayer = layersRef.current?.selectedLayer();
551
- if (selectedLayer?.id?.length === PLUGIN_LAYER_ID_LENGTH) {
351
+ if (selectedLayer && layersRef.current?.isTempLayer(selectedLayer?.id)) {
552
352
  pluginSketchLayerFeatureRemove(selectedLayer, selectedFeature.id);
553
353
  }
554
354
  }
@@ -564,11 +364,11 @@ export default function useHooks({
564
364
  });
565
365
 
566
366
  useEffect(() => {
567
- if (type === undefined) {
367
+ if (type === undefined && !sketchEditingFeature) {
568
368
  send({ type: "ABORT" });
569
- updateGeometryOptions(undefined);
369
+ updateGeometryOptionsRef.current?.(undefined);
570
370
  }
571
- }, [type, send, updateGeometryOptions]);
371
+ }, [type, sketchEditingFeature, send]);
572
372
 
573
373
  const fromRef = useRef(from);
574
374
  fromRef.current = from;
@@ -578,10 +378,108 @@ export default function useHooks({
578
378
  onSketchTypeChangeRef.current = onSketchTypeChange;
579
379
 
580
380
  useEffect(() => {
581
- overrideInteractionModeRef.current?.(type ? "sketch" : "default");
381
+ overrideInteractionModeRef.current?.(type || sketchEditingFeature ? "sketch" : "default");
382
+ }, [type, sketchEditingFeature]);
383
+
384
+ const isEditingRef = useRef(isEditing);
385
+ isEditingRef.current = isEditing;
386
+ const cancelEditRef = useRef(cancelEdit);
387
+ cancelEditRef.current = cancelEdit;
388
+
389
+ useEffect(() => {
582
390
  onSketchTypeChangeRef.current?.(type, fromRef.current);
391
+ if (isEditingRef.current) {
392
+ cancelEditRef.current();
393
+ }
583
394
  }, [type]);
584
395
 
396
+ // Edit
397
+ const onEditFeatureChangeCbs = useRef<SketchEditFeatureChangeCb[]>([]);
398
+ const onEditFeatureChange = useCallback((cb: SketchEditFeatureChangeCb) => {
399
+ onEditFeatureChangeCbs.current.push(cb);
400
+ }, []);
401
+ const onEditFeatureChangeRef = useRef(onEditFeatureChange);
402
+ onEditFeatureChangeRef.current = onEditFeatureChange;
403
+
404
+ const lastSketchEditingFeature = useRef<SketchEditingFeature | undefined>(undefined);
405
+
406
+ const catchedControlPointIndex = useMemo(
407
+ () => state.context.catchedControlPointIndex,
408
+ [state.context.catchedControlPointIndex],
409
+ );
410
+
411
+ const catchedExtrudedPoint = useMemo(
412
+ () => !!state.context.catchedExtrudedPoint,
413
+ [state.context.catchedExtrudedPoint],
414
+ );
415
+
416
+ const ignoreAutoReSelectRef = useRef(false);
417
+
418
+ useEffect(() => {
419
+ onEditFeatureChangeCbs.current.forEach(cb => {
420
+ cb(sketchEditingFeature);
421
+ });
422
+ if (sketchEditingFeature) lastSketchEditingFeature.current = sketchEditingFeature;
423
+ else {
424
+ // Select the feature after editing
425
+ if (ignoreAutoReSelectRef.current) {
426
+ ignoreAutoReSelectRef.current = false;
427
+ return;
428
+ }
429
+ layersRef.current?.selectFeatures([
430
+ {
431
+ layerId: undefined,
432
+ featureId: [],
433
+ },
434
+ ]);
435
+ setTimeout(() => {
436
+ if (lastSketchEditingFeature.current) {
437
+ layersRef.current?.selectFeatures([
438
+ {
439
+ layerId: lastSketchEditingFeature.current?.layerId,
440
+ featureId: [lastSketchEditingFeature.current?.feature.id],
441
+ },
442
+ ]);
443
+ }
444
+ lastSketchEditingFeature.current = undefined;
445
+ }, 50);
446
+ }
447
+ }, [layersRef, sketchEditingFeature, onEditFeatureChangeCbs]);
448
+
449
+ const handleControlPointMouseEvent: ControlPointMouseEventHandler = useCallback(
450
+ (index, isExtrudedPoint, eventType) => {
451
+ if (!state.matches("editing") || !state.context.controlPoints) return;
452
+
453
+ if (eventType === "mousedown") {
454
+ if (isExtrudedPoint) {
455
+ send({
456
+ type: "CATCH",
457
+ catchedControlPointIndex: -1,
458
+ controlPoints: state.context.controlPoints,
459
+ catchedExtrudedPoint: true,
460
+ });
461
+ } else {
462
+ send({
463
+ type: "CATCH",
464
+ catchedControlPointIndex: index,
465
+ controlPoints: state.context.controlPoints,
466
+ catchedExtrudedPoint: false,
467
+ });
468
+ }
469
+ } else {
470
+ if (
471
+ !isExtrudedPoint &&
472
+ (((state.context.type === "polygon" || state.context.type === "extrudedPolygon") &&
473
+ state.context.controlPoints.length > 3) ||
474
+ (state.context.type === "polyline" && state.context.controlPoints.length > 2))
475
+ ) {
476
+ setSelectedControlPointIndex(index);
477
+ }
478
+ }
479
+ },
480
+ [state, send],
481
+ );
482
+
585
483
  // API
586
484
  const getType = useGet(type);
587
485
  const getOptions = useGet(sketchOptions);
@@ -593,29 +491,137 @@ export default function useHooks({
593
491
  setType,
594
492
  getOptions,
595
493
  overrideOptions,
494
+ editFeature,
495
+ cancelEdit,
496
+ applyEdit,
497
+ deleteFeature,
498
+ onEditFeatureChange: onEditFeatureChangeRef.current,
596
499
  }),
597
- [getType, setType, getOptions, overrideOptions],
500
+ [
501
+ getType,
502
+ setType,
503
+ getOptions,
504
+ overrideOptions,
505
+ editFeature,
506
+ deleteFeature,
507
+ cancelEdit,
508
+ applyEdit,
509
+ ],
510
+ );
511
+
512
+ useEffect(() => {
513
+ onMount?.();
514
+ }, [onMount]);
515
+
516
+ const handleDeleteControlPoint = useCallback(() => {
517
+ if (selectedControlPointIndex !== undefined) {
518
+ const newControlPoints = state.context.controlPoints?.toSpliced(selectedControlPointIndex, 1);
519
+ if (!newControlPoints) return;
520
+ send({
521
+ type: "UPDATE",
522
+ controlPoints: newControlPoints,
523
+ });
524
+ setGeometryOptions(op =>
525
+ op
526
+ ? {
527
+ type: op.type,
528
+ controlPoints: newControlPoints,
529
+ }
530
+ : null,
531
+ );
532
+ setSelectedControlPointIndex(undefined);
533
+ }
534
+ }, [selectedControlPointIndex, state.context.controlPoints, send, setGeometryOptions]);
535
+
536
+ const handleDeleteControlPointRef = useRef(handleDeleteControlPoint);
537
+ handleDeleteControlPointRef.current = handleDeleteControlPoint;
538
+
539
+ const handleAddControlPoint = useCallback(
540
+ (controlPoint: Position3d, index: number) => {
541
+ if (state.context.controlPoints == null) return;
542
+ const insertPosition = index + 1;
543
+ const newControlPoints = state.context.controlPoints.toSpliced(
544
+ insertPosition,
545
+ 0,
546
+ controlPoint,
547
+ );
548
+ send({
549
+ type: "UPDATE",
550
+ controlPoints: newControlPoints,
551
+ });
552
+ setGeometryOptions(op =>
553
+ op
554
+ ? {
555
+ type: op.type,
556
+ controlPoints: newControlPoints,
557
+ }
558
+ : null,
559
+ );
560
+ },
561
+ [state.context.controlPoints, send, setGeometryOptions],
598
562
  );
599
563
 
564
+ //
565
+ const tempSwitchToMoveMode = useRef(false);
566
+ const stateRef = useRef(state);
567
+ stateRef.current = state;
568
+
569
+ useEffect(() => {
570
+ return window.addEventListener("keydown", event => {
571
+ if (event.code === "Space" && stateRef.current.matches("editing")) {
572
+ overrideInteractionMode?.("move");
573
+ tempSwitchToMoveMode.current = true;
574
+ } else if (event.code === "Delete" && stateRef.current.matches("editing")) {
575
+ handleDeleteControlPointRef.current();
576
+ }
577
+ });
578
+ }, [overrideInteractionMode]);
579
+
580
+ useEffect(() => {
581
+ return window.addEventListener("keyup", event => {
582
+ if (event.code === "Space" && tempSwitchToMoveMode.current) {
583
+ overrideInteractionMode?.("sketch");
584
+ tempSwitchToMoveMode.current = false;
585
+ }
586
+ });
587
+ }, [overrideInteractionMode]);
588
+
600
589
  return {
601
590
  state,
591
+ isEditing,
592
+ catchedControlPointIndex,
593
+ catchedExtrudedPoint,
602
594
  extrudedHeight,
595
+ extrudedPoint,
596
+ centroidBasePoint,
597
+ centroidExtrudedPoint,
603
598
  geometryOptions,
604
599
  color: sketchOptions.color,
605
600
  disableShadow: sketchOptions.disableShadow,
606
- enableRelativeHeight: sketchOptions.enableRelativeHeight,
607
- } as any;
601
+ selectedControlPointIndex,
602
+ handleControlPointMouseEvent,
603
+ handleAddControlPoint,
604
+ };
608
605
  }
609
606
 
610
- function hasDuplicate(
611
- equalFunction: (
612
- point1: Position3d,
613
- point2: Position3d,
614
- relativeEpsilon: number | undefined,
615
- absoluteEpsilon: number | undefined,
616
- ) => boolean,
617
- controlPoint: Position3d,
618
- controlPoints?: readonly Position3d[],
619
- ): boolean {
620
- return controlPoints?.some(another => equalFunction(controlPoint, another, 0, 1e-7)) === true;
607
+ async function getCentroid(
608
+ controlPoints: readonly Position3d[],
609
+ engineRef: RefObject<EngineRef>,
610
+ ): Promise<Position3d | undefined> {
611
+ let totalLat = 0;
612
+ let totalLng = 0;
613
+
614
+ controlPoints.forEach(controlPoint => {
615
+ const p = engineRef.current?.toLngLatHeight(...controlPoint);
616
+ if (!p) return;
617
+ totalLng += p[0];
618
+ totalLat += p[1];
619
+ });
620
+
621
+ const centroidLat = totalLat / controlPoints.length;
622
+ const centroidLng = totalLng / controlPoints.length;
623
+ const centroidHeight =
624
+ (await engineRef.current?.sampleTerrainHeight(centroidLng, centroidLat)) ?? 0;
625
+
626
+ return engineRef.current?.toXYZ(centroidLng, centroidLat, centroidHeight);
621
627
  }