载入中...
搜索中...
未找到
VoxelCatalogEditor.cpp
浏览该文件的文档.
2
4
5#include <algorithm>
6#include <cmath>
7#include <utility>
8
9namespace eve::voxel_editor {
10namespace {
11
12template <class T = void>
13voxel_editing::Result<T> editorError(voxel_editing::EditorStatus status, std::string rule, std::string message) {
14 return eve::editing::failed<T>(status, voxel_editing::RuleId(std::move(rule)), std::move(message));
15}
16
17} // namespace
18
20 : target_(std::move(targetId)), authority_(&target_), transactions_(&authority_) {
21 seedProject();
22 auto previewed = refreshPreview();
23 if (!previewed.ok())
24 previewed.ignore("voxel editor keeps an empty projection when the seeded preview is rejected");
25}
26
27void VoxelCatalogEditor::seedProject() {
28 auto applySeed = [this](voxel_editing::Result<voxel_editing::DomainOperation> operation, const char* why) {
29 if (!operation.ok()) {
30 operation.ignore(why);
31 return;
32 }
33 auto applied = target_.applyDomainOperation(std::move(operation).takeValue());
34 if (!applied.ok()) applied.ignore(why);
35 };
36
37 voxel_editing::VoxelModelValue cube;
38 cube.id = voxel_editing::ObjectId("cube");
39 cube.name = "cube";
40 cube.sizeX = 8;
41 cube.sizeY = 8;
42 cube.sizeZ = 8;
43 for (int z = 2; z < 6; ++z)
44 for (int y = 2; y < 6; ++y)
45 for (int x = 2; x < 6; ++x) cube.voxels.push_back({x, y, z});
46 cube.sockets[0] = {"wood-edge", voxel_editing::VoxelSocketKind::Symmetric};
47 cube.sockets[1] = {"wood-edge", voxel_editing::VoxelSocketKind::Symmetric};
48
49 voxel_editing::VoxelModelValue bed;
50 bed.id = voxel_editing::ObjectId("bed");
51 bed.name = "bed";
52 bed.sizeX = 8;
53 bed.sizeY = 4;
54 bed.sizeZ = 6;
55 for (int z = 0; z < 6; ++z)
56 for (int x = 0; x < 8; ++x) {
57 bed.voxels.push_back({x, 0, z});
58 bed.voxels.push_back({x, 1, z});
59 }
60 for (int y = 2; y < 4; ++y)
61 for (int x = 0; x < 8; ++x) bed.voxels.push_back({x, y, 0});
62 bed.sockets[0] = {"bed-rail", voxel_editing::VoxelSocketKind::Male};
63 bed.sockets[1] = {"bed-rail", voxel_editing::VoxelSocketKind::Female};
64
65 applySeed(target_.makeCreateModel(cube), "voxel editor seed cube");
66 applySeed(target_.makeCreateModel(bed), "voxel editor seed bed");
67}
68
70 editor::EditorWorkspace candidate = workspace;
71 struct Panel {
72 const char* id;
73 const char* title;
74 const char* region;
75 const char* context;
76 int order;
77 };
78 constexpr Panel panels[] = {
79 {"voxel.models", "Models", "left", "list", 100},
80 {"voxel.viewport", "Sculpt", "center", "preview", 100},
81 {"voxel.tools", "Tools", "right", "inspector", 100},
82 {"voxel.inspector", "Model", "bottom", "inspector", 100},
83 };
84 for (const auto& panel : panels) {
85 if (!candidate.registerPanel(panel.id, panel.title, panel.region, panel.order) ||
86 !candidate.setPanelCapability(panel.id, "voxel.sculpt") ||
87 !candidate.setPanelContext(panel.id, panel.context))
88 return editorError(voxel_editing::EditorStatus::Rejected, "editor.voxel.workspace-conflict",
89 "Could not install the voxel sculpt workspace");
90 }
91 if (!candidate.activatePanel("voxel.viewport"))
92 return editorError(voxel_editing::EditorStatus::Rejected, "editor.voxel.workspace-activate",
93 "Could not activate the voxel sculpt viewport");
94 workspace = std::move(candidate);
95 return eve::editing::applied<void>();
96}
97
98editor::SelectionSnapshot VoxelCatalogEditor::selection() const {
100 snapshot.channel = "voxel-catalog";
102 item.domain = editor::SelectionDomain::Asset;
103 item.target = editor::TargetId(target_.targetId());
104 item.item = editor::StableId(selectedId_);
105 item.type = "voxel.model";
106 snapshot.items.push_back(item);
107 snapshot.primary = item;
108 return snapshot;
109}
110
111const voxel_editing::VoxelModelValue* VoxelCatalogEditor::selectedModel() const {
112 return target_.findModel(voxel_editing::ObjectId(selectedId_));
113}
114
115const char* VoxelCatalogEditor::fillName(voxel_editing::VoxelCellFill fill) {
116 switch (fill) {
118 return "filled";
120 return "partial";
122 default:
123 return "empty";
124 }
125}
126
127std::string VoxelCatalogEditor::toolName() const { return tool_ == VoxelSculptTool::Erase ? "erase" : "attach"; }
128
129voxel_editing::Result<void> VoxelCatalogEditor::commit(
131 if (!operation.ok()) return voxel_editing::Result<void>::failure(operation.status());
133 spec.id = editor::TransactionId("voxel.sculpt.tx." + std::to_string(++txSequence_));
134 spec.label = std::move(label);
135 spec.target = editor::TargetId(target_.targetId());
136 spec.baseRevision = target_.revision();
137 auto begun = transactions_.begin(std::move(spec));
138 if (!begun.ok())
139 return editorError(begun.code(), "editor.voxel.begin", "Could not begin the voxel transaction");
140 auto appended = transactions_.append(std::move(operation).takeValue());
141 if (!appended.ok()) {
142 auto discarded = transactions_.discard();
143 if (!discarded.ok()) discarded.ignore("pending voxel transaction already inactive");
144 return voxel_editing::Result<void>::failure(appended.status());
145 }
146 auto committed = transactions_.commit();
147 if (!committed.ok()) return voxel_editing::Result<void>::failure(committed.status());
148 return refreshPreview();
149}
150
151void VoxelCatalogEditor::cameraAxes(float& fx, float& fy, float& fz, float& rx, float& ry, float& rz, float& ux,
152 float& uy, float& uz) const {
153 const float cy = std::cos(yaw_);
154 const float sy = std::sin(yaw_);
155 const float cp = std::cos(pitch_);
156 const float sp = std::sin(pitch_);
157 fx = sy * cp;
158 fy = sp;
159 fz = cy * cp;
160 rx = cy;
161 ry = 0.0f;
162 rz = -sy;
163 ux = -sy * sp;
164 uy = cp;
165 uz = -cy * sp;
166}
167
168void VoxelCatalogEditor::rebuildScreen() {
169 screen_.clear();
170 const auto* model = selectedModel();
171 if (!model) return;
172 float fx, fy, fz, rx, ry, rz, ux, uy, uz;
173 cameraAxes(fx, fy, fz, rx, ry, rz, ux, uy, uz);
174 const float cx = static_cast<float>(model->sizeX) * 0.5f;
175 const float cy = static_cast<float>(model->sizeY) * 0.5f;
176 const float cz = static_cast<float>(model->sizeZ) * 0.5f;
177 const float extent =
178 std::max(1.0f, static_cast<float>(std::max(model->sizeX, std::max(model->sizeY, model->sizeZ))));
179 const float scale = std::min(viewportW_, viewportH_) / (extent * 1.8f);
180 for (const auto& voxel : model->voxels) {
181 const float px = static_cast<float>(voxel.x) + 0.5f - cx;
182 const float py = static_cast<float>(voxel.y) + 0.5f - cy;
183 const float pz = static_cast<float>(voxel.z) + 0.5f - cz;
184 ScreenVoxel box;
185 box.depth = px * fx + py * fy + pz * fz;
186 box.w = scale * 0.9f;
187 box.h = scale * 0.9f;
188 box.x = viewportW_ * 0.5f + (px * rx + py * ry + pz * rz) * scale - box.w * 0.5f;
189 box.y = viewportH_ * 0.5f - (px * ux + py * uy + pz * uz) * scale - box.h * 0.5f;
190 screen_.push_back(box);
191 }
192 std::sort(screen_.begin(), screen_.end(), [](const ScreenVoxel& a, const ScreenVoxel& b) { return a.depth > b.depth; });
193}
194
195voxel_editing::Result<void> VoxelCatalogEditor::refreshPreview() {
196 if (!selectedModel())
197 return editorError(voxel_editing::EditorStatus::NotFound, "editor.voxel.selection",
198 "Voxel model was not found");
199 rebuildScreen();
200 joinPartners_ = target_.hullJoinPartners(voxel_editing::ObjectId(selectedId_), selectedFace_);
201 previewRevision_ = target_.revision();
202 return eve::editing::applied<void>();
203}
204
206 if (!target_.findModel(voxel_editing::ObjectId(id)))
207 return editorError(voxel_editing::EditorStatus::NotFound, "editor.voxel.model", "Voxel model was not found");
208 selectedId_ = std::move(id);
209 return refreshPreview();
210}
211
213 if (tool == "attach")
215 else if (tool == "erase")
217 else
218 return editorError(voxel_editing::EditorStatus::Rejected, "editor.voxel.tool",
219 "Voxel tool must be attach or erase");
220 return eve::editing::applied<void>();
221}
222
224 if (!(width > 1.0f) || !(height > 1.0f) || !std::isfinite(width) || !std::isfinite(height))
225 return editorError(voxel_editing::EditorStatus::Rejected, "editor.voxel.viewport",
226 "Viewport size must be finite and greater than 1");
227 viewportW_ = width;
228 viewportH_ = height;
229 rebuildScreen();
230 return eve::editing::applied<void>();
231}
232
234 if (!std::isfinite(yawDelta) || !std::isfinite(pitchDelta))
235 return editorError(voxel_editing::EditorStatus::Rejected, "editor.voxel.orbit",
236 "Orbit deltas must be finite");
237 yaw_ += yawDelta;
238 pitch_ = std::clamp(pitch_ + pitchDelta, -1.2f, 1.2f);
239 rebuildScreen();
240 return eve::editing::applied<void>();
241}
242
243voxel_editing::Result<void> VoxelCatalogEditor::applyPick(const voxel_editing::VoxelPick& pick) {
244 const auto* model = selectedModel();
245 if (!model)
246 return editorError(voxel_editing::EditorStatus::Rejected, "editor.voxel.selection",
247 "A model must be selected to sculpt");
248 if (tool_ == VoxelSculptTool::Erase) {
249 if (!pick.hit)
250 return editorError(voxel_editing::EditorStatus::NotFound, "editor.voxel.pick",
251 "No voxel under the pointer");
252 return commit(target_.makeSetVoxel(model->id, pick.hitX, pick.hitY, pick.hitZ, false), "Erase voxel");
253 }
254 if (!pick.canAttach)
255 return editorError(voxel_editing::EditorStatus::Rejected, "editor.voxel.attach",
256 "Attach needs an empty cell inside the model bounds");
257 return commit(target_.makeSetVoxel(model->id, pick.prevX, pick.prevY, pick.prevZ, true), "Attach voxel");
258}
259
261 float dz) {
262 const auto* model = selectedModel();
263 if (!model)
264 return editorError(voxel_editing::EditorStatus::Rejected, "editor.voxel.selection",
265 "A model must be selected to sculpt");
266 if (!std::isfinite(ox) || !std::isfinite(oy) || !std::isfinite(oz) || !std::isfinite(dx) || !std::isfinite(dy) ||
267 !std::isfinite(dz))
268 return editorError(voxel_editing::EditorStatus::Rejected, "editor.voxel.ray", "Ray values must be finite");
269 return applyPick(voxel_editing::pickVoxelModel(*model, ox, oy, oz, dx, dy, dz, 256.0f));
270}
271
273 const auto* model = selectedModel();
274 if (!model)
275 return editorError(voxel_editing::EditorStatus::Rejected, "editor.voxel.selection",
276 "A model must be selected to sculpt");
277 if (!std::isfinite(x) || !std::isfinite(y))
278 return editorError(voxel_editing::EditorStatus::Rejected, "editor.voxel.pointer",
279 "Pointer coordinates must be finite");
280 float fx, fy, fz, rx, ry, rz, ux, uy, uz;
281 cameraAxes(fx, fy, fz, rx, ry, rz, ux, uy, uz);
282 const float cx = static_cast<float>(model->sizeX) * 0.5f;
283 const float cy = static_cast<float>(model->sizeY) * 0.5f;
284 const float cz = static_cast<float>(model->sizeZ) * 0.5f;
285 const float extent =
286 std::max(1.0f, static_cast<float>(std::max(model->sizeX, std::max(model->sizeY, model->sizeZ))));
287 const float scale = std::min(viewportW_, viewportH_) / (extent * 1.8f);
288 const float nx = (x - viewportW_ * 0.5f) / scale;
289 const float ny = (viewportH_ * 0.5f - y) / scale;
290 const float ox = cx - fx * distance_ + rx * nx + ux * ny;
291 const float oy = cy - fy * distance_ + ry * nx + uy * ny;
292 const float oz = cz - fz * distance_ + rz * nx + uz * ny;
293 const auto pick = voxel_editing::pickVoxelModel(*model, ox, oy, oz, fx, fy, fz, distance_ * 4.0f);
294 return applyPick(pick);
295}
296
298 const auto* model = selectedModel();
299 if (!model)
300 return editorError(voxel_editing::EditorStatus::Rejected, "editor.voxel.selection",
301 "A model must be selected to sculpt");
302 return commit(target_.makeSetVoxel(model->id, x, y, z, occupied), occupied ? "Attach voxel" : "Erase voxel");
303}
304
306 auto tagSet =
307 commit(target_.makeSet(selection(),
308 voxel_editing::PropertyPath("model.socket." + std::to_string(selectedFace_) + ".tag"),
309 voxel_editing::EditorValue(std::move(tag)), voxel_editing::PropertySetMode::Absolute),
310 "Set socket tag");
311 if (!tagSet.ok()) return tagSet;
312 return commit(target_.makeSet(selection(),
313 voxel_editing::PropertyPath("model.socket." + std::to_string(selectedFace_) + ".kind"),
315 voxel_editing::PropertySetMode::Absolute),
316 "Set socket kind");
317}
318
320 if (face < 0 || face > 5)
321 return editorError(voxel_editing::EditorStatus::Rejected, "editor.voxel.face", "Face index must be 0..5");
322 selectedFace_ = face;
323 return refreshPreview();
324}
325
327 int sizeY, int sizeZ) {
330 model.name = std::move(name);
331 model.sizeX = sizeX;
332 model.sizeY = sizeY;
333 model.sizeZ = sizeZ;
334 auto created = commit(target_.makeCreateModel(model), "Create model");
335 if (!created.ok()) return created;
336 return selectModel(std::move(id));
337}
338
340 const auto* model = selectedModel();
341 if (!model)
342 return editorError(voxel_editing::EditorStatus::Rejected, "editor.voxel.selection",
343 "A model must be selected to delete");
344 auto deleted = commit(target_.makeDeleteModel(model->id), "Delete model");
345 if (!deleted.ok()) return deleted;
346 if (!target_.models().empty()) return selectModel(target_.models().front().id.value());
347 selectedId_.clear();
348 screen_.clear();
349 return eve::editing::applied<void>();
350}
351
353 auto undone = transactions_.undo();
354 if (!undone.ok()) return undone;
355 auto previewed = refreshPreview();
356 if (!previewed.ok()) return voxel_editing::Result<editor::TransactionReceipt>::failure(previewed.status());
357 return undone;
358}
359
361 auto redone = transactions_.redo();
362 if (!redone.ok()) return redone;
363 auto previewed = refreshPreview();
364 if (!previewed.ok()) return voxel_editing::Result<editor::TransactionReceipt>::failure(previewed.status());
365 return redone;
366}
367
368std::string VoxelCatalogEditor::modelId(int index) const {
369 if (index < 0 || index >= modelCount()) return {};
370 return target_.models()[static_cast<std::size_t>(index)].id.value();
371}
372
373std::string VoxelCatalogEditor::modelName(int index) const {
374 if (index < 0 || index >= modelCount()) return {};
375 return target_.models()[static_cast<std::size_t>(index)].name;
376}
377
378std::string VoxelCatalogEditor::modelFill(int index) const {
379 if (index < 0 || index >= modelCount()) return {};
380 return fillName(voxel_editing::voxelClassifyModelFill(target_.models()[static_cast<std::size_t>(index)]));
381}
382
384 return index >= 0 && index < modelCount() && modelId(index) == selectedId_;
385}
386
388 const auto* model = selectedModel();
389 return model ? static_cast<int>(model->voxels.size()) : 0;
390}
391
393 const auto* model = selectedModel();
394 if (!model || index < 0 || index >= static_cast<int>(model->voxels.size())) return 0;
395 return model->voxels[static_cast<std::size_t>(index)].x;
396}
397
399 const auto* model = selectedModel();
400 if (!model || index < 0 || index >= static_cast<int>(model->voxels.size())) return 0;
401 return model->voxels[static_cast<std::size_t>(index)].y;
402}
403
405 const auto* model = selectedModel();
406 if (!model || index < 0 || index >= static_cast<int>(model->voxels.size())) return 0;
407 return model->voxels[static_cast<std::size_t>(index)].z;
408}
409
411 const auto* model = selectedModel();
412 return model ? model->sizeX : 0;
413}
414
416 const auto* model = selectedModel();
417 return model ? model->sizeY : 0;
418}
419
421 const auto* model = selectedModel();
422 return model ? model->sizeZ : 0;
423}
424
426 if (index < 0 || index >= screenVoxelCount()) return 0;
427 return screen_[static_cast<std::size_t>(index)].x;
428}
429
431 if (index < 0 || index >= screenVoxelCount()) return 0;
432 return screen_[static_cast<std::size_t>(index)].y;
433}
434
436 if (index < 0 || index >= screenVoxelCount()) return 0;
437 return screen_[static_cast<std::size_t>(index)].w;
438}
439
441 if (index < 0 || index >= screenVoxelCount()) return 0;
442 return screen_[static_cast<std::size_t>(index)].h;
443}
444
446 const auto* model = selectedModel();
447 if (!model) return {};
448 return model->sockets[static_cast<std::size_t>(selectedFace_)].tag;
449}
450
452 const auto* model = selectedModel();
453 if (!model) return {};
454 return voxel_editing::voxelSocketKindName(model->sockets[static_cast<std::size_t>(selectedFace_)].kind);
455}
456
458 if (index < 0 || index >= joinPartnerCount()) return {};
459 return joinPartners_[static_cast<std::size_t>(index)].value();
460}
461
462} // namespace eve::voxel_editor
ActionParameterOperation operation
double value
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
float cx
Definition CardTypes.cpp:33
float cy
Definition CardTypes.cpp:34
float py
float nx
float ny
float pz
std::string title
std::string label
std::string message
wgpu::PopErrorScopeStatus status
int h
std::vector< Colorf > px
std::uint32_t height
std::uint32_t width
TokenKind kind
std::array< float, 3 > scale
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::vector< std::int32_t > order
const std::string * tag
std::string id
Definition PlayHost.cpp:108
std::uint32_t targetId
glm::mat4 model
int created
float dz
float dy
float dx
bool occupied
uint32_t index
double oy
double ox
const VegetationPresetContext & context
MagicaVoxel-style miniature sculpt editor: orbit view, attach/erase, undo.
void ignore(std::string_view reason={}) const noexcept
Explicitly discard this result after documenting the reason.
Definition Result.h:537
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
UUID-backed identifier adapter for legacy textual boundaries.
Definition Identity.h:314
std::uint64_t revision() const override
Return the current revision.
Dotted property path (layout.size) compared and persisted as text.
Definition EditingIds.h:78
Deterministic owning value tree shared by authoring hosts.
Composition model shared by project-specific developer and runtime editors.
bool setPanelContext(const std::string &id, const std::string &context)
Attach a semantic input context such as scene, graph, or timeline.
Result< WorkspacePanelDescriptor > registerPanel(WorkspacePanelDescriptor descriptor)
Register a checked owning panel descriptor.
Result< WorkspacePanelDescriptor > activatePanel(const StableId &id)
Activate a visible panel with structured not-found/rejected results.
bool setPanelCapability(const std::string &id, const std::string &capability)
Attach a capability requirement used by the host when filtering panels.
Result< TransactionReceipt > commit() override
Preflight and commit all active operations through the authority.
Result< TransactionId > begin(TransactionSpec specification) override
Begin a transaction with a stable id and base revision.
Result< TransactionReceipt > undo() override
Compensate the most recent committed transaction.
Result< void > discard() override
Discard the active, not-yet-committed operation list.
Result< TransactionReceipt > redo() override
Reapply the most recently compensated transaction.
Result< void > append(DomainOperation operation) override
Append an operation to the active transaction without applying it.
Result< DomainOperation > makeSet(const SelectionSnapshot &, const PropertyPath &, const EditorValue &, PropertySetMode) const override
Make set.
std::vector< ObjectId > hullJoinPartners(const ObjectId &model, int face) const
Hull join partners.
Result< void > applyDomainOperation(const DomainOperation &) override
Applies domain operation.
Result< DomainOperation > makeCreateModel(const VoxelModelValue &) const
Make create model.
Result< DomainOperation > makeSetVoxel(const ObjectId &model, int x, int y, int z, bool occupied) const
Make set voxel.
const VoxelModelValue * findModel(const ObjectId &) const
Look up a sculpted model by stable id.
TargetId targetId() const override
Target id.
Result< DomainOperation > makeDeleteModel(const ObjectId &) const
Make delete model.
const std::vector< VoxelModelValue > & models() const
Models.
bool isModelSelected(int index) const
True when model selected.
int joinPartnerCount() const
Join partner count.
voxel_editing::Result< void > pointerWorldRay(float ox, float oy, float oz, float dx, float dy, float dz)
MagicaVoxel attach/erase using a world-space camera ray.
voxel_editing::Result< void > setVoxel(int x, int y, int z, bool occupied)
Sets the voxel.
std::string modelId(int index) const
Model id.
voxel_editing::Result< void > configureWorkspace(editor::EditorWorkspace &workspace) const
Install Models / Viewport / Tools / Inspector panels.
std::string selectedSocketTag() const
Selected socket tag.
float screenVoxelY(int index) const
Screen voxel y.
std::string modelName(int index) const
Model name.
voxel_editing::Result< void > pointerDown(float x, float y)
voxel_editing::Result< void > createModel(std::string id, std::string name, int sizeX, int sizeY, int sizeZ)
Creates model.
float screenVoxelX(int index) const
Screen voxel x.
voxel_editing::Result< void > setTool(std::string tool)
std::string modelFill(int index) const
Model fill.
voxel_editing::Result< void > orbit(float yawDelta, float pitchDelta)
std::string selectedSocketKind() const
Selected socket kind.
std::string toolName() const
Tool name.
voxel_editing::Result< void > deleteSelectedModel()
Delete selected model.
voxel_editing::Result< void > selectModel(std::string id)
voxel_editing::Result< void > selectFace(int face)
Select face.
voxel_editing::Result< editor::TransactionReceipt > redo()
Redo.
std::string joinPartnerId(int index) const
Join partner id.
float screenVoxelH(int index) const
Screen voxel h.
voxel_editing::Result< void > setSelectedSocket(std::string tag, std::string kind)
Sets the selected socket.
voxel_editing::Result< editor::TransactionReceipt > undo()
Undo.
voxel_editing::Result< void > setViewport(float width, float height)
VoxelCatalogEditor(std::string targetId)
Construct a seeded solid cube and a sculpted bed.
float screenVoxelW(int index) const
Screen voxel w.
int screenVoxelCount() const
Screen voxel count.
eve::editing::StableId StableId
Definition EditorIds.h:36
eve::editing::TargetId TargetId
Definition EditorIds.h:29
eve::editing::TransactionId TransactionId
Definition EditorIds.h:34
VoxelCellFill voxelClassifyModelFill(const VoxelModelValue &model)
Classify a sculpted model as empty, partial, or a solid cube.
editing::ObjectId ObjectId
editing::Status EditorStatus
const char * voxelSocketKindName(VoxelSocketKind kind)
Stable serialized name for a socket kind. @ownership Observed static string; callers must not free it...
VoxelCellFill
Derived occupancy of a sculpted object.
editing::RuleId RuleId
VoxelPick pickVoxelModel(const VoxelModelValue &model, float ox, float oy, float oz, float dx, float dy, float dz, float maxDistance)
Raycast occupied cells with MagicaVoxel-style previous-cell attach.
SelectionItem public API.
SelectionSnapshot public API.
std::vector< SelectionItem > items
std::optional< SelectionItem > primary
TransactionSpec public API.
One MagicaVoxel-style object: a bounded occupancy grid.
DDA pick against a sculpted model.