WIP WYSIWYG ~3D SVG editor.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement widget drawing, add Translate tool.

This commit has a lot of changes underpinning "implement widget drawing."

- Added Zoodle.updateUI() that redraws widgets based on the current selection, similar to `updateHighlights`.
- UI input handling uses the same event handlers, so added the concept of layers to distinguish which layer the event is coming from.
- Added a `TemporaryTool` that draws the widgets of one tool while acting as another, to allow things like orbiting and panning without dropping the original tool.

+210 -11
+210 -11
zoodle.js
··· 28 28 29 29 this.selection = []; 30 30 this.history = new History(); 31 - this.tool = "orbit"; 32 31 this.tools = { 33 32 orbit: new OrbitTool(this), 33 + translate: new TranslateTool(this), 34 34 }; 35 + this.tool = this.tools.translate; 35 36 36 37 this.presets = { 37 38 solar: new Solar(), ··· 71 72 requestAnimationFrame(this.update.bind(this)); 72 73 } 73 74 75 + set tool(tool) { 76 + this._tool = tool; 77 + this.updateUI(); 78 + } 79 + get tool() { return this._tool; } 80 + 74 81 // Perform a command and add it to the history. 75 82 do(command) { 76 83 this.history.push(command); ··· 89 96 } else { 90 97 this.selection.push(target); 91 98 } 92 - this.updateHighlight(); 99 + this.updateHighlights(); 100 + this.updateUI(); 93 101 } 94 102 95 103 clearSelection() { 96 104 this.selection = []; 97 - this.updateHighlight(); 105 + this.updateHighlights(); 106 + this.updateUI(); 98 107 } 99 108 100 - updateHighlight() { 109 + updateHighlights() { 101 110 this.overlay.children = []; 102 111 this.selection.forEach((selected) => { 103 112 let highlight = selected.copyGraph({ 104 113 addTo: this.overlay, 105 114 color: "#E62", 106 115 backface: "#E62", 116 + ...this.getWorldTransforms(selected), 107 117 }); 108 118 109 119 // Highlight all children ··· 113 123 }); 114 124 115 125 // Apply parent transforms to selected object. 116 - Zdog.extend(highlight, this.getWorldTransforms(selected)); 126 + // Zdog.extend(highlight, this.getWorldTransforms(selected)); 117 127 }); 118 128 } 119 129 130 + updateUI() { 131 + this.ui.children = []; 132 + 133 + if (this.selection.length === 0) { 134 + return; 135 + } 136 + 137 + // Create anchors matching selected objects. 138 + let targets = this.selection.map((target) => { 139 + return new Zdog.Anchor({ 140 + addTo: this.ui, 141 + ...this.getWorldTransforms(target), 142 + }); 143 + }); 144 + 145 + this.tool.drawWidget(targets); 146 + } 147 + 120 148 syncLayers() { 121 149 const scene = this.scene; 122 150 const targets = [this.overlay, this.ui]; ··· 165 193 166 194 // input 167 195 click(ptr, target, x, y) { 196 + target.layer = this.getLayer(target.element) 168 197 this.do(new SelectCommand(this, target)); 169 198 } 170 199 171 200 dragStart(ptr, target, x, y) { 172 - // TODO: Edit Zfetch so it just returns null instead of the scene. 173 - if (!target || target.element === this.scene.element) { 174 - this.tool = "orbit"; 201 + target.layer = this.getLayer(target.element); 202 + if (!target || !target.addTo) { 203 + this.tool = new TemporaryTool(this, this.tool, this.tools.orbit, true); 175 204 } 176 205 177 - this.tools[this.tool].start(ptr, target, x, y); 206 + this.tool.start(ptr, target, x, y); 178 207 } 179 208 180 209 dragMove(ptr, target, x, y) { 181 - this.tools[this.tool].move(ptr, target, x, y); 210 + target.layer = this.getLayer(target.element); 211 + this.tool.move(ptr, target, x, y); 182 212 } 183 213 184 214 dragEnd(ptr, target, x, y) { 185 - this.tools[this.tool].end(ptr, target, x, y); 215 + target.layer = this.getLayer(target.element); 216 + this.tool.end(ptr, target, x, y); 217 + } 218 + 219 + getLayer(element) { 220 + switch (element) { 221 + case this.scene.element: 222 + return Zoodle.LAYER_CANVAS; 223 + case this.overlay.element: 224 + return Zoodle.LAYER_OVERLAY; 225 + case this.ui.element: 226 + return Zoodle.LAYER_UI; 227 + default: 228 + console.error(`Unsupported element ${element}`); 229 + } 186 230 } 231 + 232 + // returns the distance of a point (x, y) from the origin along the axis defined by the angle. 233 + getAxisDistance(x, y, angle) { 234 + return x * Math.cos(angle) + y * Math.sin(angle); 235 + } 236 + 237 + static get LAYER_CANVAS() { return 1; } 238 + static get LAYER_OVERLAY() { return 2; } 239 + static get LAYER_UI() { return 3; } 187 240 } 188 241 189 242 class Tool { ··· 193 246 start(ptr, target, x, y) {} 194 247 move(ptr, target, x, y) {} 195 248 end(ptr, target, x, y) {} 249 + drawWidget(targets) {} 250 + } 251 + 252 + // this tool performs the actions of another tool without hiding the widgets of the original 253 + class TemporaryTool extends Tool { 254 + constructor(editor, style, substance, autoRestore = true) { 255 + super(editor); 256 + this.style = style; 257 + this.substance = substance; 258 + this.autoRestore = autoRestore; 259 + } 260 + start(ptr, target, x, y) { 261 + this.substance.start(ptr, target, x, y); 262 + } 263 + move(ptr, target, x, y) { 264 + this.substance.move(ptr, target, x, y); 265 + } 266 + end(ptr, target, x, y) { 267 + this.substance.end(ptr, target, x, y); 268 + if (this.autoRestore) { 269 + this.editor.tool = this.style; 270 + } 271 + } 272 + drawWidget(targets) { 273 + this.style.drawWidget(targets); 274 + } 196 275 } 197 276 198 277 class OrbitTool extends Tool { ··· 203 282 this.editor.sceneInput.rotateMove(ptr, target, x, y); 204 283 this.editor.syncLayers(); 205 284 } 285 + } 286 + 287 + class TranslateTool extends Tool { 288 + constructor(editor) { 289 + super(editor); 290 + this.targets = null; 291 + this.startTranslate = null; 292 + this.mode = TranslateTool.MODE_NONE; 293 + this.widget = null; 294 + } 295 + start(ptr, target, x, y) { 296 + this.widget = target; 297 + // Ensure our target is the base and not the tip. 298 + if (this.widget.diameter) 299 + this.widget = this.widget.addTo; 300 + this.targets = this.editor.selection; 301 + this.startTranslate = this.targets.map((t) => t.translate.copy()); 302 + if (target.layer !== Zoodle.LAYER_UI) { 303 + this.mode = TranslateTool.MODE_VIEW; 304 + return; 305 + } 306 + switch (target.color) { 307 + case rose: 308 + this.mode = TranslateTool.MODE_X; 309 + break; 310 + case lime: 311 + this.mode = TranslateTool.MODE_Y; 312 + break; 313 + case blueberry: 314 + this.mode = TranslateTool.MODE_Z; 315 + break; 316 + default: 317 + this.mode = TranslateTool.MODE_VIEW; 318 + break; 319 + } 320 + } 321 + move(ptr, target, x, y) { 322 + if (!this.mode) { return; } 323 + let direction = this.widget.renderNormal; // TODO: Break out into a function. 324 + let delta = this.editor.getAxisDistance(x, y, Math.atan2(direction.y, direction.x)); 325 + delta /= -this.editor.scene.zoom; // TODO: Include pixel ratio as well. 326 + delta *= this.widget.scale.x; 327 + console.log("movin", x, y, direction, delta); 328 + this.editor.updateHighlights(); 329 + this.editor.updateUI(); 330 + //let delta = new Zdog.Vector({x: x, y: y}).magnitude2d(); 331 + this.targets.forEach((t, i) => { 332 + t.translate[this.mode] = this.startTranslate[i][this.mode] + delta; 333 + }); 334 + 335 + } 336 + end(ptr, target, x, y) { 337 + if (!this.mode) { return; } 338 + // ensure any final adjustments are applied. 339 + this.move(ptr, target, x, y); 340 + let direction = this.widget.renderNormal; // TODO: Break out into a function. 341 + let delta = this.editor.getAxisDistance(x, y, Math.atan2(direction.y, direction.x)); 342 + delta /= -this.editor.scene.zoom; 343 + delta /= this.widget.scale; 344 + let command = new TranslateCommand(this.editor, this.targets, new Zdog.Vector({[this.mode]: delta})); 345 + this.editor.did(command); 346 + } 347 + drawWidget(targets) { 348 + let origin = new Zdog.Shape({ 349 + stroke: .5, 350 + color: lace, 351 + }); 352 + let base = new Zdog.Shape({ 353 + path: [ { z: -1.5 }, { z: 1.5 } ], 354 + stroke: 1, 355 + translate: { z: 3 }, 356 + }); 357 + new Zdog.Cone({ 358 + addTo: base, 359 + diameter: 2, 360 + length: 1.5, 361 + stroke: .5, 362 + translate: { z: 1.5 }, 363 + }); 364 + let z = base.copyGraph({ 365 + color: blueberry, 366 + }); 367 + z.children[0].color = blueberry; 368 + let y = base.copyGraph({ 369 + color: lime, 370 + rotate: { x: -TAU/4 }, 371 + translate: { y: 3 }, 372 + }); 373 + y.children[0].color = lime; 374 + let x = base.copyGraph({ 375 + color: rose, 376 + rotate: { y: -TAU/4 }, 377 + translate: { x: 3 }, 378 + }); 379 + x.children[0].color = rose; 380 + targets.forEach(t => { 381 + origin.copyGraph({ addTo: t, scale: 1/t.scale.x }); 382 + z.copyGraph({ 383 + addTo: t, 384 + scale: 1/t.scale.x, 385 + translate: { z: 3/t.scale.x }, 386 + }); 387 + y.copyGraph({ 388 + addTo: t, 389 + scale: 1/t.scale.x, 390 + translate: { y: 3/t.scale.x }, 391 + }); 392 + x.copyGraph({ 393 + addTo: t, 394 + scale: 1/t.scale.x, 395 + translate: { x: 3/t.scale.x }, 396 + }); 397 + }); 398 + } 399 + 400 + static get MODE_NONE() { return ''; } 401 + static get MODE_X() { return 'x'; } 402 + static get MODE_Y() { return 'y'; } 403 + static get MODE_Z() { return 'z'; } 404 + static get MODE_VIEW() { return 'v'; } 206 405 } 207 406 208 407 class Command {