WIP WYSIWYG ~3D SVG editor.
0
fork

Configure Feed

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

at main 429 lines 12 kB view raw
1class Tool { 2 constructor(editor) { 3 this.editor = editor; 4 } 5 start(ptr, target, x, y) {} 6 move(ptr, target, x, y) {} 7 end(ptr, target, x, y) {} 8 drawWidget(targets) {} 9} 10 11// this tool performs the actions of another tool without hiding the widgets of the original 12class TemporaryTool extends Tool { 13 constructor(editor, style, substance, autoRestore = true) { 14 super(editor); 15 this.style = style; 16 this.substance = substance; 17 this.autoRestore = autoRestore; 18 } 19 start(ptr, target, x, y) { 20 this.substance.start(ptr, target, x, y); 21 } 22 move(ptr, target, x, y) { 23 this.substance.move(ptr, target, x, y); 24 } 25 end(ptr, target, x, y) { 26 this.substance.end(ptr, target, x, y); 27 if (this.autoRestore) { 28 this.editor.tool = this.style; 29 } 30 } 31 drawWidget(targets) { 32 this.style.drawWidget(targets); 33 } 34} 35 36class OrbitTool extends Tool { 37 constructor(editor) { 38 super(editor); 39 this.rotateStart = null; 40 } 41 start(ptr, target, x, y) { 42 this.rotateStart = this.editor.scene.rotate.copy(); 43 } 44 move(ptr, target, x, y) { 45 let displaySize = Math.min( this.editor.scene.width, this.editor.scene.height ); 46 let moveRY = x / displaySize * Math.PI * Zdog.TAU; 47 let moveRX = y / displaySize * Math.PI * Zdog.TAU; 48 this.editor.scene.rotate.x = this.rotateStart.x - moveRX; 49 this.editor.scene.rotate.y = this.rotateStart.y - moveRY; 50 51 this.editor.syncLayers(); 52 } 53} 54 55class TranslateTool extends Tool { 56 constructor(editor) { 57 super(editor); 58 this.targets = null; 59 this.startTranslate = null; 60 this.mode = TranslateTool.MODE_NONE; 61 this.widget = null; 62 } 63 start(ptr, target, x, y) { 64 this.widget = target; 65 this.targets = this.editor.selection.slice(0); 66 this.mode = TranslateTool.MODE_NONE; 67 if (!this.targets.length || target.layer !== Zoodle.LAYER_UI || !target.color) { 68 return; 69 } 70 // Ensure our widget target is the base and not the tip. 71 if (this.widget.diameter) 72 this.widget = this.widget.addTo; 73 this.startTranslate = this.targets.map((t) => t.translate.copy()); 74 switch (target.color) { 75 case rose: 76 this.mode = TranslateTool.MODE_X; 77 break; 78 case lime: 79 this.mode = TranslateTool.MODE_Y; 80 break; 81 case blueberry: 82 this.mode = TranslateTool.MODE_Z; 83 break; 84 default: 85 this.mode = TranslateTool.MODE_NONE; 86 break; 87 } 88 } 89 move(ptr, target, x, y) { 90 if (!this.mode) { return; } 91 let direction = this.widget.renderNormal; // TODO: Break out into a function. 92 let delta = this.editor.getAxisDistance(x, y, Math.atan2(direction.y, direction.x)); 93 delta /= -this.editor.scene.zoom; // TODO: Include pixel ratio as well. 94 delta *= this.widget.addTo.addTo.scale.x; 95 delta *= Math.abs(this.widget.renderNormal.magnitude2d()); 96 // TODO: Oh I know where this is going wrong, we're not including how the view rotation is going to shorten the distance. 97 // We need some sines or cosines or something or both in here. 98 this.targets.forEach((t, i) => { 99 t.translate[this.mode] = this.startTranslate[i][this.mode] + delta; 100 }); 101 this.editor.updateHighlights(); 102 this.editor.updateUI(); 103 this.editor.props.updatePanel(); 104 } 105 end(ptr, target, x, y) { 106 if (!this.mode) { return; } 107 // ensure any final adjustments are applied. 108 this.move(ptr, target, x, y); 109 let direction = this.widget.renderNormal; // TODO: Break out into a function. 110 let delta = this.editor.getAxisDistance(x, y, Math.atan2(direction.y, direction.x)); 111 delta /= -this.editor.scene.zoom; 112 delta *= this.widget.addTo.addTo.scale.x; 113 delta /= Math.abs(this.widget.renderNormal.magnitude2d()); 114 let command = new TranslateCommand(this.editor, this.targets, new Zdog.Vector({[this.mode]: delta}), this.startTranslate); 115 this.editor.did(command); 116 } 117 drawWidget(targets) { 118 // Create anchors matching selected objects. 119 targets = targets.map((target) => { 120 // TODO: Double check this is correct. 121 let parentTransforms = this.editor.getWorldTransforms(target.addTo); 122 let childTranslate = target.translate.copy().rotate(parentTransforms.rotate); 123 parentTransforms.translate.add(childTranslate); 124 parentTransforms.translate.multiply(parentTransforms.scale); 125 return new Zdog.Anchor({ 126 addTo: this.editor.ui, 127 ...parentTransforms, 128 }); 129 }); 130 131 let origin = new Zdog.Shape({ 132 stroke: .5, 133 color: lace, 134 }); 135 let base = new Zdog.Shape({ 136 path: [ { z: -1.5 }, { z: 1.5 } ], 137 stroke: 1, 138 translate: { z: 3 }, 139 }); 140 new Zdog.Cone({ 141 addTo: base, 142 diameter: 2, 143 length: 1.5, 144 stroke: .5, 145 translate: { z: 1.5 }, 146 }); 147 let z = base.copyGraph({ 148 color: blueberry, 149 }); 150 z.children[0].color = blueberry; 151 let y = base.copyGraph({ 152 color: lime, 153 rotate: { x: -TAU/4 }, 154 translate: { y: 3 }, 155 }); 156 y.children[0].color = lime; 157 let x = base.copyGraph({ 158 color: rose, 159 rotate: { y: -TAU/4 }, 160 translate: { x: 3 }, 161 }); 162 x.children[0].color = rose; 163 targets.forEach(t => { 164 origin.copyGraph({ addTo: t, scale: 1/t.scale.x }); 165 z.copyGraph({ 166 addTo: t, 167 scale: 1/t.scale.x, 168 translate: { z: 3/t.scale.x }, 169 }); 170 y.copyGraph({ 171 addTo: t, 172 scale: 1/t.scale.x, 173 translate: { y: 3/t.scale.x }, 174 }); 175 x.copyGraph({ 176 addTo: t, 177 scale: 1/t.scale.x, 178 translate: { x: 3/t.scale.x }, 179 }); 180 }); 181 } 182 183 static get MODE_NONE() { return ''; } 184 static get MODE_X() { return 'x'; } 185 static get MODE_Y() { return 'y'; } 186 static get MODE_Z() { return 'z'; } 187 static get MODE_VIEW() { return 'v'; } 188} 189 190// TODO: I think this is going to need to run on basis vectors like getWorldTransforms. 191class RotateTool extends Tool { 192 constructor(editor) { 193 super(editor); 194 this.targets = null; 195 this.startRotate = null; 196 this.mode = RotateTool.MODE_NONE; 197 this.widget = null; 198 } 199 200 start(ptr, target, x, y) { 201 this.widget = target; 202 this.targets = this.editor.selection.slice(0); 203 this.mode = RotateTool.MODE_NONE; 204 if (!this.targets.length || target.layer !== Zoodle.LAYER_UI || !target.color) { 205 return; 206 } 207 208 this.startRotate = this.targets.map( t => t.rotate.copy() ); 209 210 switch (target.color) { 211 case rose: 212 this.mode = RotateTool.MODE_X; 213 break; 214 case lime: 215 this.mode = RotateTool.MODE_Y; 216 break; 217 case blueberry: 218 this.mode = RotateTool.MODE_Z; 219 break; 220 } 221 } 222 move( ptr, target, x, y ) { 223 if (!this.mode) { return; } 224 225 let displaySize = Math.min( this.editor.scene.width, this.editor.scene.height ); 226 x /= displaySize / Math.PI * Zdog.TAU; 227 y /= displaySize / Math.PI * Zdog.TAU; 228 let direction = this.widget.renderNormal; 229 let delta = this.editor.getAxisDistance(x, y, Math.atan2(direction.y, direction.x) + TAU/4 ); 230 this.targets.forEach((t, i) => { 231 t.rotate[this.mode] = this.startRotate[i][this.mode] + delta; 232 }); 233 this.editor.updateHighlights(); 234 this.editor.updateUI(); 235 this.editor.props.updatePanel(); 236 } 237 // TODO: Let's stash `delta` somewhere so we don't have to recalculate it in end() 238 end( ptr, target, x, y ) { 239 if (!this.mode) { return; } 240 // ensure any final adjustments are applied. 241 this.move( ptr, target, x, y ); 242 let displaySize = Math.min( this.editor.scene.width, this.editor.scene.height ); 243 x /= displaySize * Math.PI * Zdog.TAU; 244 y /= displaySize * Math.PI * Zdog.TAU; 245 let direction = this.widget.renderNormal; 246 let delta = this.editor.getAxisDistance( x, y, Math.atan2(direction.y, direction.x) + TAU/4 ); 247 let command = new RotateCommand(this.editor, this.targets, new Zdog.Vector({[this.mode]: delta})); 248 this.editor.did(command); 249 } 250 drawWidget(targets) { 251 // Create anchors matching selected objects. 252 targets = targets.map((target) => { 253 return new Zdog.Anchor({ 254 addTo: this.editor.ui, 255 ...this.editor.getWorldTransforms(target), 256 }); 257 }); 258 259 const widgetDiameter = 10; 260 const widgetStroke = 0.75; 261 262 let origin = new Zdog.Shape({ 263 stroke: .5, 264 color: lace, 265 }); 266 let zRing = new Zdog.Ellipse({ 267 diameter: widgetDiameter, 268 stroke: widgetStroke, 269 color: blueberry, 270 }); 271 let yRing = zRing.copyGraph({ 272 rotate: { x: TAU/4 }, 273 color: lime, 274 }); 275 let xRing = zRing.copyGraph({ 276 rotate: { y: TAU/4 }, 277 color: rose, 278 }); 279 targets.forEach(t => { 280 origin.copyGraph({ addTo: t, scale: 1/t.scale.x }); 281 zRing.copyGraph({ addTo: t, scale: 1/t.scale.x }); 282 yRing.copyGraph({ addTo: t, scale: 1/t.scale.x }); 283 xRing.copyGraph({ addTo: t, scale: 1/t.scale.x }); 284 }); 285 } 286 287 static get MODE_NONE() { return ''; } 288 static get MODE_X() { return 'x'; } 289 static get MODE_Y() { return 'y'; } 290 static get MODE_Z() { return 'z'; } 291} 292 293class Command { 294 constructor(editor) { 295 this.editor = editor; 296 } 297 do() {} 298 undo() {} 299} 300 301class SelectCommand extends Command { 302 constructor(editor, target, replace = false) { 303 super(editor); 304 this.replace = replace; 305 this.oldSelection = null; 306 307 // If this is a compositeChild, find its parent 308 while (target && target.compositeChild) { 309 target = target.addTo; 310 } 311 // Don't select root elements. 312 if (target && !target.addTo) { 313 target = null; 314 } 315 this.target = target; 316 } 317 do() { 318 this.oldSelection = this.editor.selection.slice( 0 ); 319 if (!this.target) { 320 this.editor.clearSelection(); 321 } else if (this.replace) { 322 this.editor.setSelection(this.target); 323 } else { 324 this.editor.toggleSelection(this.target); 325 } 326 this.refresh(); 327 } 328 undo() { 329 this.editor.selection = this.oldSelection; 330 this.refresh(); 331 } 332 refresh() { 333 this.editor.updateHighlights(); 334 this.editor.updateUI(); 335 this.editor.props.updatePanel(); 336 } 337} 338 339class TranslateCommand extends Command { 340 constructor(editor, target, delta, oldTranslate = null) { 341 super(editor); 342 if (!Array.isArray(target)) { 343 target = [target]; 344 } 345 this.target = target; 346 this.delta = delta; 347 this.oldTranslate = oldTranslate || target.map((t) => t.translate.copy()); 348 } 349 350 do() { 351 if (!this.target) return console.error("Doing TranslateCommand with no target."); 352 353 this.target.forEach((t, i) => { 354 t.translate.set(this.oldTranslate[i]).add(this.delta); 355 }); 356 this.refresh(); 357 } 358 undo() { 359 if (!this.target) return console.error("Undoing TranslateCommand with no target."); 360 361 this.target.forEach((t, i) => { 362 t.translate.set(this.oldTranslate[i]); 363 }); 364 this.refresh(); 365 } 366 refresh() { 367 this.editor.updateHighlights(); 368 this.editor.updateUI(); 369 this.editor.props.updatePanel(); 370 } 371} 372 373class RotateCommand extends Command { 374 // TODO: Add oldTranslate and oldRotate to the constructor, or maybe a flag to tell it if it's getting new or old transforms. 375 constructor(editor, target, delta) { 376 super(editor); 377 if (!Array.isArray(target)) { 378 target = [target]; 379 } 380 this.target = target; 381 this.delta = delta; 382 this.oldRotate = target.map((t) => t.rotate.copy()); 383 } 384 385 do() { 386 // TODO: Probably better to just throw in the constructor. 387 if (!this.target) return console.error("Doing RotateCommand with no target."); 388 389 this.target.forEach((t, i) => { 390 t.rotate.set(this.oldRotate[i]).add(this.delta); 391 }); 392 } 393 undo() { 394 if (!this.target) return console.error("Undoing RotateCommand with no target."); 395 396 this.target.forEach((t, i) => { 397 t.rotate.set(this.oldRotate[i]); 398 }); 399 } 400} 401 402class EditCommand extends Command { 403 constructor(editor, target, propId, value, oldValue = null) { 404 super(editor); 405 if (!target) { 406 throw new Error("No target specified for EditCommand"); 407 } 408 409 if (!Array.isArray(target)) { 410 target = [target]; 411 } 412 this.target = target; 413 this.propId = propId; 414 this.value = value; 415 this.oldValue = oldValue || target.map( (t) => t[propId]); 416 } 417 do() { 418 this.target.forEach( (t) => { 419 t[this.propId] = this.value; 420 if (t.updatePath) t.updatePath(); 421 }); 422 } 423 undo() { 424 this.target.forEach( (t, i) => { 425 t[this.propId] = this.oldValue[i]; 426 if (t.updatePath) t.updatePath(); 427 }); 428 } 429}